import io, { Socket } from "socket.io-client";

import { Config, Room, RoomOptions, Timeline } from "../types/types";
import { Sub, useSub } from "../util/sub";

class Connection {
  url = "/";

  socket: Socket;

  isConnected = new Sub<boolean>();

  config = new Sub<Config | null>();
  room = new Sub<Room | null>();
  timeline = new Sub<Timeline | null>();

  timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000];
  timeSyncs: { [requestId: number]: TimeSyncRequest } = {};
  timeSyncTimeoutIds: number[] = [];
  timeSyncTooOld = 120000;
  roomTime = new Sub<number>();

  constructor() {
    this.isConnected.set(false);

    this.socket = io(this.url, {
      autoConnect: false,
      transports: ["websocket"],
    });

    this.setupSocketListeners();

    this.connect();

    this.roomTime.set(0);
  }

  connect() {
    this.socket.connect();
  }

  setupSocketListeners() {
    this.socket.on("connect", async () => {
      this.isConnected.set(true);
      await this.onConnect();
    });

    this.socket.on("disconnect", () => {
      this.isConnected.set(false);
      this.onDisconnect();
    });

    this.socket.on("config", (data: any) => {
      this.config.set(data.config);
    });

    this.socket.on("time_sync", (data: any) => {
      this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime);
    });

    this.socket.on("room", (data: any) => {
      if (data.room) {
        this.setQueryLobbyId(data.room.id);
      }

      this.room.set(data.room);
    });

    this.socket.on("timeline", (data: any) => {
      if (data.timeline) {
        this.timeline.set(new Timeline(data.timeline));
      } else {
        this.timeline.set(null);
      }
    });
  }

  async onConnect() {
    this.startTimeSync();

    const lobbyId = this.getQueryLobbyId();
    if (lobbyId) {
      const exists = await this.requestJoin(lobbyId);
      if (!exists) {
        this.setQueryLobbyId();
        this.requestJoinRandom();
      }
    } else {
      this.requestJoinRandom();
    }
  }

  onDisconnect() {
    this.stopTimeSync();
  }

  private getQueryLobbyId(): number | null {
    const query = new URLSearchParams(window.location.search);
    const lobby = query.get("lobby");

    if (!lobby) {
      return null;
    }

    const lobbyId = Number.parseInt(query.get("lobby")!.toString());

    if (!Number.isSafeInteger(lobbyId) || lobbyId < 1) {
      return null;
    }

    return lobbyId;
  }

  private setQueryLobbyId(lobbyId?: number) {
    const newUrl = new URL(window.location.href);

    if (lobbyId) {
      newUrl.searchParams.set("lobby", String(lobbyId));
    } else {
      newUrl.searchParams.delete("lobby");
    }

    window.history.pushState({}, "", newUrl.toString());
  }

  setRoomOptions(roomOptions: RoomOptions) {
    this.socket.emit("room_options", roomOptions);
  }

  requestStart() {
    this.socket.emit("request_start");
  }

  submitTickerMessage(message: string) {
    return new Promise<void>((resolve, reject) => {
      this.socket.emit(
        "submit_ticker_message",
        message,
        (_: null, err?: string) => {
          if (err) {
            reject(err);
          } else {
            resolve();
          }
        }
      );
    });
  }

  async requestJoin(roomId: number): Promise<boolean> {
    return new Promise<boolean>((resolve, reject) => {
      this.socket.emit(
        "request_join",
        roomId,
        (err?: string, didJoinRoom?: boolean) => {
          if (err) {
            return reject(err);
          }

          return resolve(!!didJoinRoom);
        }
      );
    });
  }

  requestSetReady() {
    this.socket.emit("request_set_ready");
  }

  requestJoinRandom() {
    this.socket.emit("request_join_random");
  }

  startTimeSync() {
    for (let i = 0; i < this.timeSyncIntervals.length; i++) {
      const timeoutId = setTimeout(() => {
        // Only reschedule the last sync interval (i.e. every 30 seconds)
        const shouldReschedule = i === this.timeSyncIntervals.length - 1;
        this.sendTimeSync(shouldReschedule);
      }, this.timeSyncIntervals[i]);

      this.timeSyncTimeoutIds.push(timeoutId);
    }
  }

  stopTimeSync() {
    for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) {
      clearTimeout(this.timeSyncTimeoutIds[i]);
    }
  }

  sendTimeSync(alsoSchedule: boolean) {
    const sync = new TimeSyncRequest();
    this.socket.emit("time_sync", sync.requestId, Date.now());
    this.timeSyncs[sync.requestId] = sync;

    if (alsoSchedule) {
      setTimeout(() => {
        this.sendTimeSync(true);
      }, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]);
    }
  }

  timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) {
    const syncReq = this.timeSyncs[requestId];
    if (!syncReq) {
      return;
    }

    syncReq.response(clientDiff, serverTime);

    for (const i in this.timeSyncs) {
      if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) {
        delete this.timeSyncs[i];
        break;
      }
    }

    this.roomTime.set(calculateRoomTime());
  }

  serverTime(): number {
    return Date.now() + this.serverTimeOffset();
  }

  serverTimeOffset(): number {
    let num = 0;
    let sum = 0;
    for (const i in this.timeSyncs) {
      const sync = this.timeSyncs[i];
      if (!sync.ready) continue;
      sum += sync.offset;
      num += 1;
    }

    if (num === 0) {
      return 0;
    }

    return Math.round(sum / num);
  }
}

let _timeSyncId = 0;

class TimeSyncRequest {
  requestId: number;
  start: number;
  offset = 0;
  ready = false;

  constructor() {
    this.requestId = _timeSyncId++;
    this.start = Date.now();
  }

  response(clientDiff: number, serverTime: number) {
    this.ready = true;
    const now = Date.now();

    const lag = now - this.start;
    this.offset = serverTime - now + lag / 2;
    // console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
    // lag, 'diff:', serverTime - now, 'offset:', this.offset);
  }
}

const connection: Connection = new Connection();
export default connection;

export function useRoom(): Room | null {
  return useSub(connection.room);
}

export function useConfig(): Config | null {
  return useSub(connection.config);
}

export function useRoomRunningAndReadyChanged(): Room | null {
  return useSub(connection.room, (v) => [v, v?.running, v?.readyToParticipate]);
}

export function useTimeline(): Timeline | null {
  return useSub(connection.timeline);
}

export function useTimelineSongFileChanged(): Timeline | null {
  return useSub(connection.timeline, (v) => [v?.songFile]);
}

export function useRoomTime(): number {
  return useSub(connection.roomTime) || 0;
}

/**
 * Calculates the current room time, adjusted for any possible server time
 * offset and lag.
 */
export function calculateRoomTime(): number {
  const room = connection.room.get();

  if (!room || typeof room.startTime === "undefined") {
    return 0;
  }

  return (connection.serverTime() - room.startTime) * room.speedFactor;
}

export function useIsConnected(): boolean {
  return useSub(connection.isConnected) || false;
}
