/*
 * Copyright 
 *   2005 axYus - www.axyus.com
 *   2005 C.Marchand - christophe.marchand@axyus.com
 * 
 * This file is part of XEMELIOS.
 * 
 * XEMELIOS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * XEMELIOS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with XEMELIOS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package fr.gouv.finances.cp.xemelios.ui.ia;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JComponent;

/**
 * The board
 */
public class Grille extends JComponent implements Runnable, FocusListener, KeyListener {
    
    /**
     * Comment for <code>serialVersionUID</code>
     */
    private static final long serialVersionUID = 3257291348414771511L;
    public static final int DEFAULT_WIDTH=10, DEFAULT_HEIGHT=20;
    public static final int DIRECTION_UP=-1, DIRECTION_DOWN=1;

    private Thread m_Grille; // for animation
    private Color[][] grids; 
    private Color backColor; 
    private Piece[] pieces; // pieces that can move down
    private Piece currentPiece;
    public Piece nextOne;
    private int posX, posY; // position of currentPiece
    private int gridWidth, gridHeight;
    private int idCurrent, idNextOne;
    private boolean paintAll;
    private boolean paintPiece;
    private boolean gameRunning;
    private boolean gameOverPainted;
    public boolean gameFinished;
    public int cptLines, cptPoints;
    public int bonus, level;
    private int direction; // +1 : down, -1 : up

    protected DisplayScore ds = null;	// the score displayer, to notify it
    
    private boolean finished=false;	// controls thread execution

    // double-buffering
    private Image offscreenImg;
    private Image backImg;
    private Graphics offscreenG;

