import { Server, Socket } from 'socket.io';
import { RequestHandler } from 'express';
import { IcyNetUser } from '../common/types/user';
import { CharacterPacket, PositionUpdatePacket } from '../common/types/packet';
import { Persistence } from './object/persistence';
import { Logger } from './object/logger';

const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
  const randomName = `player-${socket.id.substring(0, 8)}`;
  return {
    id: Math.floor(Math.random() * 1000 + 1000),
    username: randomName,
    display_name: randomName,
    uuid: socket.id,
    accessToken: 'player',
  };
};

export class Game {
  private _connections: Socket[] = [];
  private _changedPlayers: number[] = [];
  private _db = new Persistence();
  private _log = new Logger();

  constructor(private io: Server, private session: RequestHandler) {}

  private mapPlayer(user: IcyNetUser): any {
    return {
      id: user.id,
      username: user.username,
      display_name: user.display_name,
    };
  }

  async initialize() {
    await this._db.initialize();
    this._log.initialize();

    this.io.use((socket, next) =>
      this.session(socket.request as any, {} as any, next as any),
    );

    this.io.on('connection', (socket) => {
      const session = (socket.request as any).session;
      const user =
        process.env.SKIP_LOGIN === 'true'
          ? PLACEHOLDER_USER(socket)
          : (session?.passport?.user as IcyNetUser);
      const publicUserInfo = user ? this.mapPlayer(user) : null;

      if (
        user &&
        this._connections.find((entry) => entry.data.user?.id === user.id)
      ) {
        socket.emit('error.duplicate');
        return;
      }

      this._connections.push(socket);

      if (user) {
        socket.broadcast.emit('playerjoin', publicUserInfo);
        this._log.writeEvent('PlayerJoin', `<${user.display_name}:${user.id}>`);

        socket.data.user = user;
        socket.data.playerinfo = {
          velocity: [0, 0, 0],
          angular: [0, 0, 0, 'XYZ'],
          position: [0, 0, 0],
          rotation: [0, 0, 0, 'XYZ'],
          animState: 0,
          character: {},
        };

        socket.on('move', (packet) => {
          socket.data.playerinfo = {
            ...socket.data.playerinfo,
            ...packet,
          };
        });

        socket.on('character', (info: CharacterPacket) => {
          socket.data.playerinfo.character = {
            ...socket.data.playerinfo.character,
            ...info,
          };

          this.io.emit('playercharacter', {
            id: socket.data.user.id,
            ...socket.data.playerinfo.character,
          });
        });

        socket.on('chat-send', (raw) => {
          const message = raw.trim().substring(0, 280);
          this._log.writeChat(user, message);
          this.io.emit('chat', { sender: publicUserInfo, message });
        });

        this._db.upsertUser(user).then((user) => {
          this._db.getUserPony(user).then((pony) => {
            if (pony) {
              socket.data.playerinfo.character =
                (pony.character as CharacterPacket) || {};

              socket.data.playerinfo.position = pony.position;
              socket.data.playerinfo.rotation = pony.rotation;
            } else {
              this._db.upsertPony(user, socket.data.playerinfo);
            }

            socket.emit('me', {
              ...publicUserInfo,
              character: socket.data.playerinfo.character,
              position: pony?.position,
              rotation: pony?.rotation,
            });

            socket.emit(
              'players',
              this._connections
                .filter((player) => player.data.user)
                .map((conn) => ({
                  ...this.mapPlayer(conn.data.user),
                  ...conn.data.playerinfo,
                })),
            );

            if (pony) {
              this.io.emit('playercharacter', {
                id: socket.data.user.id,
                ...socket.data.playerinfo.character,
              });
            }
          });
        });
      } else {
        socket.emit('me', null);
      }

      socket.on('disconnect', () => {
        this._connections.splice(this._connections.indexOf(socket), 1);

        if (user) {
          this.io.emit('playerleave', publicUserInfo);
          this._log.writeEvent(
            'PlayerLeave',
            `<${user.display_name}:${user.id}>`,
          );
          this._db.upsertPony(user, socket.data.playerinfo);
        }
      });
    });

    setInterval(() => {
      const playerInfo: PositionUpdatePacket[] = [];
      this._connections
        .filter(
          (conn) => conn.data.user, // && this._changedPlayers.includes(conn.data.user.id),
        )
        .forEach((conn) =>
          playerInfo.push({
            position: conn.data.playerinfo.position,
            rotation: conn.data.playerinfo.rotation,
            animState: conn.data.playerinfo.animState,
            id: conn.data.user.id,
          }),
        );
      this.io.emit('playerupdate', playerInfo);
      this._changedPlayers.length = 0;
    }, 1000 / 60);
  }
}
