/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package tetris2;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JButton;
import javax.swing.JPanel;

/**
 *
 * @author Fmunch
 */
public class TetrisPanel extends JPanel {
  // Different types of blocks

  static int TYPE_L = 0;
  static int TYPE_LR = 1;
  static int TYPE_T = 2;
  static int TYPE_B = 3;
  static int TYPE_Z = 4;
  static int TYPE_ZR = 5;
  static int TYPE_S = 6;
  ScoreRegistrar scorereg;
  String user;

  public void registerScore() {
    scorereg.registerScore(user, score);
  }

  public void setScoreRegistrar(ScoreRegistrar sr, String user_) {
    scorereg = sr;
    user = user_;
  }

  void setPause(boolean b) {
    pause = b;
    repaint();
  }

  boolean getPause() {
    return pause;
  }

  class Block {

    short[] matrix;
    int tbx, tby;
    int bw, bh;

    /**
     * Build a block
     * @param type Type of the block, either TYPE_L, TYPE_T, TYPE_LR
     * TYPE_Z,or TYPE_ZR depending it's form, R are reverse type
     * they are the same that without except with reversed chirality
     */
    Block(int type) {
      tbx = w / 2;
      tby = 0;
      if ((type == TYPE_L) || (type == TYPE_T) || (type == TYPE_LR) ||
              (type == TYPE_Z) || (type == TYPE_ZR)) {
        tbx -= 1;
        bw = 3;
        bh = 3;
        matrix = new short[bw * bh];
        Arrays.fill(matrix, (short) 0);
        if (type == TYPE_L) {
          matrix[1] = 1;
          matrix[4] = 1;
          matrix[7] = 1;
          matrix[8] = 1;
        }
        if (type == TYPE_T) {
          matrix[1] = 1;
          matrix[4] = 1;
          matrix[5] = 1;
          matrix[7] = 1;
        }
        if (type == TYPE_LR) {
          matrix[1] = 1;
          matrix[4] = 1;
          matrix[7] = 1;
          matrix[6] = 1;
        }
        if (type == TYPE_Z) {
          matrix[1] = 1;
          matrix[4] = 1;
          matrix[5] = 1;
          matrix[8] = 1;
        }
        if (type == TYPE_ZR) {
          matrix[1] = 1;
          matrix[4] = 1;
          matrix[3] = 1;
          matrix[6] = 1;
        }
      } else if (type == TYPE_B) {
        tbx -= 2;
        bw = 4;
        bh = 4;
        matrix = new short[bw * bh];
        Arrays.fill(matrix, (short) 0);
        matrix[2] = 1;
        matrix[6] = 1;
        matrix[10] = 1;
        matrix[14] = 1;
      } else if (type == TYPE_S) {
        tbx -= 1;
        bw = 2;
        bh = 2;
        matrix = new short[bw * bh];
        Arrays.fill(matrix, (short) 0);
        matrix[0] = 1;
        matrix[1] = 1;
        matrix[2] = 1;
        matrix[3] = 1;
      }
    }
    /**
     * rotate the block 90° Left
     */
    void rotateL() {
      short[] newblock = new short[bh * bw]; //F better to do
      for (int i = 0; i < bh; i++) {
        for (int j = 0; j < bw; j++) {
          int I = bw - j - 1;
          int J = i;
          newblock[I * bh + J] = matrix[i * bw + j];
        }
      }
      matrix = newblock;
      int tmp = bh;
      bh = bw;
      bw = tmp;
    }
    /**
     *  turn the block by 90° right
     */
    private void rotateR() {
      short[] newblock = new short[bh * bw]; //F better to do
      for (int i = 0; i < bh; i++) {
        for (int j = 0; j < bw; j++) {
          int I = j;
          int J = bh - i - 1;
          newblock[I * bh + J] = matrix[i * bw + j];
        }
      }
      matrix = newblock;
      int tmp = bh;
      bh = bw;
      bw = tmp;
    }
    /**
     * shift the block up ( for cancelling a downshift that is forbiden )
     */
    void shiftUp() {
      if (tby > 0) {
        tby--;
      }
    }
    /**
     * Shift the block down
     */
    void shiftDown() {
      tby++;
    }
    /**
     * Shift the block left
     */
    void shiftLeft() {
      tbx--;
    }

