import { Socket } from "socket.io";

import Room, { SerializedRoom } from "./Room";
import { getTimelineNames } from "./timeline";

export interface Config {
  availableTimelines: string[];
}

export default class User {
  socket: Socket;
  id: string;

  room: Room | null = null;
  readyToParticipate = false;

  constructor(socket: Socket) {
    this.socket = socket;
    this.id = socket.id;
  }

  serialize() {
    return {
      id: this.id,
      readyToParticipate: this.readyToParticipate,
    };
  }

  async setRoom(room: Room | null) {
    if (this.room === room) return;

    if (this.room !== null) {
      await this.socket.leave(this.room.id.toString());
      this.readyToParticipate = false;
    }

    this.room = room;

    if (this.room !== null) {
      await this.socket.join(this.room.id.toString());
    }

    this.sync();
  }

  getConfig() {
    return {
      availableTimelines: getTimelineNames(),
    };
  }

  sentConfig: Config | null = null;
  sentRoom: SerializedRoom | null = null;
  sentTimelineName: string | null = null;

  sync() {
    // Config
    const config = this.getConfig();
    if (!this.syncEquals(this.sentConfig, config)) {
      this.sentConfig = Object.assign({}, config);
      this.emit("config", {
        config: this.sentConfig,
      });
    }

    // Room
    if (!this.syncEquals(this.sentRoom, this.room?.serialize(this))) {
      this.sentRoom = this.room
        ? (JSON.parse(
            JSON.stringify(this.room.serialize(this))
          ) as SerializedRoom)
        : null;

      this.emit("room", {
        room: this.sentRoom,
      });
    }

    // Timeline
    if (!this.syncEquals(this.sentTimelineName, this.room?.timelineName)) {
      this.sentTimelineName = this.room?.timelineName || null;
      this.emit("timeline", {
        timeline:
          this.sentTimelineName == null ? null : this.room?.serializeTimeline(),
      });
    }
  }

  emit(eventName: string, obj: unknown) {
    this.socket.emit(eventName, obj);
  }

  syncEquals(obj1: unknown, obj2: unknown): boolean {
    if (typeof obj1 !== typeof obj2) {
      return false;
    }

    if (typeof obj1 !== "object") {
      // Both are not 'object'
      return Object.is(obj1, obj2);
    }

    if (obj1 === null && obj2 === null) {
      return true;
    }

    if (obj1 === null || obj2 === null) {
      return false;
    }

    if (typeof obj2 !== "object") {
      // This can not happen ;)
      throw new TypeError("Obj2 is not object while obj1 is.");
    }

    if (Object.keys(obj1).length !== Object.keys(obj2).length) {
      return false;
    }

    return Object.keys(obj1).every((key: string) => {
      if (!(key in obj1) || !(key in obj2)) {
        return false;
      }

      return this.syncEquals(
        obj1[key as keyof object],
        obj2[key as keyof object]
      );
    });
  }
}
