import { Entity, EntityNames, FunctionBox, Key, Particles } from ".";
import {
  BoundingBox,
  Colliding,
  ComponentNames,
  Grid,
  Highlight,
  Interactable,
  LambdaTerm,
  Modal,
  Sprite,
} from "../components";
import {
  Failure,
  IMAGES,
  LambdaTransformSound,
  SOUNDS,
  SPRITE_SPECS,
  SpriteSpec,
  Sprites,
} from "../config";
import { Coord2D, Direction } from "../interfaces";
import { Game } from "..";
import { Grid as GridSystem, SystemNames } from "../systems";
import { colors, tryWrap } from "../utils";
import {
  DebrujinIndex,
  SymbolTable,
  Visitors,
  emitNamed,
  interpret,
} from "../../interpreter";

const APPLICATION_RESULTS: Record<
  string,
  (gridPosition: Coord2D) => null | Entity
> = {
  _KEY: (gridPosition: Coord2D) => new Key(gridPosition),
  _EMPTY: (_gridPosition: Coord2D) => null,
};

export class FunctionApplication extends Entity {
  private static spriteSpec = SPRITE_SPECS.get(Sprites.BUBBLE) as SpriteSpec;

  constructor(gridPosition: Coord2D, lambdaTerm: string) {
    super(EntityNames.FunctionApplication);

    const dimension = {
      width: FunctionApplication.spriteSpec.width,
      height: FunctionApplication.spriteSpec.height,
    };
    this.addComponent(
      new BoundingBox(
        {
          x: 0,
          y: 0,
        },
        dimension,
        0,
      ),
    );

    this.addComponent(new Grid(gridPosition));

    this.addComponent(new LambdaTerm(lambdaTerm));

    this.addComponent(
      new Sprite(
        IMAGES.get(FunctionApplication.spriteSpec.sheet)!,
        { x: 0, y: 0 },
        dimension,
        FunctionApplication.spriteSpec.msPerFrame,
        FunctionApplication.spriteSpec.frames,
      ),
    );

    this.addComponent(new Colliding(this.handleCollision.bind(this)));

    this.addComponent(
      new Highlight(this.onHighlight.bind(this), this.onUnhighlight.bind(this)),
    );
  }

  private onHighlight(_direction: Direction) {
    this.addComponent(new Interactable(this.interaction.bind(this)));
  }

  private interaction() {
    const codeConsumer = (_code: string) => {
      this.removeComponent(ComponentNames.Modal);
      return { consumed: true };
    };
    const { last, code } = this.getComponent<LambdaTerm>(
      ComponentNames.LambdaTerm,
    );
    this.addComponent(
      new Modal({
        type: "CODE_EDITOR",
        codeInit: {
          code,
          codeConsumer,
          readonly: true,
          result: {
            error: last?.error && `Error: ${last.error.message}`,
            data: last?.data && `Last Result: ${emitNamed(last.data)}`,
          },
        },
      }),
    );
  }

  private onUnhighlight() {
    this.removeComponent(ComponentNames.Modal);
    this.removeComponent(ComponentNames.Interactable);
  }

