import { Socket } from 'socket.io-client';
import { Color, DoubleSide, MeshBasicMaterial, Vector3 } from 'three';
import { clamp, isMobileOrTablet } from '../common/helper';
import { CharacterPacket, CompositePacket } from '../common/types/packet';
import { IcyNetUser } from '../common/types/user';
import { ThirdPersonCamera } from './object/camera';
import { Chat } from './object/chat';
import { Joystick } from './object/joystick';
import { PonyModelLoader } from './object/resource/pony-loader';
import { PonyEyes } from './object/model/eyes';
import { CubeMap } from './object/resource/cubemap';
import { VideoPlayer } from './object/other/video-player';
import { Player } from './object/player';
import { PlayerEntity } from './object/player-entity';
import { Renderer } from './renderer';
import { Grass } from './object/model/grass';
import { BaseTexture } from './object/resource/texture';
import { ClientWorld } from './object/world/client-world';
import { ClientWorldManifest } from './object/world/client-world-manifest';
import { ClientWorldLoader } from './object/world/client-world-loader';
import { LoadingManagerWrapper } from './object/resource/loading-manager';

export class Game {
  public players: (Player | PlayerEntity)[] = [];
  public player!: Player;
  public me!: IcyNetUser;
  public thirdPersonCamera!: ThirdPersonCamera;
  public joystick!: Joystick;
  public chat!: Chat;
  private _loading = LoadingManagerWrapper.getInstance();
  private character: CharacterPacket = {};
  private party: string[] = [];
  private _locked = false;

  public world!: ClientWorld;
  public renderer = new Renderer();

  private videoTest = new VideoPlayer(24, 12);

  constructor(public socket: Socket) {}

  async initialize(): Promise<void> {
    this._loading.initialize();

    const worldManifest = await ClientWorldManifest.loadManifest();
    this.world = new ClientWorld(new ClientWorldLoader(), worldManifest);

    const cube = await CubeMap.load('/assets/skybox/default');
    const grasstex = await BaseTexture.load(
      '/assets/terrain/decoration/grass01.png',
    );
    const flowertex = await BaseTexture.load(
      '/assets/terrain/decoration/flowers02.png',
    );
    const flowertex2 = await BaseTexture.load(
      '/assets/terrain/decoration/flowers01.png',
    );

    await PonyModelLoader.getInstance().initialize();
    await PonyEyes.getInstance().initialize();
    await this.world.initialize();

    this.renderer.initialize();
    this.bindSocket();
    this.chat = new Chat();
    this.chat.initialize();
    this.socket.connect();

    // experimental
    this.videoTest.initialize();
    this.videoTest.mesh.position.set(0, 22, 20);
    this.videoTest.mesh.rotateY(Math.PI / 2);
    this.renderer.scene.add(this.videoTest.mesh);
    this.party = (localStorage.getItem('party')?.split('|') || []).filter(
      (item) => item,
    );
    // end of

    this.chat.registerSendFunction((message) => {
      this.socket.emit('chat-send', message);
    });

    this.renderer.registerUpdateFunction((dt: number) => {
      this.update(dt);
    });

    this.renderer.scene.add(this.world.world);
    this.renderer.scene.background = cube.texture;

    // test
    const grassfield = Grass.getInstance().createGrassPatch(
      new Vector3(10, 0, 10),
      8,
      0.5,
      8,
      this.world.getHeight.bind(this.world),
    );

    const flowerfield = Grass.getInstance().createGrassPatch(
      new Vector3(10, 0, 10),
      8,
      4,
      3,
      this.world.getHeight.bind(this.world),
    );

    const flowerfield2 = Grass.getInstance().createGrassPatch(
      new Vector3(8, 0, 8),
      4,
      4,
      3,
      this.world.getHeight.bind(this.world),
    );

    const grass = Grass.getInstance().createInstance(
      grassfield,
      new MeshBasicMaterial({
        side: DoubleSide,
        map: grasstex.texture,
        alphaTest: 0.7,
      }),
    );

    const flowers = Grass.getInstance().createInstance(
      flowerfield,
      new MeshBasicMaterial({
        side: DoubleSide,
        map: flowertex.texture,
        alphaTest: 0.7,
      }),
    );

    const flowers2 = Grass.getInstance().createInstance(
      flowerfield2,
      new MeshBasicMaterial({
        side: DoubleSide,
        map: flowertex2.texture,
        alphaTest: 0.7,
      }),
    );

    this.renderer.scene.add(grass);
    this.renderer.scene.add(flowers);
    this.renderer.scene.add(flowers2);
    this._loading.isConnecting();

    window.addEventListener('keyup', (ev) => {
      if (ev.key === 'Shift') {
        this.toggleCamLock();
      }
    });
  }

