/*
 * Copyright 2018 The boardgame.io Authors
 *
 * Use of this source code is governed by a MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */

import { InMemory } from '../../server/db/inmemory';
import { LocalStorage } from '../../server/db/localstorage';
import { Master } from '../../master/master';
import type { TransportAPI, TransportData } from '../../master/master';
import { Transport } from './transport';
import type { TransportOpts } from './transport';
import type {
  ChatMessage,
  CredentialedActionShape,
  Game,
  PlayerID,
  State,
} from '../../types';
import { getFilterPlayerView } from '../../master/filter-player-view';

/**
 * Returns null if it is not a bot's turn.
 * Otherwise, returns a playerID of a bot that may play now.
 */
export function GetBotPlayer(state: State, bots: Record<PlayerID, any>) {
  if (state.ctx.gameover !== undefined) {
    return null;
  }

  if (state.ctx.activePlayers) {
    for (const key of Object.keys(bots)) {
      if (key in state.ctx.activePlayers) {
        return key;
      }
    }
  } else if (state.ctx.currentPlayer in bots) {
    return state.ctx.currentPlayer;
  }

  return null;
}

interface LocalOpts {
  bots?: Record<PlayerID, any>;
  persist?: boolean;
  storageKey?: string;
}

type LocalMasterOpts = LocalOpts & {
  game: Game;
};

/**
 * Creates a local version of the master that the client
 * can interact with.
 */
export class LocalMaster extends Master {
  connect: (
    playerID: PlayerID,
    callback: (data: TransportData) => void
  ) => void;

  constructor({ game, bots, storageKey, persist }: LocalMasterOpts) {
    const clientCallbacks: Record<PlayerID, (data: TransportData) => void> = {};
    const initializedBots = {};

    if (game && game.ai && bots) {
      for (const playerID in bots) {
        const bot = bots[playerID];
        initializedBots[playerID] = new bot({
          game,
          enumerate: game.ai.enumerate,
          seed: game.seed,
        });
      }
    }

    const send: TransportAPI['send'] = ({ playerID, ...data }) => {
      const callback = clientCallbacks[playerID];
      if (callback !== undefined) {
        callback(filterPlayerView(playerID, data));
      }
    };

    const filterPlayerView = getFilterPlayerView(game);
    const transportAPI: TransportAPI = {
      send,
      sendAll: (payload) => {
        for (const playerID in clientCallbacks) {
          send({ playerID, ...payload });
        }
      },
    };
    const storage = persist ? new LocalStorage(storageKey) : new InMemory();
    super(game, storage, transportAPI);

    this.connect = (playerID, callback) => {
      clientCallbacks[playerID] = callback;
    };

    this.subscribe(({ state, matchID }) => {
      if (!bots) {
        return;
      }
      const botPlayer = GetBotPlayer(state, initializedBots);
      if (botPlayer !== null) {
        setTimeout(async () => {
          const botAction = await initializedBots[botPlayer].play(
            state,
            botPlayer
          );
          await this.onUpdate(
            botAction.action,
            state._stateID,
            matchID,
            botAction.action.payload.playerID
          );
        }, 100);
      }
    });
  }
}

type LocalTransportOpts = TransportOpts & {
  master?: LocalMaster;
};

/**
 * Local
 *
 * Transport interface that embeds a GameMaster within it
 * that you can connect multiple clients to.
 */
export class LocalTransport extends Transport {
  master: LocalMaster;

  /**
   * Creates a new Mutiplayer instance.
   * @param {string} matchID - The game ID to connect to.
   * @param {string} playerID - The player ID associated with this client.
   * @param {string} gameName - The game type (the `name` field in `Game`).
   * @param {string} numPlayers - The number of players.
   */
  constructor({ master, ...opts }: LocalTransportOpts) {
    super(opts);
    this.master = master;
  }

  sendChatMessage(matchID: string, chatMessage: ChatMessage): void {
    const args: Parameters<Master['onChatMessage']> = [
      matchID,
      chatMessage,
      this.credentials,
    ];
    this.master.onChatMessage(...args);
  }

  sendAction(state: State, action: CredentialedActionShape.Any): void {
    this.master.onUpdate(action, state._stateID, this.matchID, this.playerID);
  }

  requestSync(): void {
    this.master.onSync(
      this.matchID,
      this.playerID,
      this.credentials,
      this.numPlayers
    );
  }

  connect(): void {
    this.setConnectionStatus(true);
    this.master.connect(this.playerID, (data) => this.notifyClient(data));
    this.requestSync();
  }

  disconnect(): void {
    this.setConnectionStatus(false);
  }

  updateMatchID(id: string): void {
    this.matchID = id;
    this.connect();
  }

  updatePlayerID(id: PlayerID): void {
    this.playerID = id;
    this.connect();
  }

  updateCredentials(credentials?: string): void {
    this.credentials = credentials;
    this.connect();
  }
}

/**
 * Global map storing local master instances.
 */
const localMasters: Map<Game, { master: LocalMaster } & LocalOpts> = new Map();

/**
 * Create a local transport.
 */
export function Local({ bots, persist, storageKey }: LocalOpts = {}) {
  return (transportOpts: TransportOpts) => {
    const { gameKey, game } = transportOpts;
    let master: LocalMaster;

    const instance = localMasters.get(gameKey);
    if (
      instance &&
      instance.bots === bots &&
      instance.storageKey === storageKey &&
      instance.persist === persist
    ) {
      master = instance.master;
    }

    if (!master) {
      master = new LocalMaster({ game, bots, persist, storageKey });
      localMasters.set(gameKey, { master, bots, persist, storageKey });
    }

    return new LocalTransport({ master, ...transportOpts });
  };
}
