package game2048;

import java.util.Formatter;
import java.util.Observable;


/** The state of a game of 2048.
 *  @author Tianye Meng
 */
public class Model extends Observable {
    /** Current contents of the board. */
    private Board _board;
    /** Current score. */
    private int _score;
    /** Maximum score so far.  Updated when game ends. */
    private int _maxScore;
    /** True iff game is ended. */
    private boolean _gameOver;

    /* Coordinate System: column C, row R of the board (where row 0,
     * column 0 is the lower-left corner of the board) will correspond
     * to board.tile(c, r).  Be careful! It works like (x, y) coordinates.
     */

    /** Largest piece value. */
    public static final int MAX_PIECE = 2048;

    /** A new 2048 game on a board of size SIZE with no pieces
     *  and score 0. */
    public Model(int size) {
        _board = new Board(size);
    }

    /** A new 2048 game where RAWVALUES contain the values of the tiles
     * (0 if null). VALUES is indexed by (row, col) with (0, 0) corresponding
     * to the bottom-left corner. Used for testing purposes. */
    public Model(int[][] rawValues, int score, int maxScore, boolean gameOver) {
        _board = new Board(rawValues, score);
        _maxScore = maxScore;
        _gameOver = gameOver;
    }

    /** Return the current Tile at (COL, ROW), where 0 <= ROW < size(),
     *  0 <= COL < size(). Returns null if there is no tile there.
     *  Used for testing. Should be deprecated and removed.
     */
    public Tile tile(int col, int row) {
        return _board.tile(col, row);
    }

    /** Return the number of squares on one side of the board.
     *  Used for testing. Should be deprecated and removed. */
    public int size() {
        return _board.size();
    }

    /** Return true iff the game is over (there are no moves, or
     *  there is a tile with value 2048 on the board). */
    public boolean gameOver() {
        checkGameOver();
        if (_gameOver) {
            _maxScore = Math.max(_score, _maxScore);
        }
        return _gameOver;
    }

    /** Return the current score. */
    public int score() {
        return _score;
    }

    /** Return the current maximum game score (updated at end of game). */
    public int maxScore() {
        return _maxScore;
    }

    /** Clear the board to empty and reset the score. */
    public void clear() {
        _score = 0;
        _gameOver = false;
        _board.clear();
        setChanged();
    }

    /** Add TILE to the board. There must be no Tile currently at the
     *  same position. */
    public void addTile(Tile tile) {
        _board.addTile(tile);
        checkGameOver();
        setChanged();
    }

    /** Tilt the board toward SIDE. Return true iff this changes the board.
     *
     * 1. If two Tile objects are adjacent in the direction of motion and have
     *    the same value, they are merged into one Tile of twice the original
     *    value and that new value is added to the score instance variable
     * 2. A tile that is the result of a merge will not merge again on that
     *    tilt. So each move, every tile will only ever be part of at most one
     *    merge (perhaps zero).
     * 3. When three adjacent tiles in the direction of motion have the same
     *    value, then the leading two tiles in the direction of motion merge,
     *    and the trailing tile does not.
     */
    //if moving a tile causes merge (if move(c, r, t)), then update score & changed
    public boolean tilt(Side side) {
        boolean changed;
        changed = false;


        _board.setViewingPerspective(side);
        for (int i = 0; i < _board.size(); i ++){
            if(changed == false)
                changed = colchanger(i);
            else{
                colchanger(i);
            }
            System.out.println("in main func changed is "+changed);
        }
        _board.setViewingPerspective(Side.NORTH);

        checkGameOver();
        if (changed) {
            setChanged();
        }
        return changed;
    }