  public toggleCamLock(): boolean {
    this._locked = !this._locked;
    this.thirdPersonCamera?.setLock(this._locked);
    this.player?.setCameraLock(this._locked);
    return this._locked;
  }

  public dispose() {
    this.players.forEach((player) => {
      player.dispose();
      this.renderer.scene.remove(player.container);
    });

    this.thirdPersonCamera?.dispose();
    this.joystick?.dispose();

    this.players.length = 0;
  }

  public update(dt: number) {
    this.players.forEach((player) => player.update(dt));
    this.player?.createPacket(this.socket);
    this.thirdPersonCamera?.update(dt);
    this.joystick?.update(dt);

    this.player && this.world?.update(this.player.container.position);
  }

  bindSocket() {
    this.socket.on('connect', () => {
      this.dispose();
      this._loading.connected();
      console.log('connected');
    });

    this.socket.on('error.duplicate', () => {
      this._loading.showError(
        'Error: You are already connected on another device!',
      );
    });

    this.socket.on('me', (user) => {
      console.log(user);

      if (!user) {
        this._loading.showError('Error: You need to log in!');
        window.location.href = '/login';
        return;
      }

      this.me = user;

      this.chat.addMessage(
        `Welcome to Icy3D World Experiment, ${user.display_name}!`,
        null,
        {
          color: '#fbff4e',
        },
      );

      const player = Player.fromUser(user, this.renderer.scene);
      user.character && player.setCharacter(user.character);
      user.position && player.container.position.fromArray(user.position);
      user.rotation && player.container.rotation.fromArray(user.rotation);
      player.setHeightSource(this.world);

      this.players.push(player);
      this.player = player;
      this.thirdPersonCamera = new ThirdPersonCamera(
        this.renderer.camera,
        this.player.container,
        this.renderer.canvas,
      );
      this.thirdPersonCamera.initialize();
      this.thirdPersonCamera.registerAltMoveFunction((x, y) => {
        this.player.angularVelocity.set(
          0,
          clamp(x * 0.5, -Math.PI, Math.PI),
          0,
        );
      });

      this.joystick = new Joystick(player);
      this.joystick.initialize();
      this.joystick.addButton(-60, -20, 'LOCK', () => this.toggleCamLock());
      this.joystick.addButton(135, -20, 'JUMP', () => this.player.jump());

      if (isMobileOrTablet()) {
        this.joystick.show();
      }

      this.socket.emit('character', this.character);
    });

    this.socket.on('playerjoin', (user) => {
      if (user.id === this.me.id) {
        return;
      }

      const newplayer = PlayerEntity.fromUser(user, this.renderer.scene);
      newplayer.setHeightSource(this.world);
      this.chat.addMessage(`${user.display_name} has joined the game.`, null, {
        color: '#fbff4e',
      });
      this.players.push(newplayer);
    });

    this.socket.on('playerleave', (user) => {
      const findPlayer = this.players.find((item) => item.user.id === user.id);
      if (findPlayer) {
        this.chat.addMessage(`${user.display_name} has left the game.`, null, {
          color: '#fbff4e',
        });

        this.renderer.scene.remove(findPlayer.container);
        findPlayer.dispose();
        this.players.splice(this.players.indexOf(findPlayer), 1);
      }
    });

    this.socket.on('players', (list: CompositePacket[]) => {
      list.forEach((player) => {
        if (player.id === this.me.id) {
          return;
        }

        const newplayer = PlayerEntity.fromUser(player, this.renderer.scene);
        newplayer.setHeightSource(this.world);
        newplayer.addUncommittedChanges(player);
        this.players.push(newplayer);
      });

      this.chat.addMessage(
        `List of players: ${list.map((user) => user.display_name).join(', ')}`,
        null,
        {
          color: '#fbff4e',
        },
      );
    });

    this.socket.on('playerupdate', (data) => {
      data.forEach((item) => {
        const player = this.players.find(
          (player) => player.user.id === item.id,
        );
        if (
          player &&
          player instanceof PlayerEntity &&
          player.user.id !== this.me.id
        ) {
          player.addUncommittedChanges(item);
        }
      });
    });

    this.socket.on('playercharacter', (data) => {
      const player = this.players.find((player) => player.user.id === data.id);
      if (
        player &&
        player instanceof PlayerEntity &&
        player.user.id !== this.me.id
      ) {
        player.setCharacter(data);
      }
    });

    this.socket.on('chat', (event) => {
      const player = this.players.find(
        (item) => item.user.id === event.sender.id,
      );

      if (player && player.user.id !== this.me.id) {
        (player as PlayerEntity).addChat(event.message);
      }

      this.chat.addMessage(event.message, event.sender.display_name);

      // experimental stuff
      this.experimentalPlayerCmd(event.message, event.sender);
    });

    this.socket.on('disconnect', () => {
      this.chat.addMessage(
        `Disconnected from the server, reconnecting..`,
        null,
        {
          color: '#ff0000',
        },
      );
    });
  }