  public handleCollision(game: Game, entity: Entity) {
    if (entity.name !== EntityNames.FunctionBox) {
      return;
    }

    const entityGrid = entity.getComponent<Grid>(ComponentNames.Grid);
    if (entityGrid.movingDirection !== Direction.NONE) {
      // prevent recursive functionBox -> application creation
      return;
    }

    const grid = this.getComponent<Grid>(ComponentNames.Grid);
    const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
    const fail = () => {
      entityGrid.movingDirection = gridSystem.oppositeDirection(
        entityGrid.previousDirection,
      );
      entity.addComponent(entityGrid);

      const failureSound = SOUNDS.get(Failure.name)!;
      failureSound.play();
    };

    const applicationTerm = this.getComponent<LambdaTerm>(
      ComponentNames.LambdaTerm,
    );
    const functionTerm = entity.getComponent<LambdaTerm>(
      ComponentNames.LambdaTerm,
    );
    const newCode = applicationTerm.code.replace("_INPUT", functionTerm.code);

    const { symbolTable, visitors } = this.getVisitors(game);
    const result = tryWrap(() =>
      interpret(newCode, symbolTable, true, visitors),
    );
    applicationTerm.last = result;
    if (result.error || !result.data) {
      console.error(result.error);
      fail();
      return;
    }

    const nextPosition = gridSystem.getNewGridPosition(
      grid.gridPosition,
      entityGrid.previousDirection,
    );

    let applicationResultingEntity: Entity | null = null; // this should be its own function
    const { data } = result;
    if ("abstraction" in data) {
      const code = emitNamed(data);
      applicationResultingEntity = new FunctionBox(grid.gridPosition, code);
    }
    if ("name" in data) {
      const { name } = data;
      const entityFactory = APPLICATION_RESULTS[name];
      if (entityFactory) {
        const entity = entityFactory(nextPosition);
        entity && game.addEntity(entity);
      }
    }

    game.removeEntity(entity.id);
    if (!applicationResultingEntity) {
      return;
    }

    applicationResultingEntity.getComponent<Grid>(
      ComponentNames.Grid,
    ).movingDirection = entityGrid.previousDirection;
    game.addEntity(applicationResultingEntity);
  }

  private getVisitors(game: Game): {
    visitors: Visitors;
    symbolTable: SymbolTable;
  } {
    const directionKeywords = {
      _LEFT: Direction.LEFT,
      _RIGHT: Direction.RIGHT,
      _DOWN: Direction.DOWN,
      _UP: Direction.UP,
    };
    const entityKeywords = {
      _KEY: (pos: Coord2D) => new Key(pos),
    };

    const visitors: Visitors = new Map();
    visitors.set("_SPAWN", (_term) => {
      const position = this.getComponent<Grid>(
        ComponentNames.Grid,
      ).gridPosition;
      return {
        abstraction: {
          param: "_DIRECTION",
          body: (direction) => {
            const destinationDirection =
              directionKeywords[
                (direction as DebrujinIndex)
                  .name! as keyof typeof directionKeywords
              ];
            const destination = game
              .getSystem<GridSystem>(SystemNames.Grid)
              .getNewGridPosition(position, destinationDirection);
            return {
              abstraction: {
                param: "_ENTITY",
                body: (entityType) => {
                  const entityFactory =
                    entityKeywords[
                      (entityType as DebrujinIndex)
                        .name! as keyof typeof entityKeywords
                    ];
                  const newEntity = entityFactory(destination);
                  game.addEntity(newEntity);
                  return {
                    abstraction: {
                      param: "_x",
                      body: (_t) => {
                        return {
                          application: {
                            left: {
                              index: 1,
                              name: "_SPAWN",
                            },
                            args: [direction, entityType],
                          },
                        };
                      },
                    },
                  };
                },
              },
            };
          },
        },
      };
    });

    return {
      visitors,
      symbolTable: SymbolTable.from(
        Array.from(visitors.keys())
          .concat(Object.keys(APPLICATION_RESULTS))
          .concat(Object.keys(directionKeywords))
          .concat(Object.keys(entityKeywords))
          .concat(["_x"]),
      ),
    };
  }

  private addParticles(game: Game, position: Coord2D) {
    const gridSystem = game.getSystem<GridSystem>(SystemNames.Grid);
    const { dimension } = gridSystem;
    SOUNDS.get(LambdaTransformSound.name)!.play();
    const particles = new Particles({
      center: gridSystem.gridToScreenPosition(position),
      spawnerDimensions: {
        width: dimension.width / 2,
        height: dimension.height / 2,
      },
      particleCount: 10,
      spawnerShape: "circle",
      particleShape: "circle",
      particleMeanSpeed: 0.25,
      particleSpeedVariance: 0.15,
      particleMeanLife: 150,
      particleMeanSize: 2,
      particleSizeVariance: 1,
      particleLifeVariance: 20,
      particleColors: [
        colors.lightAqua,
        colors.blue,
        colors.green,
        colors.lightGreen,
      ],
    });
    game.addEntity(particles);
  }
}