    /**
     * Shift the block right
     */
    void shiftRight() {
      tbx++;
    }
    // we could also check if two blocks are overlapping

    /**
     * Check if a block is overlapping a matrix
     * @param omatrix : matrix to check
     * @param w : width of the matrix
     * @param h : height of the matrix
     * @return
     */
    boolean isOverlaping(short[] omatrix, int w, int h) {
      for (int i = 0; i < bh; i++) {
        int X = i * bw;
        int oX = (tby + i) * w;
        for (int j = 0; j < bh; j++) {
          if (matrix[X + j] != 0) // if full then check
          {
            if ((tby + i) < 0 || (tby + i) >= h) {
              return true;
            }
            if ((tbx + j) < 0 || (tbx + j) >= w) {
              return true;
            }
            if (omatrix[oX + (tbx + j)] != 0) {
              return true;
            }
          }
        }
      }
      return false;
    }
    /**
     * Put the current block at the current position on the matrix
     * @param omatrix : matrix on which to write the block
     * @param w : width of this matrix
     * @param h : height of this matrix
     */
    private void dump(short[] omatrix, int w, int h) {
      for (int i = 0; i < bh; i++) {
        for (int j = 0; j < bw; j++) {
          if (matrix[i * bw + j] != 0) {
            omatrix[(tby + i) * w + tbx + j] = 1;
          }
        }
      }
    }
  }
  int w, h; // number of cells w*h
  short[] matrix; // matrix containing the filled cells
  short[] linecount; // number of elements on a line
  Block currblock, nextblock; // current block, and the next coming block
  Random rand; // random source
  boolean pause, end; // tells if game is finished or not
  List<TetrisGameType> gamestypes; // contains a list of game types
  int levelmax; // maximum of the current level
  int gtindex; // index of the game type
  int score_bincr;
  int score_lincr;
  long rate;
  Timer t;
  TimerTask tt = mkTT();

  /**
   * Define the type of the game
   * @param gt
   */
  public void setGameTypeList(List gt) {
    gamestypes = gt;
  }
  
