package io.github.lonamiwebs.klooni.game;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import io.github.lonamiwebs.klooni.Klooni;
import io.github.lonamiwebs.klooni.serializer.BinSerializable;

// Represents the on screen board, with all the put cells
// and functions to determine when it is game over given a PieceHolder
public class Board implements BinSerializable {

    //region Members

    public final int cellCount;
    public float cellSize;
    private Cell[][] cells;

    final Vector2 pos;
    private final Sound stripClearSound;

    // Used to animate cleared cells vanishing
    private final Vector2 lastPutPiecePos;

    //endregion

    //region Constructor

    public Board(final GameLayout layout, int cellCount) {
        this.cellCount = cellCount;

        stripClearSound = Gdx.audio.newSound(Gdx.files.internal("sound/strip_clear.mp3"));

        lastPutPiecePos = new Vector2();
        pos = new Vector2();

        // Cell size depends on the layout to be updated first
        layout.update(this);
        cells = new Cell[this.cellCount][this.cellCount];
        for (int i = 0; i < this.cellCount; ++i) {
            for (int j = 0; j < this.cellCount; ++j) {
                cells[i][j] = new Cell(
                        pos.x + j * cellSize, pos.y + i * cellSize, cellSize);
            }
        }
    }

    //endregion

    //region Private methods

    // True if the given cell coordinates are inside the bounds of the board
    private boolean inBounds(int x, int y) {
        return x >= 0 && x < cellCount && y >= 0 && y < cellCount;
    }

    // True if the given piece at the given coordinates is not outside the bounds of the board
    private boolean inBounds(Piece piece, int x, int y) {
        return inBounds(x, y) && inBounds(x + piece.cellCols - 1, y + piece.cellRows - 1);
    }

    // This only tests for the piece on the given coordinates, not the whole board
    private boolean canPutPiece(Piece piece, int x, int y) {
        if (!inBounds(piece, x, y))
            return false;

        for (int i = 0; i < piece.cellRows; ++i)
            for (int j = 0; j < piece.cellCols; ++j)
                if (!cells[y+i][x+j].isEmpty() && piece.filled(i, j))
                    return false;

        return true;
    }

    // Returns true iff the piece was put on the board
    private boolean putPiece(Piece piece, int x, int y) {
        if (!canPutPiece(piece, x, y))
            return false;

        lastPutPiecePos.set(piece.calculateGravityCenter());
        for (int i = 0; i < piece.cellRows; ++i)
            for (int j = 0; j < piece.cellCols; ++j)
                if (piece.filled(i, j))
                    cells[y+i][x+j].set(piece.colorIndex);

        return true;
    }

    //endregion

    //region Public methods

    public void draw(SpriteBatch batch) {
        for (int i = 0; i < cellCount; ++i)
            for (int j = 0; j < cellCount; ++j)
                cells[i][j].draw(batch);
    }

    public boolean canPutPiece(Piece piece) {
        for (int i = 0; i < cellCount; ++i)
            for (int j = 0; j < cellCount; ++j)
                if (canPutPiece(piece, j, i))
                    return true;

        return false;
    }

    boolean putScreenPiece(Piece piece) {
        // Convert the on screen coordinates of the piece to the local-board-space coordinates
        // This is done by subtracting the piece coordinates from the board coordinates
        Vector2 local = piece.pos.cpy().sub(pos);
        int x = MathUtils.round(local.x / piece.cellSize);
        int y = MathUtils.round(local.y / piece.cellSize);
        return putPiece(piece, x, y);
    }

    Vector2 snapToGrid(final Piece piece, final Vector2 position) {
        // Snaps the given position (e.g. mouse) to the grid,
        // assuming piece wants to be put at the specified position.
        // If the piece was not on the grid, the original position is returned
        //
        // Logic to determine the x and y is a copy-paste from putScreenPiece
        final Vector2 local = position.cpy().sub(pos);
        int x = MathUtils.round(local.x / piece.cellSize);
        int y = MathUtils.round(local.y / piece.cellSize);
        if (canPutPiece(piece, x, y))
            return new Vector2(pos.x + x * piece.cellSize, pos.y + y * piece.cellSize);
        else
            return position;
    }

    // This will clear both complete rows and columns, all at once.
    // The reason why we can't check first rows and then columns
    // (or vice versa) is because the following case (* filled, _ empty):
    //
    // 4x4 boardHeight    piece
    // _ _ * *      * *
    // _ * * *      *
    // * * _ _
    // * * _ _
    //
    // If the piece is put on the top left corner, all the cells will be cleared.
    // If we first cleared the columns, then the rows wouldn't have been cleared.
    public int clearComplete() {
        int clearCount = 0;
        boolean[] clearedRows = new boolean[cellCount];
        boolean[] clearedCols = new boolean[cellCount];

        // Analyze rows and columns that will be cleared
        for (int i = 0; i < cellCount; ++i) {
            clearedRows[i] = true;
            for (int j = 0; j < cellCount; ++j) {
                if (cells[i][j].isEmpty()) {
                    clearedRows[i] = false;
                    break;
                }
            }
            if (clearedRows[i])
                clearCount++;
        }
        for (int j = 0; j < cellCount; ++j) {
            clearedCols[j] = true;
            for (int i = 0; i < cellCount; ++i) {
                if (cells[i][j].isEmpty()) {
                    clearedCols[j] = false;
                    break;
                }
            }
            if (clearedCols[j])
                clearCount++;
        }
        if (clearCount > 0) {
            float pan = 0;

            // Do clear those rows and columns
            for (int i = 0; i < cellCount; ++i)
                if (clearedRows[i])
                    for (int j = 0; j < cellCount; ++j)
                        cells[i][j].vanish(lastPutPiecePos);

            for (int j = 0; j < cellCount; ++j) {
                if (clearedCols[j]) {
                    pan += 2f * (j - cellCount / 2) / (float)cellCount;
                    for (int i = 0; i < cellCount; ++i) {
                        cells[i][j].vanish(lastPutPiecePos);
                    }
                }
            }

            if (Klooni.soundsEnabled()) {
                pan = MathUtils.clamp(pan, -1, 1);
                stripClearSound.play(
                        MathUtils.random(0.7f, 1f), MathUtils.random(0.8f, 1.2f), pan);
            }
        }

        return clearCount;
    }

    //endregion

    //region Serialization

    @Override
    public void write(DataOutputStream out) throws IOException {
        // Cell count, cells in row-major order
        out.writeInt(cellCount);
        for (int i = 0; i < cellCount; ++i)
            for (int j = 0; j < cellCount; ++j)
                cells[i][j].write(out);
    }

    @Override
    public void read(DataInputStream in) throws IOException {
        // If the saved cell count does not match the current cell count,
        // then an IOException is thrown since the data saved was invalid
        final int savedCellCount = in.readInt();
        if (savedCellCount != cellCount)
            throw new IOException("Invalid cellCount saved.");

        for (int i = 0; i < cellCount; ++i)
            for (int j = 0; j < cellCount; ++j)
                cells[i][j].read(in);
    }

    //endregion
}
