import { IcyNetUser } from '../../common/types/user';
import { PonyEntity } from './model/pony';
import {
  FullStatePacket,
  PositionUpdatePacket,
} from '../../common/types/packet';
import { CanvasUtils } from './canvas-utils';
import { ChatBubble } from './chat-bubble';
import { Vector3, Scene, Euler } from 'three';
import { clamp } from '../../common/helper';

const chatBuilder = new CanvasUtils({
  rounded: true,
  roundedRadius: 8,
});

export class PlayerEntity extends PonyEntity {
  private _updateQueue: PositionUpdatePacket[] = [];
  private _targetFrame: any = null;
  private _lastFrame: any = null;
  private _chats: ChatBubble[] = [];
  public remote = true;

  private _p1 = new Vector3();
  private _p2 = new Vector3();
  private _q1 = new Vector3();
  private _q2 = new Vector3();

  private _pf = new Vector3();
  private _qf = new Vector3();

  constructor(public user: IcyNetUser) {
    super();
  }

  public static fromUser(user: IcyNetUser, scene: Scene): PlayerEntity {
    const entity = new PlayerEntity(user);
    entity.initialize();
    entity.addNameTag(user.display_name);
    scene.add(entity.container);
    return entity;
  }

  public setVelocity(velocity: Vector3) {
    this.velocity.copy(velocity);
  }

  public setAngularVelocity(velocity: Vector3) {
    this.angularVelocity.copy(velocity);
  }

  public setPosition(pos: Vector3) {
    this.container.position.copy(pos);
  }

  public addChat(message: string): void {
    const lines: string[] = [];
    let truncated = message;

    while (truncated.length > 80) {
      lines.push(truncated.substring(0, 80));
      truncated = truncated.substring(80);
    }

    if (truncated) {
      lines.push(truncated);
    }

    const newChat = new ChatBubble(chatBuilder, lines);

    this._chats.forEach((item) => {
      const scaled = item.tag.scale.y;
      item.tag.position.add(new Vector3(0, scaled, 0));
    });

    this._chats.unshift(newChat);
    newChat.tag.position.set(0, 1.8 + this.nameTag!.tag.scale.y + 0.15, 0.5);
    this.container.add(newChat.tag);

    if (this._chats.length > 3) {
      this._chats.splice(3, this._chats.length - 1).forEach((item) => {
        this.container.remove(item.tag);
        item.dispose();
      });
    }

    setTimeout(() => {
      const i = this._chats.indexOf(newChat);

      if (i !== -1) {
        this.container.remove(newChat.tag);
        this._chats.splice(i, 1);
        newChat.dispose();
      }
    }, 5000);
  }

  public setRotation(rot: Vector3 | Euler) {
    this.container.rotation.copy(
      rot instanceof Euler ? rot : new Euler(rot.x, rot.y, rot.z, 'XYZ'),
    );
  }

  public addUncommittedChanges(packet: FullStatePacket) {
    const appendix = { ...packet, time: 0.1 };
    this._updateQueue.push(appendix);
    if (!this._targetFrame) {
      this.setFromPacket(packet);
      this._targetFrame = appendix;
    }

    if (packet.character) {
      this.setCharacter(packet.character);
    }
  }

  public update(dt: number) {
    this.commitServerUpdate(dt);
    super.update(dt);
  }

  private setFromPacket(packet: FullStatePacket | PositionUpdatePacket) {
    if ((packet as FullStatePacket).velocity) {
      this.setVelocity(
        new Vector3().fromArray((packet as FullStatePacket).velocity!),
      );
    }

    if ((packet as FullStatePacket).angular) {
      this.setAngularVelocity(
        new Vector3().fromArray((packet as FullStatePacket).angular!),
      );
    }

    if (packet.position) {
      this.setPosition(new Vector3().fromArray(packet.position));
    }

    if (packet.rotation) {
      this.setRotation(new Euler().fromArray(packet.rotation));
    }

    if (packet.animState !== undefined) {
      this.setWalkAnimationState(packet.animState);
    }
  }

  private commitServerUpdate(dt: number) {
    if (this._updateQueue.length == 0) {
      return;
    }

    for (let i = 0; i < this._updateQueue.length; ++i) {
      this._updateQueue[i].time! -= dt;
    }

    while (this._updateQueue.length > 0 && this._updateQueue[0].time! <= 0.0) {
      this._lastFrame = {
        animState: this._targetFrame.animState,
        position: this.container.position.toArray(),
        rotation: this.container.rotation.toArray(),
      };
      this._targetFrame = this._updateQueue.shift();
      this._targetFrame.time = 0.0;
    }

    // Lerp the position and rotation
    if (this._targetFrame && this._lastFrame) {
      this._targetFrame.time += dt;
      this._p1.fromArray(this._lastFrame.position);
      this._p2.fromArray(this._targetFrame.position);
      this._q1.fromArray(this._lastFrame.rotation);
      this._q2.fromArray(this._targetFrame.rotation);
      this._pf.copy(this._p1);
      this._qf.copy(this._q1);

      const t = clamp(this._targetFrame.time / 0.1, 0.0, 1.0);
      this._pf.lerp(this._p2, t);
      this._qf.lerp(this._q2, t);

      this.setPosition(this._pf);
      this.setRotation(this._qf);
      this.setWalkAnimationState(this._lastFrame.animState);
    }
  }
}
