import {
  Entity,
  EntityNames,
  FunctionBox,
  Key,
  Particles,
  makeLambdaTermHighlightComponent,
} from ".";
import {
  BoundingBox,
  Colliding,
  ComponentNames,
  Grid,
  LambdaTerm,
  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 } from "../utils";
import {
  DebrujinifiedLambdaTerm,
  SymbolTable,
  emitNamed,
  interpret,
} from "../../interpreter";

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

  private symbolTable: SymbolTable;

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

    this.symbolTable = new SymbolTable();
    this.symbolTable.add("key");

    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(makeLambdaTermHighlightComponent(this));
  }

  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);
    let result: DebrujinifiedLambdaTerm | null = null;
    try {
      result = interpret(newCode, this.symbolTable);
    } catch (e) {
      console.error(e);
      fail();
      return;
    }

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

    let applicationResultingEntity: Entity | null = null; // this should be its own function
    if ("abstraction" in result) {
      const code = emitNamed(result);

      applicationResultingEntity = new FunctionBox(grid.gridPosition, code);
    } else if ("name" in result) {
      const { name } = result;
      if (name === "key") {
        applicationResultingEntity = new Key(grid.gridPosition);
      }
    } else {
      fail();
      return;
    }

    game.removeEntity(entity.id);
    if (applicationResultingEntity) {
      const grid = applicationResultingEntity.getComponent<Grid>(
        ComponentNames.Grid
      );
      grid.movingDirection = entityGrid.previousDirection;
      applicationResultingEntity.addComponent(grid);

      game.addEntity(applicationResultingEntity);
    }

    this.playTransformSound();
    const particles = new Particles({
      center: gridSystem.gridToScreenPosition(nextPosition),
      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);
  }

  private playTransformSound() {
    const audio = SOUNDS.get(LambdaTransformSound.name)!;
    audio.play();
  }
}
