import { BoardElement, Color, Size, CoreRequest } from "core-types";
import { assertNever } from "../assertNever";

const MARGIN = 20;
const BOARD_WIDTH = 800;
const BOARD_HEIGHT = 800;

type Pixel = { x: number; y: number };
type Coordinate = { column: number; row: number };

export interface GoBoardProps {
  board: BoardElement;
  onClick: (_: CoreRequest) => void;
}

export class GoBoard {
  private board: BoardElement;
  private pen: Pen;
  private cursorLocation: Coordinate | null;
  private backgroundBoard: HTMLCanvasElement;
  private currentBoardState: HTMLCanvasElement;
  canvas: HTMLCanvasElement;

  constructor({ board, onClick }: GoBoardProps) {
    this.board = board;

    this.canvas = document.createElement("canvas");
    this.canvas.classList.add("board");
    this.canvas.width = BOARD_WIDTH;
    this.canvas.height = BOARD_HEIGHT;

    this.pen = new Pen(
      this.canvas.width,
      this.canvas.height,
      MARGIN,
      this.board.size.width,
      this.board.size.height
    );

    this.backgroundBoard = document.createElement("canvas");
    this.backgroundBoard.width = BOARD_WIDTH;
    this.backgroundBoard.height = BOARD_HEIGHT;
    this.renderBackgroundBoard();

    this.currentBoardState = document.createElement("canvas");
    this.currentBoardState.width = BOARD_WIDTH;
    this.currentBoardState.height = BOARD_HEIGHT;

    this.cursorLocation = null;

    this.canvas.onmousemove = (event) => {
      const bounds = this.canvas.getBoundingClientRect();
      const coordinate = {
        x: event.clientX - bounds.x,
        y: event.clientY - bounds.y,
      };

      let address = this.pen.address(coordinate);

      if (this.cursorLocation != address) {
        this.cursorLocation = this.pen.address(coordinate);
        this.renderBoard();
      }
    };

    this.canvas.onclick = (_) => {
      if (this.cursorLocation) {
        const intersection =
          this.board.spaces[boardAddress(this.board.size, this.cursorLocation)];
        switch (intersection.type) {
          case "Unplayable":
            break;
          case "Empty":
            onClick(intersection.content);
            break;
          case "Filled":
            break;
          default:
            assertNever(intersection);
        }
      }
    };
  }

  setBoard(board: BoardElement) {
    this.board = board;

    this.pen = new Pen(
      this.canvas.width,
      this.canvas.height,
      MARGIN,
      this.board.size.width,
      this.board.size.height
    );
    this.renderBoard();
  }

  renderBoard() {
    // Cache:
    // - Standard blank board with background
    // - Current board state without the ghost stone
    const ctx = this.canvas.getContext("2d");
    if (!ctx) {
      alert("could not get the canvas context");
      return null;
    }

    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    ctx.drawImage(this.backgroundBoard, 0, 0);

    let col = 0;
    let row = 0;
    for (let idx = 0; idx < this.board.spaces.length; idx++) {
      const space = this.board.spaces[idx];
      switch (space.type) {
        case "Filled":
          this.pen.stone(ctx, { column: col, row: row }, space.content.color);
          break;
        default:
          break;
      }
      col = col + 1;
      if (col == this.board.size.width) {
        col = 0;
        row = row + 1;
      }
    }

    if (this.cursorLocation) {
      this.pen.ghostStone(ctx, this.cursorLocation, Color.White);
    }
  }

