/*
 * Copyright (c) 2019 - 2020 Arnaud Vergnet.
 *
 * This file is part of Campus INSAT.
 *
 * Campus INSAT 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 3 of the License, or
 * (at your option) any later version.
 *
 * Campus INSAT 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 Campus INSAT.  If not, see <https://www.gnu.org/licenses/>.
 */

import ShapeL from '../Shapes/ShapeL';
import ShapeI from '../Shapes/ShapeI';
import ShapeJ from '../Shapes/ShapeJ';
import ShapeO from '../Shapes/ShapeO';
import ShapeS from '../Shapes/ShapeS';
import ShapeT from '../Shapes/ShapeT';
import ShapeZ from '../Shapes/ShapeZ';
import type { CoordinatesType } from '../Shapes/BaseShape';
import BaseShape from '../Shapes/BaseShape';
import type { GridType } from '../components/GridComponent';

/**
 * Class used as an abstraction layer for shapes.
 * Use this class to manipulate pieces rather than Shapes directly
 *
 */
export default class Piece {
  shapes = [ShapeL, ShapeI, ShapeJ, ShapeO, ShapeS, ShapeT, ShapeZ];

  currentShape: BaseShape;

  theme: ReactNativePaper.Theme;

  /**
   * Initializes this piece's color and shape
   *
   * @param theme Object containing current theme
   */
  constructor(theme: ReactNativePaper.Theme) {
    this.currentShape = this.getRandomShape(theme);
    this.theme = theme;
  }

  /**
   * Gets a random shape object
   *
   * @param theme Object containing current theme
   */
  getRandomShape(theme: ReactNativePaper.Theme): BaseShape {
    return new this.shapes[Math.floor(Math.random() * 7)](theme);
  }

  /**
   * Removes the piece from the given grid
   *
   * @param grid The grid to remove the piece from
   */
  removeFromGrid(grid: GridType) {
    const pos: Array<CoordinatesType> =
      this.currentShape.getCellsCoordinates(true);
    pos.forEach((coordinates: CoordinatesType) => {
      grid[coordinates.y][coordinates.x] = {
        color: this.theme.colors.tetrisBackground,
        isEmpty: true,
        key: grid[coordinates.y][coordinates.x].key,
      };
    });
  }

  /**
   * Adds this piece to the given grid
   *
   * @param grid The grid to add the piece to
   * @param isPreview Should we use this piece's current position to determine the cells?
   */
  toGrid(grid: GridType, isPreview: boolean) {
    const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
      !isPreview
    );
    pos.forEach((coordinates: CoordinatesType) => {
      grid[coordinates.y][coordinates.x] = {
        color: this.currentShape.getColor(),
        isEmpty: false,
        key: grid[coordinates.y][coordinates.x].key,
      };
    });
  }

  /**
   * Checks if the piece's current position is valid
   *
   * @param grid The current game grid
   * @param width The grid's width
   * @param height The grid's height
   * @return {boolean} If the position is valid
   */
  isPositionValid(grid: GridType, width: number, height: number): boolean {
    let isValid = true;
    const pos: Array<CoordinatesType> =
      this.currentShape.getCellsCoordinates(true);
    for (let i = 0; i < pos.length; i += 1) {
      if (
        pos[i].x >= width ||
        pos[i].x < 0 ||
        pos[i].y >= height ||
        pos[i].y < 0 ||
        !grid[pos[i].y][pos[i].x].isEmpty
      ) {
        isValid = false;
        break;
      }
    }
    return isValid;
  }

  /**
   * Tries to move the piece by the given offset on the given grid
   *
   * @param x Position X offset
   * @param y Position Y offset
   * @param grid The grid to move the piece on
   * @param width The grid's width
   * @param height The grid's height
   * @param freezeCallback Callback to use if the piece should freeze itself
   * @return {boolean} True if the move was valid, false otherwise
   */
  tryMove(
    x: number,
    y: number,
    grid: GridType,
    width: number,
    height: number,
    freezeCallback: () => void
  ): boolean {
    let newX = x;
    let newY = y;
    if (x > 1) {
      newX = 1;
    } // Prevent moving from more than one tile
    if (x < -1) {
      newX = -1;
    }
    if (y > 1) {
      newY = 1;
    }
    if (y < -1) {
      newY = -1;
    }
    if (x !== 0 && y !== 0) {
      newY = 0;
    } // Prevent diagonal movement

    this.removeFromGrid(grid);
    this.currentShape.move(newX, newY);
    const isValid = this.isPositionValid(grid, width, height);

    if (!isValid) {
      this.currentShape.move(-newX, -newY);
    }

    const shouldFreeze = !isValid && newY !== 0;
    this.toGrid(grid, false);
    if (shouldFreeze) {
      freezeCallback();
    }
    return isValid;
  }

  /**
   * Tries to rotate the piece
   *
   * @param grid The grid to rotate the piece on
   * @param width The grid's width
   * @param height The grid's height
   * @return {boolean} True if the rotation was valid, false otherwise
   */
  tryRotate(grid: GridType, width: number, height: number): boolean {
    this.removeFromGrid(grid);
    this.currentShape.rotate(true);
    if (!this.isPositionValid(grid, width, height)) {
      this.currentShape.rotate(false);
      this.toGrid(grid, false);
      return false;
    }
    this.toGrid(grid, false);
    return true;
  }

  /**
   * Gets this piece used cells coordinates
   *
   * @return {Array<CoordinatesType>} An array of coordinates
   */
  getCoordinates(): Array<CoordinatesType> {
    return this.currentShape.getCellsCoordinates(true);
  }

  getCurrentShape(): BaseShape {
    return this.currentShape;
  }
}