    public Grille(Piece[] lp) {
        this(lp,DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
    /**
     * Creates a new Grid
     * @param lp
     * @param w grid width
     * @param h grid height
     */
    public Grille(Piece[] lp,int w, int h) {
        super();
        gridWidth = w;
        gridHeight = h;
        direction = DIRECTION_DOWN;
        grids = new Color[gridHeight][gridWidth];
        pieces = lp;
        selectPiece();
        gameRunning = false;
        gameFinished = false;
        paintAll = false;
        cptLines = cptPoints = 0;
        level = 1;
        backColor = Color.darkGray;
        gameOverPainted = false;
        initListeners();
    }

    /**
     * Selects next piece
     */
    private void selectPiece() {
        if (nextOne == null) {
            // creates 2 new pieces
            idCurrent = (int)(Math.random() * pieces.length);
            idNextOne = (int)(Math.random() * pieces.length);
            currentPiece = new Piece(pieces[idCurrent]);
            nextOne = new Piece(pieces[idNextOne]);
        } else {
            // switches next to current
            currentPiece = nextOne;
            idCurrent = idNextOne;
            // creates the next one
            idNextOne = (int)(Math.random() * pieces.length);
            nextOne = new Piece(pieces[idNextOne]);
        }
        resetPiece();
        bonus = 20;
        paintPiece = true;
    }

    /**
     * Calculates if piece <p>p</p> can be displayed at<p>(x,y)</p>
     * @param x
     * @param y
     * @param p
     * @return
     */
    private boolean canDisplayAt(int x, int y, Piece p) {
        if (y < 0 || y + p.getHeight() > gridHeight)
            return false;
        if (x < 0 || x + p.getWidth() > gridWidth)
            return false;

        int mask = 0x8000;

        for (int i = 0; i < p.getHeight(); i++) {
            for (int j = 0; j < p.getWidth(); j++, mask >>= 1)
                if (((p.getVal() & mask) != 0) && grids[y + i][x + j] != null)
                    return false;
            // move mask to next line
            mask >>= 4 - p.getWidth();
        }
        return true;

    }
    
	/**
	 * Calculates if current piece can be displayed at <p>(x,y)</p> 
	 * @param x
	 * @param y
	 * @return
	 */
    private boolean canDisplayAt(int x, int y) {
        return canDisplayAt(x, y, currentPiece);
    }

    /**
     * Reverses game direction
     */
    private void reverseGame() {
        Color buffer;
        int i, j;

        for (i = 0; i < gridHeight / 2; i++) {
            for (j = 0; j < gridWidth; j++) {
                buffer = grids[i][j];
                grids[i][j] = grids[gridHeight - 1 - i][j];
                grids[gridHeight - 1 - i][j] = buffer;
            }
        }
    }

    private void resetPiece() {
        posX = (gridWidth / 2) - (currentPiece.getWidth() / 2);
        if (direction == 1) {
            posY = 0;
        } else {
            posY = gridHeight - currentPiece.getHeight();
        }
        currentPiece.reset(posX * Piece.SQUARE_SIZE, posY * Piece.SQUARE_SIZE);
    }

    /**
     * Drops filled lines, computes score, etc...
     */
    private void pieceStop() {
        int i, j, mask = 0x8000;
        int scr_i, scr_j; 
        int oldCptLines = cptLines;

        // update grid
        for (i = 0; i < currentPiece.getHeight(); i++) {
            for (j = 0; j < currentPiece.getWidth(); j++, mask >>= 1) {
                if ((currentPiece.getVal() & mask) != 0)
                    grids[posY + i][posX + j] = pieces[idCurrent].getColor();
            }
            // move mask to next line
            mask >>= 4 - currentPiece.getWidth();
        }

        // incs score
        cptPoints += 5;

        if (direction == DIRECTION_DOWN) {
            // check lines
            for (i = posY; i < posY + currentPiece.getHeight(); i++) {
                for (j = 0; j < gridWidth; j++) {
                    if (grids[i][j] == null)
                        break;
                }
                // line is filled
                if (j == gridWidth) {
                    // scrolls down
                    for (scr_i = i; scr_i > 0; scr_i--) {
                        for (scr_j = 0; scr_j < gridWidth; scr_j++) {
                            grids[scr_i][scr_j] = grids[scr_i - 1][scr_j];
                        }
                    }
                    for (scr_j = 0; scr_j < gridWidth; scr_j++) {
                        grids[0][scr_j] = null;
                    }
                    cptLines++;
                }
            }
        } else {
            for (i = posY + currentPiece.getHeight() - 1; i >= posY; i--) {
                for (j = 0; j < gridWidth; j++) {
                    if (grids[i][j] == null)
                        break;
                }
                if (j == gridWidth) {
                    for (scr_i = i; scr_i < gridHeight - 1; scr_i++) {
                        for (scr_j = 0; scr_j < gridWidth; scr_j++) {
                            grids[scr_i][scr_j] = grids[scr_i + 1][scr_j];
                        }
                    }
                    for (scr_j = 0; scr_j < gridWidth; scr_j++) {
                        grids[gridHeight - 1][scr_j] = null;
                    }
                    cptLines++;
                }
            }
        }
        if (oldCptLines != cptLines) {
            // 10 points a line
            cptPoints += (cptLines - oldCptLines) * 10;
            // 100 points a four
            if (cptLines - oldCptLines == 4)
                cptPoints += 100;
        }

        // max level = 12
        if (level < 12 && cptPoints >= 1000 * level)
            level++;

        paintAll = true;
        paintPiece = false;
        repaint();
    }
    @Override
    public Dimension getMinimumSize() {
        return getPreferredSize();
    }
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(Piece.SQUARE_SIZE*gridWidth,Piece.SQUARE_SIZE*gridHeight);
    }
    public void restartGame() {
        selectPiece();
        gameRunning = true;
        gameFinished = false;
        paintAll = true;
        cptLines = cptPoints = 0;
        level = 1;
        gameOverPainted = false;
        int i, j;
        for (i = 0; i < gridHeight; i++)
            for (j = 0; j < gridWidth; j++)
                grids[i][j] = null;
    }

    public void start() {
        if (m_Grille == null) {
            m_Grille = new Thread(this);
            finished=false;
            m_Grille.start();
        }
    }

    public void stop() {
        if (m_Grille != null) {
            finished=true;
            m_Grille = null;
        }
    }
    @Override
    public synchronized void update(Graphics g) {
        paint(g);
    }

    public synchronized void paintPiece(boolean effPiece) {
        if (offscreenImg == null)
            return;

        if (effPiece)
            currentPiece.erase(offscreenG, backColor);
        currentPiece.display(offscreenG);
    }

    private synchronized void paintGameOver() {
        Font laFont = new Font("Arial", Font.BOLD, 20);
        FontMetrics fm = getFontMetrics(laFont);
        Color c = new Color(0, 128, 224);

        for (int i = 0; i < gridHeight; i++)
            for (int j = 0; j < gridWidth; j++) {
                offscreenG.setColor(c);
                offscreenG.fill3DRect(j * Piece.SQUARE_SIZE, i * Piece.SQUARE_SIZE, Piece.SQUARE_SIZE, Piece.SQUARE_SIZE, true);
            }

        offscreenG.setColor(Color.yellow);
        offscreenG.setFont(laFont);
        offscreenG.drawString("Partie", (this.getSize().width - fm.stringWidth("Partie")) / 2, 32);
        offscreenG.drawString("terminée", (this.getSize().width - fm.stringWidth("terminée")) / 2, 64);

        String s;
        laFont = new Font("Arial", Font.BOLD, 14);
        fm = getFontMetrics(laFont);
        offscreenG.setColor(Color.white);
        offscreenG.setFont(laFont);
        s = "cliquez pour rejouer";
        offscreenG.drawString(s, (this.getSize().width - fm.stringWidth(s)) / 2, 220);

        gameOverPainted = true;
    }
    @Override
    public void paint(Graphics g) {
        if (offscreenImg == null) {
            offscreenImg = createImage(gridWidth * Piece.SQUARE_SIZE, gridHeight * Piece.SQUARE_SIZE);
            backImg = createImage(gridWidth * Piece.SQUARE_SIZE, gridHeight * Piece.SQUARE_SIZE);
            offscreenG = backImg.getGraphics();
            offscreenG.setColor(backColor);
            for (int i = 0; i < gridHeight; i++)
                for (int j = 0; j < gridWidth; j++)
                    offscreenG.fill3DRect(j * Piece.SQUARE_SIZE, i * Piece.SQUARE_SIZE, Piece.SQUARE_SIZE, Piece.SQUARE_SIZE, true);
            offscreenG = offscreenImg.getGraphics();
            offscreenG.drawImage(backImg, 0, 0, this);
        }

        if (!gameFinished) {
            // do we need to paint everything ?
            if (paintAll) {
                offscreenG.drawImage(backImg, 0, 0, this);
                for (int i = 0; i < gridHeight; i++) {
                    for (int j = 0; j < gridWidth; j++) {
                        if (grids[i][j] != null) {
                            offscreenG.setColor(grids[i][j]);
                            offscreenG.fill3DRect(j * Piece.SQUARE_SIZE, i * Piece.SQUARE_SIZE, Piece.SQUARE_SIZE, Piece.SQUARE_SIZE, true);
                        }
                    }
                }
                paintAll = false;

                if (paintPiece)
                    paintPiece(false);
            } else {
                paintPiece(true);
            }
        } else if (!gameOverPainted) {
            paintGameOver();
            paintAll = false; // do not erase the gameOver screen
        }

        // paint double-buffer
        g.drawImage(offscreenImg, 0, 0, this);
        ds.repaint();
    }

    /**
     * Moves current piece to its lowest position
     */
    public synchronized void goDown() {
        int cpt = 1; 

        posY++; 

        while(true) {
            // outside ?
            if (posY < 0 || posY + currentPiece.getHeight() > gridHeight) {
                posY--;
                currentPiece.goDown(cpt - 1);
                return;
            }

            int mask = 0x8000;

            // try to move down one line
            for (int i = 0; i < currentPiece.getHeight(); i++) {
                for (int j = 0; j < currentPiece.getWidth(); j++, mask >>= 1) {
                    if (((currentPiece.getVal() & mask) != 0) && grids[posY + i][posX + j] != null) {
                        posY--;
                        currentPiece.goDown(cpt - 1);
                        return;
                    }
                }
                // ok, moves it
                mask >>= 4 - currentPiece.getWidth();
            }
            cpt++;
            posY++;
        }
    }

    /**
     * Moves piece up to its highest position
     *
     */
    public synchronized void goUp() {
        int cpt = 1; 
        posY--;

        while(true) {
            if (posY < 0 || posY + currentPiece.getHeight() > gridHeight) {
                posY++;
                currentPiece.goUp(cpt - 1);
                return;
            }
            int mask = 0x8000;
            for (int i = 0; i < currentPiece.getHeight(); i++) {
                for (int j = 0; j < currentPiece.getWidth(); j++, mask >>= 1) {
                    if (((currentPiece.getVal() & mask) != 0) && grids[posY + i][posX + j] != null) {
                        posY++;
                        currentPiece.goUp(cpt - 1);
                        return;
                    }
                }
                mask >>= 4 - currentPiece.getWidth();
            }
            cpt++;
            posY--;
        }
    }

    /**
     * plays the animation
     */
    public void run() {
        while(!finished) {
            if (!gameOverPainted) {
                repaint();

                if (gameRunning && !gameFinished) {
                    bonus--;

                    if (direction == DIRECTION_DOWN) {
                        if (canDisplayAt(posX, posY + 1)) {
                            posY++;
                            currentPiece.goDown();
                        } else {
                            pieceStop();
                            selectPiece();
                            if (!canDisplayAt(posX, posY)) gameFinished = true;
                        }
                    } else {
                        if (canDisplayAt(posX, posY - 1)) {
                            posY--;
                            currentPiece.goUp();
                        } else {
                            pieceStop();
                            selectPiece();
                            if (!canDisplayAt(posX, posY)) gameFinished = true;
                        }
                    }
                }
            }

            try {
                Thread.sleep(300 - ((level - 1) * 20));
            } catch (InterruptedException e) {
                stop();
            }
        }
    }

    public void keyPressed(KeyEvent e) {
        int key = e.getKeyCode();
        
        if (key == 'p' || key == 'P') {
            gameRunning = !gameRunning;
        } else if (gameRunning) {
            switch (key) {
                case KeyEvent.VK_LEFT:
                    if (canDisplayAt(posX - 1, posY)) {
                        posX--;
                        currentPiece.goLeft();
                    }
                    break;
                case KeyEvent.VK_RIGHT:
                    if (canDisplayAt(posX + 1, posY)) {
                        posX++;
                        currentPiece.goRight();
                    }
                    break;
                case KeyEvent.VK_UP:
                    if (canDisplayAt(posX, posY, currentPiece.getNextPiece()))
                        currentPiece.rotate();
                    break;
                case KeyEvent.VK_DOWN:
                    if (bonus > 0)
                        cptPoints += bonus;
                    if (direction == 1)
                        goDown();
                    else goUp();
                    break;
                case 'i':
                case 'I':
                    reverseGame();
                    direction = -direction;
                    resetPiece();
                    bonus = 0;
                    paintAll = true;
                    break;
                default:
                    return;
            }
        }
        repaint();
    }
    public void keyTyped(KeyEvent e) {}
    public void keyReleased(KeyEvent e) {}
    
    public void focusLost(FocusEvent e) {
        gameRunning = false;
    }
    public void focusGained(FocusEvent e) {}

    public boolean mouseDown() {
        gameRunning = true;
        if (gameFinished) {
            restartGame();
            repaint();
        }
        return true;
    }
    
    private void initListeners() {
        addMouseListener(new MouseListener() {
            public void mouseClicked(MouseEvent e) {mouseDown();}
            public void mousePressed(MouseEvent e) {}
            public void mouseReleased(MouseEvent e) {}
            public void mouseEntered(MouseEvent e) {}
            public void mouseExited(MouseEvent e) {}
        });
        // listeners moves to JTRIS
    }
}