  private renderBackgroundBoard() {
    const ctx = this.backgroundBoard.getContext("2d");
    if (!ctx) {
      alert("could not get the background canvas context");
      return;
    }
    ctx.lineWidth = 2;
    ctx.strokeStyle = "black";
    ctx.beginPath();
    for (var col = 0; col < this.board.size.width; col++) {
      ctx.moveTo(MARGIN + col * this.pen.hspaceBetween, MARGIN);
      ctx.lineTo(
        MARGIN + col * this.pen.hspaceBetween,
        MARGIN + (this.board.size.height - 1) * this.pen.vspaceBetween
      );
    }
    for (var row = 0; row < this.board.size.height; row++) {
      ctx.moveTo(MARGIN, MARGIN + row * this.pen.vspaceBetween);
      ctx.lineTo(
        MARGIN + (this.board.size.width - 1) * this.pen.hspaceBetween,
        MARGIN + row * this.pen.vspaceBetween
      );
    }
    ctx.closePath();
    ctx.stroke();

    this.pen.starPoint(ctx, { column: 3, row: 3 });
    this.pen.starPoint(ctx, { column: 3, row: 9 });
    this.pen.starPoint(ctx, { column: 3, row: 15 });
    this.pen.starPoint(ctx, { column: 9, row: 3 });
    this.pen.starPoint(ctx, { column: 9, row: 9 });
    this.pen.starPoint(ctx, { column: 9, row: 15 });
    this.pen.starPoint(ctx, { column: 15, row: 3 });
    this.pen.starPoint(ctx, { column: 15, row: 9 });
    this.pen.starPoint(ctx, { column: 15, row: 15 });
  }
}

class Pen {
  margin: number;
  hspaceBetween: number;
  vspaceBetween: number;

  constructor(
    width: number,
    height: number,
    margin: number,
    columns: number,
    rows: number
  ) {
    this.margin = margin;
    this.hspaceBetween = (width - margin * 2) / (columns - 1);
    this.vspaceBetween = (height - margin * 2) / (rows - 1);
  }

  starPoint(ctx: CanvasRenderingContext2D, addr: Coordinate) {
    ctx.fillStyle = "rgba(0, 0, 0, 1.0);";
    ctx.beginPath();
    const pixel = this.position(addr);
    ctx.moveTo(pixel.x, pixel.y);
    ctx.arc(pixel.x, pixel.y, 5, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.fill();
  }

  ghostStone(ctx: CanvasRenderingContext2D, addr: Coordinate, color: Color) {
    switch (color) {
      case Color.White:
        ctx.fillStyle = "rgba(230, 230, 230, 0.5)";
        break;
      case Color.Black:
        ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
        break;
    }

    this.drawStone(ctx, addr);
  }

  stone(ctx: CanvasRenderingContext2D, addr: Coordinate, color: Color) {
    switch (color) {
      case Color.White:
        ctx.fillStyle = "rgb(230, 230, 230)";
        break;
      case Color.Black:
        ctx.fillStyle = "rgb(0, 0, 0)";
        break;
    }

    this.drawStone(ctx, addr);
  }

  drawStone(ctx: CanvasRenderingContext2D, addr: Coordinate) {
    ctx.beginPath();
    const radius = this.hspaceBetween / 2 - 2;
    const pixel = this.position(addr);
    ctx.moveTo(pixel.x, pixel.y);
    ctx.arc(pixel.x, pixel.y, radius, 0, 2.0 * Math.PI);
    ctx.closePath();
    ctx.fill();
  }

  position(addr: Coordinate): Pixel {
    return {
      x: this.margin + addr.column * this.hspaceBetween,
      y: this.margin + addr.row * this.vspaceBetween,
    };
  }

  address(pixel: Pixel): Coordinate | null {
    if (
      Math.round(pixel.x) < this.margin ||
      Math.round(pixel.y) < this.margin
    ) {
      return null;
    } else {
      return {
        column: Math.round(
          (Math.round(pixel.x) - this.margin) / this.hspaceBetween
        ),
        row: Math.round(
          (Math.round(pixel.y) - this.margin) / this.vspaceBetween
        ),
      };
    }
  }
}

const boardAddress = (size: Size, coordinate: Coordinate): number =>
  coordinate.column + size.width * coordinate.row;