  private experimentalPlayerCmd(message: string, sender: IcyNetUser) {
    if (this.me.id === sender.id) {
      if (message.startsWith('!color')) {
        const [cmd, color] = message.split(' ');
        try {
          const colorr = new Color(color);
          if (!colorr) {
            throw 'invalid';
          }
        } catch (e: any) {
          this.chat.addMessage('Invalid color.');
          return;
        }

        this.character = { ...this.character, color };
        this.player.setColor(color);
        this.socket.emit('character', this.character);
      }

      if (message.startsWith('!eyecolor')) {
        const [cmd, eyeColor] = message.split(' ');
        try {
          const colorr = new Color(eyeColor);
          if (!colorr) {
            throw 'invalid';
          }
        } catch (e: any) {
          this.chat.addMessage('Invalid color.');
          return;
        }

        this.character = { ...this.character, eyeColor };
        this.player.setEyeColor(eyeColor);
        this.socket.emit('character', this.character);
        localStorage.setItem('character', JSON.stringify(this.character));
      }

      if (message.startsWith('!party')) {
        const array = message.split(' ');
        const name = array.slice(2).join(' ');

        if (array[1] === 'join') {
          this.party.push(name);
          this.chat.addMessage(`Joined party of user "${name}".`);
        }

        if (array[1] === 'leave') {
          this.party.splice(this.party.indexOf(name), 1);
          this.chat.addMessage(`Left party of user "${name}".`);
        }

        if (array[1] === 'clear') {
          this.party.length = 0;
          this.chat.addMessage('Cleared party list.');
        }

        if (array[1] === 'list') {
          this.chat.addMessage(
            `You have joined the watch party of: ${this.party.join(', ')}`,
          );
        }

        localStorage.setItem('party', this.party.join('|'));
      }
    }

    if (
      !(
        sender.display_name === this.me.display_name ||
        this.party.includes(sender.display_name)
      )
    ) {
      return;
    }

    if (message.startsWith('!play')) {
      const [cmd, src] = message.split(' ');
      if (src) {
        this.videoTest.setSource(src, true);
      } else {
        this.videoTest.play();
      }
      return;
    }

    if (message.startsWith('!stop') || message.startsWith('!pause')) {
      this.videoTest.stop();
      return;
    }

    if (message.startsWith('!volume')) {
      const [cmd, vol] = message.split(' ');
      if (!vol) {
        this.chat.addMessage(
          `Current volume: ${Math.floor(this.videoTest.video.volume * 100)}`,
        );
        return;
      }
      this.videoTest.setVolume(parseInt(vol.replace('%', ''), 10));
    }
  }
}