  /**
   *  Set the list of all game types
   * @param tgt
   */
  public void setGameTypes(GameTypes<TetrisGameType> tgt) {
    gamestypes = new ArrayList<TetrisGameType>();
    gamestypes.addAll(tgt);
  }
  /**
   * Set a type of game ie : speed, the score increase for each block, and for
   * each lines, and the maximum score to get to go to the next level
   * @param tgt
   */
  private void setGameType(TetrisGameType tgt) {
    score_bincr = tgt.getBlockPoints();
    score_lincr = tgt.getLinePoints();
    levelmax = tgt.getStart();
    t.cancel();
    t = new Timer();
    tt = mkTT();
    t.schedule(tt, 0, tgt.getSpeed());
  }
  /**
   * Change the level of the current game
   * @param level
   */
  private void setGameType(int level) {
    gtindex = level;
    setGameType(gamestypes.get(level));
  }
  /**
   * Go the next game type in the list if it's possible
   * otherwise stay at the current level
   */
  public void goNextGameType() {
    gtindex++;
    if (gtindex >= gamestypes.size()) {
      gtindex = gamestypes.size();
    }
    setGameType(gamestypes.get(gtindex));
  }
  /**
   * Start a new tetris play
   */
  public TetrisPanel() {
    this(16, 24);
  }
  /**
   * Register testing buttons ( only for testing purpose )
   */
  private void registerTestButtons() {
    JButton jbn1 = new JButton("newL");
    JButton jbn2 = new JButton("newT");
    JButton jbn3 = new JButton("newB");
    jbn1.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        currblock = newBlock(TYPE_L);
        repaint();
      }
    });
    jbn2.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        currblock = newBlock(TYPE_T);
        repaint();
      }
    });
    jbn3.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        currblock = newBlock(TYPE_B);
        repaint();
      }
    });
    add(jbn1);
    add(jbn2);
    add(jbn3);
    JButton jb = new JButton("RotateL");
    jb.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        rotateL();
        repaint();
      }
    });
    JButton jbb = new JButton("RotateR");
    jbb.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        rotateR();
        repaint();
      }
    });
    add(jbb);
    JButton jb1 = new JButton("Shift down");
    jb1.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        tryshiftDown();
        repaint();
      }
    });
    add(jb);
    JButton jb2 = new JButton("shiftLeft");
    jb2.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        tryshiftLeft();
        repaint();
      }
    });
    add(jb);
    JButton jb3 = new JButton("shiftRight");
    jb3.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        tryshiftRight();
        repaint();
      }
    });
    JButton jbov = new JButton("isOverlaping");
    jbov.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        long t1 = System.nanoTime();
        boolean over = currblock.isOverlaping(matrix, w, h);
        long t2 = System.nanoTime();
        System.out.println((t2 - t1));
        System.out.println(over);
      }
    });
    add(jb);
    add(jb1);
    add(jb2);
    add(jb3);
    add(jbov);
  }
  /**
   * return a new TimerTask, that will be configured according to
   * a new rate, check if the currblock exists, and then try to put it down
   * if it's no more possible to put it down, then dump it to the current matrix
   * then take the nextblock as current block, build a new next block
   * end the game if the new current block is overlapping
   * and then register the score
   * check also the level, try to increase it
   * at the end repaint the panel
   * @return
   */
  TimerTask mkTT() {
    return new TimerTask() {

      @Override
      public void run() {
        if (pause || end) {
          return;
        }
        if (currblock == null) {
          nextblock = makeBlock();
          currblock = makeBlock();
        }
        if (!tryshiftDown()) {
          currblock.dump(matrix, w, h);
          updateCount(currblock);
          int[] lines = checkCount();
          deletelines(lines);
          if (nextblock == null) {
            nextblock = makeBlock();
          }
          currblock = nextblock;
          if (currblock.isOverlaping(matrix, w, h)) {
            end = true;
            registerScore();
          }
          nextblock = makeBlock();
          score += score_bincr;
          if (score >= levelmax) {
            goNextGameType();
          }
        }
        repaint();
      }
    };
  }
  /**
   * Build a new block, and rotate it 0 to 4 time
   * @return
   */
  public Block makeBlock() {
    int type = rand.nextInt(7);
    Block nblock = newBlock(type);
    int rot = rand.nextInt(4);
    for (int i = 0; i < rot; i++) {
      nblock.rotateL();
    }
    return nblock;
  }

  /**
   * Restart the game at another level, it reinitialize the whole game
   * and then setGameType to the required level
   * @param level
   */
  public void restart(int level) {
    nextblock = null;
    currblock = null;
    end = false;
    pause = false;
    score = 0;
    if (level > 0 && gamestypes != null && level < gamestypes.size()) {
      setGameType(level);
    } else {
      gtindex = level;
      rate = 144;
      score_bincr = 2;
      score_lincr = 10;
    }
    matrix = new short[w * h];
    linecount = new short[h];
    Arrays.fill(linecount, (short) 0);
  }
  /**
   * Build up a new TetrisPanel
   * Register keyEvents
   * change background color, request for the focus, diseable foccus elseware
   * lunch the TimerTask ( useless may be ? )
   * @param w_
   * @param h_
   */
  public TetrisPanel(int w_, int h_) {
    w = w_;
    h = h_;
    this.setBackground(Color.WHITE);
    rand = new Random();
    this.setFocusable(true);
    t = new Timer();
    restart(-1);
    this.addKeyListener(new KeyAdapter() {

      @Override
      public void keyPressed(KeyEvent e) {
        //System.out.println("Pressed : " + e );
        int keycode = e.getKeyCode();
        switch (keycode) {
          case KeyEvent.VK_LEFT:
            tryshiftLeft();
            break;
          case KeyEvent.VK_RIGHT:
            tryshiftRight();
            break;
          case KeyEvent.VK_UP:
            tryrotateL();
            break;
          case KeyEvent.VK_DOWN:
            tryrotateR();
            break;
          case KeyEvent.VK_SPACE:
            dropDown();
            break;
          case KeyEvent.VK_P:
            pause = !pause;
            repaint();
            break;
          case KeyEvent.VK_R:
            if (end) {
              restart(-1);
            }
        }

      }
    });
    // disable other buttons
    for (int l = 0; l < this.getComponentCount(); l++) {
      this.getComponent(l).setFocusable(false);
    }
    this.requestFocus();
    t.schedule(tt, 0, rate);
  }
  /**
   * Do the count agin once a line is formed
   * @param block
   */
  void updateCount(Block block) {
    for (int i = 0; i < block.bh; i++) {
      for (int j = 0; j < block.bw; j++) {
        if (block.matrix[i * block.bw + j] != 0) {
          linecount[block.tby + i]++;
        }
      }
    }
  }
  /**
   * Check if a line is full
   * @return
   */
  int[] checkCount() {
    int[] list = new int[w];
    int count = 0;
    for (int i = 0; i < linecount.length; i++) {
      if (linecount[i] == w) {
        list[count] = i;
        count++;
      }
    }
    int[] res = new int[count];
    for (int i = 0; i < count; i++) {
      res[i] = list[i];
    }
    score += (count * score_lincr);
    return res;
  }
  /**
   * Remove a list of line, line by lines
   * @param list
   */
  void deletelines(int[] list) {
    for (int line : list) {
      deleteline(line);
    }
  }
  /**
   * Remove a line of index line
   * @param line
   */
  void deleteline(int line) {
    for (int i = line - 1; i > 0; i--) {
      linecount[i + 1] = linecount[i];
      for (int j = 0; j < w; j++) {
        matrix[(i + 1) * w + j] = matrix[i * w + j];
      }
    }
  }

  /**
   * Create a new block of a given type
   * @param type
   * @return
   */
  Block newBlock(int type) {
    return new Block(type);
  }

  /**
   * Check a block ????
   * @param block
   * @param tbx
   * @param tby
   * @return
   */
  private boolean checkBlock(short block, int tbx, int tby) {

    return true;
  }
  /**
   * Rotatte the current block left
   */
  private void rotateL() {
    currblock.rotateL();
  }
  /**
   * rotatate the current block right
   */
  private void rotateR() {
    currblock.rotateR();
  }
  /**
   * shift the current block down
   */
  private void shiftDown() {
    currblock.shiftDown();
  }
  /**
   * Shift current block left
   */
  private void shiftLeft() {
    currblock.shiftLeft();
  }
  /**
   * Shift the current block right
   */
  private void shiftRight() {
    currblock.shiftRight();
  }
  /**
   * try to shift the current block down
   * cancel it if it's not possible
   * @return
   */
  private boolean tryshiftDown() {
    if (currblock == null) {
      return false;
    }
    currblock.shiftDown();
    if (currblock.isOverlaping(matrix, w, h)) {
      currblock.shiftUp();
      return false;
    }
    return true;
  }
  /**
   * put the block as much down as possible
   * once it's too far, then put it up again
   */
  private void dropDown() {
    while (!currblock.isOverlaping(matrix, w, h)) {
      currblock.shiftDown();
    }
    currblock.shiftUp();
  }
  /**
   * try to shift the block to left, it's it's bupping then cancel this action
   * calling shift right
   * @return
   */
  private boolean tryshiftLeft() {
    currblock.shiftLeft();
    if (currblock.isOverlaping(matrix, w, h)) {
      currblock.shiftRight();
      return false;
    }
    return true;
  }
  /**
   * try to shift the block to right, it's it's bupping then cancel this action
   * calling shift left
   * @return
   */
  private boolean tryshiftRight() {
    currblock.shiftRight();
    if (currblock.isOverlaping(matrix, w, h)) {
      currblock.shiftLeft();
      return false;
    }
    return true;
  }
  /**
   * Try to rotate a block left, if it don't work, then cancel it
   * @return
   */
  private boolean tryrotateL() {
    currblock.rotateL();
    if (currblock.isOverlaping(matrix, w, h)) {
      currblock.rotateR();
      return false;
    }
    return true;
  }
  /**
   * Try to rotate a block right, if it don't work, then cancel it
   * @return
   */
  private boolean tryrotateR() {
    currblock.rotateR();
    if (currblock.isOverlaping(matrix, w, h)) {
      currblock.rotateL();
      return false;
    }
    return true;
  }
  /**
   * Drawn a block
   * @param g : graphics on which to draw
   * @param block : block to draw
   * @param wx : cell width
   * @param wy : cell height
   */
  private void drawBlock(Graphics g, Block block, double wx, double wy) {
    if (block == null) {
      return;
    }
    double X = block.tbx * wx;
    double Y = block.tby * wy;
    drawBlock(g, block, wx, wy, X, Y);
  }
  /**
   * Draw a block at a given position
   * @param g : Graphics on which to draw
   * @param block : block to draw
   * @param wx : width of a cell
   * @param wy : height of a cell
   * @param X : position X
   * @param Y : position Y
   */
  private void drawBlock(Graphics g, Block block, double wx, double wy, double X, double Y) {
    if (block == null) {
      return;
    }
    for (int i = 0; i < block.bh; i++) {
      for (int j = 0; j < block.bw; j++) {
        if (block.matrix[i * block.bw + j] != 0) {
          paintBox(g, X + j * wx, Y + i * wy, wx, wy);
        }
      }
    }
  }

  /**
   * fill the matrix with random cell ( for testing only )
   */
  private void randomfill() {
    for (int i = 0; i < matrix.length; i++) {
      matrix[i] = (short) (Math.abs(rand.nextInt()) % (2));
    }
  }
  /**
   * Draw a cell, coloring it with red outside
   * @param g
   * @param sx
   * @param sy
   * @param sw
   * @param sh
   */
  void paintBox(Graphics g, double sx, double sy, double sw, double sh) {
    Graphics2D g2d = (Graphics2D) g;
    g2d.setColor(Color.BLACK);
    Rectangle2D rect = new Rectangle2D.Double(sx, sy, sw, sh);
    g2d.fill(rect);
    g2d.setColor(Color.RED.darker().darker());
    g2d.draw(rect);
  }
  static double wfact = 0.75;
  String pause_str = "paused";
  String end_str = "End";
  String score_str = "Score = %d";
  int score;

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g;
    g2d.drawString(String.format(score_str, score), (float) (getWidth() * wfact), 200f);
    g2d.setColor(Color.BLACK);
    g2d.draw(new Rectangle2D.Double(0.0, 0.0, getWidth() * wfact, getHeight()));
    g2d.setColor(Color.GRAY.brighter());
    g2d.fill(new Rectangle2D.Double(0.0, 0.0, getWidth() * wfact, getHeight()));
    double wx = ((double) wfact * getWidth()) / w;
    double wy = ((double) getHeight()) / h;
    drawBlock(g, nextblock, wx, wy, getWidth() * wfact, 0.0);
    if (pause) {
      g2d.setColor(Color.BLACK);
      Font font = g2d.getFont();
      font = font.deriveFont(16f).deriveFont(Font.BOLD);
      g2d.setFont(font);
      g2d.drawString(pause_str, (float) (wfact * getWidth() / 2.), (float) (getHeight() / 2.));
      return;
    }
    for (int i = 0; i < h; i++) {
      for (int j = 0; j < w; j++) {
        if (matrix[i * w + j] != 0) {
          paintBox(g, j * wx, i * wy, wx, wy);
        }
      }
    }
    drawBlock(g, currblock, wx, wy);
    if (end) {
      g2d.setColor(Color.RED);
      Font font = g2d.getFont();
      font = font.deriveFont(16f).deriveFont(Font.BOLD);
      g2d.setFont(font);
      g2d.drawString(end_str, (float) (wfact * getWidth() / 2.), (float) (getHeight() / 2.));
    }
  }
}