    private boolean colchanger (int column){
        boolean changed = false;
        int[] merged = {-1, -1};
        for(int i = _board.size()-2; i >=0; i--){
            Tile t = _board.tile(column, i);
            if(t != null){
                for (int j = i+1; j < _board.size(); j++) {
                    if (j != _board.size() - 1 && _board.tile(column, j) == null) {
                        System.out.println("the next slot is empty, continue checking");
                            continue;
                    }
                    else if (j == _board.size() -1 && _board.tile(column, j) == null) {
                        System.out.println("the last slot is empty, move to the last slot");
                        changed = true;//_board.move(column, j, t);
                        _board.move(column, _board.size()-1, t);
                    }
                    else if (_board.tile(column, j).value() == t.value() && !contains(merged, j)) {//j != merged
                        System.out.println("t "+t.value()+" and "+tile(column, j).value()+" are merged");
                        changed = true;//_board.2move(column, j, t);
                        if(merged[0] == -1)
                            merged[0] = j;
                        else{
                            merged[1] =j;
                        }
                        //merged = j;
                        _board.move(column, j, t);
                    }
                    else if (contains(merged, -1) &&
                            _board.tile(column, j).value() != t.value() ||
                            (_board.tile(column, j).value() == t.value() && contains(merged, j))){//j == merged
                        changed = true;//_board.move(column, j - 1, t);
                        System.out.println("there is a non-empty slot stops current movement");
                        System.out.println("column now is"+_board.tile(column, 3)+_board.tile(column, 2)+
                                _board.tile(column, 1)+_board.tile(column, 0));
                        System.out.println("merged is "+merged[0]+" and "+merged[1]);
                        System.out.println("the position of t is "+t.row()+" and final pos is at "+ (j-1));
                        System.out.println("final position value is"+_board.tile(column, j-1)+", and t value is"+t.value());
                        _board.move(column, j - 1, t);
                    }
                }

            }
        }
        System.out.println("changed in colchanger is "+changed);

        return changed;
    }
    private boolean contains(int[] arr, int num){
        for(int i = 0; i< arr.length; i++){
            if (arr[i] == num)
                return true;
        }
        return false;
    }
    /** Checks if the game is over and sets the gameOver variable
     *  appropriately.
     */
    private void checkGameOver() {
        _gameOver = checkGameOver(_board);
    }

    /** Determine whether game is over. */
    private static boolean checkGameOver(Board b) {
        return maxTileExists(b) || !atLeastOneMoveExists(b);
    }

    /** Returns true if at least one space on the Board is empty.
     *  Empty spaces are stored as null.
     */
    public static boolean emptySpaceExists(Board b) {
        for(int i = 0; i < b.size(); i++){
            for (int j = 0; j< b.size(); j++){
                if (b.tile(i, j) == null)
                    return true;
            }
        }
        return false;
    }

    /**
     * Returns true if any tile is equal to the maximum valid value.
     * Maximum valid value is given by MAX_PIECE. Note that
     * given a Tile object t, we get its value with t.value().
     */
    public static boolean maxTileExists(Board b) {
        for(int i = 0; i < b.size(); i++){
            for (int j = 0; j< b.size(); j++){
                if (b.tile(i, j) != null && b.tile(i, j).value() == MAX_PIECE)
                    return true;
            }
        }
        return false;
    }

    /**
     * Returns true if there are any valid moves on the board.
     * There are two ways that there can be valid moves:
     * 1. There is at least one empty space on the board.
     * 2. There are two adjacent tiles with the same value.
     */
    public static boolean atLeastOneMoveExists(Board b) {
        if(emptySpaceExists(b)){
            return true;
        }
        for(int i = 0; i < b.size(); i++){
            for (int j = 0; j < b.size()-1; j++){
                if (b.tile(i, j).value() == b.tile(i, j+1).value()){
                    return true;
                }
            }
        }
        for(int i = 0; i < b.size(); i++){
            for (int j = 0; j < b.size()-1; j++){
                if (b.tile(j, i).value() == b.tile(j+1, i).value()){
                    return true;
                }
            }
        }
        return false;
    }

    /** Returns the model as a string, used for debugging. */
    @Override
    public String toString() {
        Formatter out = new Formatter();
        out.format("%n[%n");
        for (int row = size() - 1; row >= 0; row -= 1) {
            for (int col = 0; col < size(); col += 1) {
                if (tile(col, row) == null) {
                    out.format("|    ");
                } else {
                    out.format("|%4d", tile(col, row).value());
                }
            }
            out.format("|%n");
        }
        String over = gameOver() ? "over" : "not over";
        out.format("] %d (max: %d) (game is %s) %n", score(), maxScore(), over);
        return out.toString();
    }

    /** Returns whether two models are equal. */
    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        } else if (getClass() != o.getClass()) {
            return false;
        } else {
            return toString().equals(o.toString());
        }
    }

    /** Returns hash code of Model’s string. */
    @Override
    public int hashCode() {
        return toString().hashCode();
    }
}
