import { CellType } from "../constants/CellType";
import { Direction } from "../constants/Direction";
import { ExitType } from "../constants/ExitType";
import { PieceId, pieceMap } from "../constants/Pieces";
import { Exit } from "./Exit";
import { ExternalNode } from "./ExternalNode";
import { InternalNode } from "./InternalNode";
import { Piece } from "./Piece";
import { PlacedPiece } from "./PlacedPiece";

export class Cell {
  public readonly externalNodes: Map<Direction, ExternalNode>;
  public readonly cellType: CellType;
  public placedPiece?: {
    piece: PlacedPiece;
    id: PieceId;
  };

  constructor(cellType: CellType) {
    this.externalNodes = new Map([
      [Direction.NORTH, new ExternalNode(this, Direction.NORTH)],
      [Direction.SOUTH, new ExternalNode(this, Direction.SOUTH)],
      [Direction.EAST, new ExternalNode(this, Direction.EAST)],
      [Direction.WEST, new ExternalNode(this, Direction.WEST)],
    ]);
    this.cellType = cellType;
  }

  public getNodeAt(direction: Direction): ExternalNode {
    const node = this.externalNodes.get(direction);
    if (!node) throw Error(`Could not find node at ${direction}`);
    return node!;
  }

  /**
   * @param rotation in degrees counter-clockwise
   */
  public placePiece(pieceId: PieceId, rotation: 0 | 90 | 180 | 270) {
    if (this.placedPiece !== undefined) return;
    const piece: Piece = pieceMap[pieceId].rotate(rotation);

    this.validatePiecePlacement(piece);

    this.placedPiece = {
      piece: piece.toPlacedPiece(this),
      id: pieceId,
    };
  }

  private validatePiecePlacement(piece: Piece) {
    const hasAnyConnection = Array.from(piece.tracks)
      .map((track) => {
        const trackExternalNodes = [
          this.getNodeAt(track.joinedPoints.firstPoint),
        ];
        if (!(track.joinedPoints.secondPoint instanceof InternalNode)) {
          trackExternalNodes.push(
            this.getNodeAt(track.joinedPoints.secondPoint),
          );
        }
        return {
          trackExternalNodes: trackExternalNodes,
          trackType: track.type,
        };
      })
      .reduce((isConnected, { trackExternalNodes, trackType }) => {
        let isTrackConnected: boolean = false;
        trackExternalNodes
          .filter((node) => node.traverseBorder() instanceof Exit)
          .forEach((node) => {
            const exitType = (node.traverseBorder() as Exit).type;
            isTrackConnected = true;
            if (
              exitType !== ExitType.AMBIVALENT &&
              exitType.toString() !== trackType.toString()
            ) {
              throw Error(
                `Unable to place piece, invalid exit type at direction ${node.direction}`,
              );
            }
          });

        trackExternalNodes
          .filter((node) => node.traverseBorder() instanceof ExternalNode)
          .forEach((node) => {
            const adjacentExternalNode = node.traverseBorder() as ExternalNode;
            const adjacentTrack =
              adjacentExternalNode.cell.placedPiece?.piece.findTrackForDirection(
                adjacentExternalNode.direction,
              );
            if (adjacentTrack !== undefined) {
              isTrackConnected = true;
              if (adjacentTrack.type !== trackType) {
                throw Error(
                  `Unable to place piece next to another due to conflicting track types at ${node.direction}`,
                );
              }
            }
          });

        return isConnected || isTrackConnected;
      }, false);

    if (!hasAnyConnection) {
      throw Error("No adjacent exit or piece available to connect to");
    }
  }

  public removePiece() {
    this.placedPiece = undefined;
  }
}
