import { Server, Socket } from 'socket.io';
import { RequestHandler } from 'express';
import { IcyNetUser } from '../common/types/user';
import { Persistence } from './object/persistence';
import { Logger } from './object/logger';
import { ServerWorld } from './world/server-world';
import { ServerWorldLoader } from './world/server-world-loader';
import { ServerWorldManifest } from './world/server-world-manifest';
import { Player } from './object/player';
import {
  ServerToClientEvents,
  ClientToServerEvents,
} from '../common/types/socket';
import { InterServerEvents, SocketData } from './types/socket';
import { CompositePacket } from '../common/types/packet';

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 _players: Player[] = [];
  private _db = new Persistence();
  private _log = new Logger();
  private _world!: ServerWorld;

  constructor(
    private io: Server<
      ClientToServerEvents,
      ServerToClientEvents,
      InterServerEvents,
      SocketData
    >,
    private session: RequestHandler,
  ) {}

  async initialize() {
    const manifest = await ServerWorldManifest.loadManifest();
    this._world = new ServerWorld(new ServerWorldLoader(), manifest);

    await this._db.initialize();
    await this._world.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);

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

      if (user) {
        const player = new Player(socket, this._db, user);
        this._players.push(player);

        this._log.writeEvent('PlayerJoin', `<${user.display_name}:${user.id}>`);

        socket.data.player = player;
        player.initialize().then(() => {
          // Send player list to new player
          socket.emit(
            'player.list',
            this._players
              .filter((player) => player.user)
              .map((player) => ({
                ...player.getPublicUserProfile(),
                ...(player.toSave() as CompositePacket),
              })),
          );
        });

        socket.on('set.chat', (raw) => {
          const message = raw.trim().substring(0, 280);
          this._log.writeChat(user, message);
          this.io.emit('player.chat', {
            sender: player.getPublicUserProfile(),
            message,
          });
        });
      } else {
        socket.emit('set.me', null);
      }

      socket.on('disconnect', () => {
        if (user) {
          const player = this._players.find(
            (player) => player.user.id === user.id,
          );

          if (!player) {
            return;
          }

          this.io.emit('player.leave', player.getPublicUserProfile());

          this._log.writeEvent(
            'PlayerLeave',
            `<${user.display_name}:${user.id}>`,
          );

          player.dispose();
          this._players.splice(this._players.indexOf(player), 1);
        }
      });
    });

    setInterval(() => {
      this._players.forEach((player) => player.update(1000 / 60));
      this.io.emit(
        'player.update',
        this._players.map((player) => player.toUpdatePacket()),
      );
    }, 1000 / 60);
  }
}
