import { World } from "../world/World";
import { IInputReceiver } from "../interfaces/IInputReceiver";
import { IUpdatable } from "../interfaces/IUpdatable";
import { EventType } from "../enums/EventType";
import { MATEMATROLII_EVENT } from "../enums/WorldType";

export class InputManager implements IUpdatable {
  public updateOrder: number = 3;

  public world: World;
  public domElement: any;
  public pointerLock: any;
  public isLocked: boolean;
  public inputReceiver: IInputReceiver;
  public boundOnMouseDown: (evt: any) => void;
  public boundOnMouseMove: (evt: any) => void;
  public boundOnMouseUp: (evt: any) => void;
  public boundOnMouseWheelMove: (evt: any) => void;
  public boundOnPointerlockChange: (evt: any) => void;
  public boundOnPointerlockError: (evt: any) => void;
  public boundOnKeyDown: (evt: any) => void;
  public boundOnKeyUp: (evt: any) => void;
  public boundMatematroliiPointerLock: (evt: any) => void;

  constructor(world: World, domElement: HTMLElement) {
    this.world = world;
    this.pointerLock = world.params.Pointer_Lock;
    this.domElement = domElement || document.body;
    this.isLocked = false;

    // Bindings for later event use
    // Mouse
    this.boundOnMouseDown = (evt) => this.onMouseDown(evt);
    this.boundOnMouseMove = (evt) => this.onMouseMove(evt);
    this.boundOnMouseUp = (evt) => this.onMouseUp(evt);
    this.boundOnMouseWheelMove = (evt) => this.onMouseWheelMove(evt);

    // Pointer lock
    this.boundOnPointerlockChange = (evt) => this.onPointerlockChange(evt);
    this.boundOnPointerlockError = (evt) => this.onPointerlockError(evt);
    this.boundMatematroliiPointerLock = (evt) =>
      this.onMatematroliiPointerLock(evt);

    // Keys
    this.boundOnKeyDown = (evt) => this.onKeyDown(evt);
    this.boundOnKeyUp = (evt) => this.onKeyUp(evt);

    // Init event listeners
    // Mouse
    this.domElement.addEventListener("mousedown", this.boundOnMouseDown, false);
    document.addEventListener("wheel", this.boundOnMouseWheelMove, false);
    document.addEventListener(
      "pointerlockchange",
      this.boundOnPointerlockChange,
      false
    );
    document.addEventListener(
      "pointerlockerror",
      this.boundOnPointerlockError,
      false
    );
    document.addEventListener(
      MATEMATROLII_EVENT,
      this.boundMatematroliiPointerLock,
      false
    );

    // Keys
    document.addEventListener("keydown", this.boundOnKeyDown, false);
    document.addEventListener("keyup", this.boundOnKeyUp, false);

    world.registerUpdatable(this);
  }

  public destroy(): void {
    this.world.unregisterUpdatable(this);
    this.domElement.removeEventListener("mousedown", this.boundOnMouseDown, false);
    document.removeEventListener("wheel", this.boundOnMouseWheelMove, false);
    document.removeEventListener(
      "pointerlockchange",
      this.boundOnPointerlockChange,
      false
    );
    document.removeEventListener(
      "pointerlockerror",
      this.boundOnPointerlockError,
      false
    );
    document.removeEventListener(
      MATEMATROLII_EVENT,
      this.boundMatematroliiPointerLock,
      false
    );

    // Keys
    document.removeEventListener("keydown", this.boundOnKeyDown, false);
    document.removeEventListener("keyup", this.boundOnKeyUp, false);

    this.world = undefined;
    this.pointerLock = undefined;
    this.domElement = undefined;
    this.isLocked = undefined;
    this.boundOnMouseDown = undefined;
    this.boundOnMouseMove = undefined;
    this.boundOnMouseUp = undefined;
    this.boundOnMouseWheelMove = undefined;

    // Pointer lock
    this.boundOnPointerlockChange = undefined;
    this.boundOnPointerlockError = undefined;
    this.boundMatematroliiPointerLock = undefined;

    // Keys
    this.boundOnKeyDown = undefined;
    this.boundOnKeyUp = undefined;
  }

  public onMatematroliiPointerLock(event: CustomEvent): void {
    if (event.detail) {
      const type = event.detail?.type;
      if (type === EventType.START) {
        this.domElement.requestPointerLock();
        this.world.render(this.world);
      } else if (type === EventType.PAUSE) {
        document.exitPointerLock();
        this.world.stopRender();
      }
    }
  }

  public update(timestep: number, unscaledTimeStep: number): void {
    if (
      this.inputReceiver === undefined &&
      this.world !== undefined &&
      this.world.cameraOperator !== undefined
    ) {
      this.setInputReceiver(this.world.cameraOperator);
    }

    this.inputReceiver?.inputReceiverUpdate(unscaledTimeStep);
  }

  public setInputReceiver(receiver: IInputReceiver): void {
    this.inputReceiver = receiver;
    this.inputReceiver.inputReceiverInit();
  }

  public setPointerLock(enabled: boolean): void {
    this.pointerLock = enabled;
  }

  public onPointerlockChange(event: MouseEvent): void {
    if (document.pointerLockElement === this.domElement) {
      this.domElement.addEventListener(
        "mousemove",
        this.boundOnMouseMove,
        false
      );
      this.domElement.addEventListener("mouseup", this.boundOnMouseUp, false);
      this.isLocked = true;
    } else {
      this.domElement.removeEventListener(
        "mousemove",
        this.boundOnMouseMove,
        false
      );
      this.domElement.removeEventListener(
        "mouseup",
        this.boundOnMouseUp,
        false
      );
      this.isLocked = false;
    }
  }

  public onPointerlockError(event: MouseEvent): void {
    console.error("PointerLockControls: Unable to use Pointer Lock API");
  }

  public onMouseDown(event: MouseEvent): void {
    if (this.pointerLock) {
      this.domElement.requestPointerLock();
    } else {
      this.domElement.addEventListener(
        "mousemove",
        this.boundOnMouseMove,
        false
      );
      this.domElement.addEventListener("mouseup", this.boundOnMouseUp, false);
    }

    if (this.inputReceiver !== undefined && this.pointerLock) {
      this.inputReceiver.handleMouseButton(event, "mouse" + event.button, true);
    }
  }

  public onMouseMove(event: MouseEvent): void {
    if (this.inputReceiver !== undefined) {
      this.inputReceiver.handleMouseMove(
        event,
        event.movementX,
        event.movementY
      );
    }
  }

  public onMouseUp(event: MouseEvent): void {
    if (!this.pointerLock) {
      this.domElement.removeEventListener(
        "mousemove",
        this.boundOnMouseMove,
        false
      );
      this.domElement.removeEventListener(
        "mouseup",
        this.boundOnMouseUp,
        false
      );
    }

    if (this.inputReceiver !== undefined) {
      this.inputReceiver.handleMouseButton(
        event,
        "mouse" + event.button,
        false
      );
    }
  }

  public onKeyDown(event: KeyboardEvent): void {
    if (this.inputReceiver !== undefined) {
      this.inputReceiver.handleKeyboardEvent(event, event.code, true);
    }
  }

  public onKeyUp(event: KeyboardEvent): void {
    if (event.code === "Escape") {
      document.exitPointerLock();
    }
    if (this.inputReceiver !== undefined) {
      this.inputReceiver.handleKeyboardEvent(event, event.code, false);
    }
  }

  public onMouseWheelMove(event: WheelEvent): void {
    if (this.inputReceiver !== undefined) {
      this.inputReceiver.handleMouseWheel(event, event.deltaY);
    }
  }
}
