/*
 * Copyright 2017 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 * as logging from './logger';
import * as plugin from '../plugins/main';
import type {
  Ctx,
  StageArg,
  ActivePlayersArg,
  PlayerID,
  State,
  TurnConfig,
  FnContext,
} from '../types';
import { supportDeprecatedMoveLimit } from './backwards-compatibility';

export function SetActivePlayers(ctx: Ctx, arg: ActivePlayersArg): Ctx {
  let activePlayers: typeof ctx.activePlayers = {};
  let _prevActivePlayers: typeof ctx._prevActivePlayers = [];
  let _nextActivePlayers: ActivePlayersArg | null = null;
  let _activePlayersMinMoves = {};
  let _activePlayersMaxMoves = {};

  if (Array.isArray(arg)) {
    // support a simple array of player IDs as active players
    const value = {};
    arg.forEach((v) => (value[v] = Stage.NULL));
    activePlayers = value;
  } else {
    // process active players argument object

    // stages previously did not enforce minMoves, this behaviour is kept intentionally
    supportDeprecatedMoveLimit(arg);

    if (arg.next) {
      _nextActivePlayers = arg.next;
    }

    if (arg.revert) {
      _prevActivePlayers = [
        ...ctx._prevActivePlayers,
        {
          activePlayers: ctx.activePlayers,
          _activePlayersMinMoves: ctx._activePlayersMinMoves,
          _activePlayersMaxMoves: ctx._activePlayersMaxMoves,
          _activePlayersNumMoves: ctx._activePlayersNumMoves,
        },
      ];
    }

    if (arg.currentPlayer !== undefined) {
      ApplyActivePlayerArgument(
        activePlayers,
        _activePlayersMinMoves,
        _activePlayersMaxMoves,
        ctx.currentPlayer,
        arg.currentPlayer
      );
    }

    if (arg.others !== undefined) {
      for (let i = 0; i < ctx.playOrder.length; i++) {
        const id = ctx.playOrder[i];
        if (id !== ctx.currentPlayer) {
          ApplyActivePlayerArgument(
            activePlayers,
            _activePlayersMinMoves,
            _activePlayersMaxMoves,
            id,
            arg.others
          );
        }
      }
    }

    if (arg.all !== undefined) {
      for (let i = 0; i < ctx.playOrder.length; i++) {
        const id = ctx.playOrder[i];
        ApplyActivePlayerArgument(
          activePlayers,
          _activePlayersMinMoves,
          _activePlayersMaxMoves,
          id,
          arg.all
        );
      }
    }

    if (arg.value) {
      for (const id in arg.value) {
        ApplyActivePlayerArgument(
          activePlayers,
          _activePlayersMinMoves,
          _activePlayersMaxMoves,
          id,
          arg.value[id]
        );
      }
    }

    if (arg.minMoves) {
      for (const id in activePlayers) {
        if (_activePlayersMinMoves[id] === undefined) {
          _activePlayersMinMoves[id] = arg.minMoves;
        }
      }
    }

    if (arg.maxMoves) {
      for (const id in activePlayers) {
        if (_activePlayersMaxMoves[id] === undefined) {
          _activePlayersMaxMoves[id] = arg.maxMoves;
        }
      }
    }
  }

  if (Object.keys(activePlayers).length === 0) {
    activePlayers = null;
  }

  if (Object.keys(_activePlayersMinMoves).length === 0) {
    _activePlayersMinMoves = null;
  }

  if (Object.keys(_activePlayersMaxMoves).length === 0) {
    _activePlayersMaxMoves = null;
  }

  const _activePlayersNumMoves = {};
  for (const id in activePlayers) {
    _activePlayersNumMoves[id] = 0;
  }

  return {
    ...ctx,
    activePlayers,
    _activePlayersMinMoves,
    _activePlayersMaxMoves,
    _activePlayersNumMoves,
    _prevActivePlayers,
    _nextActivePlayers,
  };
}

/**
 * Update activePlayers, setting it to previous, next or null values
 * when it becomes empty.
 * @param ctx
 */
export function UpdateActivePlayersOnceEmpty(ctx: Ctx) {
  let {
    activePlayers,
    _activePlayersMinMoves,
    _activePlayersMaxMoves,
    _activePlayersNumMoves,
    _prevActivePlayers,
    _nextActivePlayers,
  } = ctx;

  if (activePlayers && Object.keys(activePlayers).length === 0) {
    if (_nextActivePlayers) {
      ctx = SetActivePlayers(ctx, _nextActivePlayers);
      ({
        activePlayers,
        _activePlayersMinMoves,
        _activePlayersMaxMoves,
        _activePlayersNumMoves,
        _prevActivePlayers,
      } = ctx);
    } else if (_prevActivePlayers.length > 0) {
      const lastIndex = _prevActivePlayers.length - 1;
      ({
        activePlayers,
        _activePlayersMinMoves,
        _activePlayersMaxMoves,
        _activePlayersNumMoves,
      } = _prevActivePlayers[lastIndex]);
      _prevActivePlayers = _prevActivePlayers.slice(0, lastIndex);
    } else {
      activePlayers = null;
      _activePlayersMinMoves = null;
      _activePlayersMaxMoves = null;
    }
  }

  return {
    ...ctx,
    activePlayers,
    _activePlayersMinMoves,
    _activePlayersMaxMoves,
    _activePlayersNumMoves,
    _prevActivePlayers,
  };
}

/**
 * Apply an active player argument to the given player ID
 * @param {Object} activePlayers
 * @param {Object} _activePlayersMinMoves
 * @param {Object} _activePlayersMaxMoves
 * @param {String} playerID The player to apply the parameter to
 * @param {(String|Object)} arg An active player argument
 */
function ApplyActivePlayerArgument(
  activePlayers: Ctx['activePlayers'],
  _activePlayersMinMoves: Ctx['_activePlayersMinMoves'],
  _activePlayersMaxMoves: Ctx['_activePlayersMaxMoves'],
  playerID: PlayerID,
  arg: StageArg
) {
  if (typeof arg !== 'object' || arg === Stage.NULL) {
    arg = { stage: arg as string | null };
  }

  if (arg.stage !== undefined) {
    // stages previously did not enforce minMoves, this behaviour is kept intentionally
    supportDeprecatedMoveLimit(arg);

    activePlayers[playerID] = arg.stage;
    if (arg.minMoves) _activePlayersMinMoves[playerID] = arg.minMoves;
    if (arg.maxMoves) _activePlayersMaxMoves[playerID] = arg.maxMoves;
  }
}

/**
 * Converts a playOrderPos index into its value in playOrder.
 * @param {Array} playOrder - An array of player ID's.
 * @param {number} playOrderPos - An index into the above.
 */
function getCurrentPlayer(
  playOrder: Ctx['playOrder'],
  playOrderPos: Ctx['playOrderPos']
) {
  // convert to string in case playOrder is set to number[]
  return playOrder[playOrderPos] + '';
}

/**
 * Called at the start of a turn to initialize turn order state.
 *
 * TODO: This is called inside StartTurn, which is called from
 * both UpdateTurn and StartPhase (so it's called at the beginning
 * of a new phase as well as between turns). We should probably
 * split it into two.
 */
export function InitTurnOrderState(state: State, turn: TurnConfig) {
  let { G, ctx } = state;
  const { numPlayers } = ctx;
  const pluginAPIs = plugin.GetAPIs(state);
  const context = { ...pluginAPIs, G, ctx };
  const order = turn.order;

  let playOrder = [...Array.from({ length: numPlayers })].map((_, i) => i + '');
  if (order.playOrder !== undefined) {
    playOrder = order.playOrder(context);
  }

  const playOrderPos = order.first(context);
  const posType = typeof playOrderPos;
  if (posType !== 'number') {
    logging.error(
      `invalid value returned by turn.order.first — expected number got ${posType} “${playOrderPos}”.`
    );
  }
  const currentPlayer = getCurrentPlayer(playOrder, playOrderPos);

  ctx = { ...ctx, currentPlayer, playOrderPos, playOrder };
  ctx = SetActivePlayers(ctx, turn.activePlayers || {});

  return ctx;
}

/**
 * Called at the end of each turn to update the turn order state.
 * @param {object} G - The game object G.
 * @param {object} ctx - The game object ctx.
 * @param {object} turn - A turn object for this phase.
 * @param {string} endTurnArg - An optional argument to endTurn that
                                may specify the next player.
 */
export function UpdateTurnOrderState(
  state: State,
  currentPlayer: PlayerID,
  turn: TurnConfig,
  endTurnArg?: true | { remove?: any; next?: string }
) {
  const order = turn.order;

  let { G, ctx } = state;
  let playOrderPos = ctx.playOrderPos;
  let endPhase = false;

  if (endTurnArg && endTurnArg !== true) {
    if (typeof endTurnArg !== 'object') {
      logging.error(`invalid argument to endTurn: ${endTurnArg}`);
    }

    Object.keys(endTurnArg).forEach((arg) => {
      switch (arg) {
        case 'remove':
          currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
          break;
        case 'next':
          playOrderPos = ctx.playOrder.indexOf(endTurnArg.next);
          currentPlayer = endTurnArg.next;
          break;
        default:
          logging.error(`invalid argument to endTurn: ${arg}`);
      }
    });
  } else {
    const pluginAPIs = plugin.GetAPIs(state);
    const context = { ...pluginAPIs, G, ctx };
    const t = order.next(context);
    const type = typeof t;
    if (t !== undefined && type !== 'number') {
      logging.error(
        `invalid value returned by turn.order.next — expected number or undefined got ${type} “${t}”.`
      );
    }

    if (t === undefined) {
      endPhase = true;
    } else {
      playOrderPos = t;
      currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
    }
  }

  ctx = {
    ...ctx,
    playOrderPos,
    currentPlayer,
  };

  return { endPhase, ctx };
}

/**
 * Set of different turn orders possible in a phase.
 * These are meant to be passed to the `turn` setting
 * in the flow objects.
 *
 * Each object defines the first player when the phase / game
 * begins, and also a function `next` to determine who the
 * next player is when the turn ends.
 *
 * The phase ends if next() returns undefined.
 */
export const TurnOrder = {
  /**
   * DEFAULT
   *
   * The default round-robin turn order.
   */
  DEFAULT: {
    first: ({ ctx }: FnContext) =>
      ctx.turn === 0
        ? ctx.playOrderPos
        : (ctx.playOrderPos + 1) % ctx.playOrder.length,
    next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
  },

  /**
   * RESET
   *
   * Similar to DEFAULT, but starts from 0 each time.
   */
  RESET: {
    first: () => 0,
    next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
  },

  /**
   * CONTINUE
   *
   * Similar to DEFAULT, but starts with the player who ended the last phase.
   */
  CONTINUE: {
    first: ({ ctx }: FnContext) => ctx.playOrderPos,
    next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
  },

  /**
   * ONCE
   *
   * Another round-robin turn order, but goes around just once.
   * The phase ends after all players have played.
   */
  ONCE: {
    first: () => 0,
    next: ({ ctx }: FnContext) => {
      if (ctx.playOrderPos < ctx.playOrder.length - 1) {
        return ctx.playOrderPos + 1;
      }
    },
  },

  /**
   * CUSTOM
   *
   * Identical to DEFAULT, but also sets playOrder at the
   * beginning of the phase.
   *
   * @param {Array} playOrder - The play order.
   */
  CUSTOM: (playOrder: string[]) => ({
    playOrder: () => playOrder,
    first: () => 0,
    next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
  }),

  /**
   * CUSTOM_FROM
   *
   * Identical to DEFAULT, but also sets playOrder at the
   * beginning of the phase to a value specified by a field
   * in G.
   *
   * @param {string} playOrderField - Field in G.
   */
  CUSTOM_FROM: (playOrderField: string) => ({
    playOrder: ({ G }: FnContext) => G[playOrderField],
    first: () => 0,
    next: ({ ctx }: FnContext) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
  }),
};

export const Stage = {
  NULL: null,
};

export const ActivePlayers = {
  /**
   * ALL
   *
   * The turn stays with one player, but any player can play (in any order)
   * until the phase ends.
   */
  ALL: { all: Stage.NULL },

  /**
   * ALL_ONCE
   *
   * The turn stays with one player, but any player can play (once, and in any order).
   * This is typically used in a phase where you want to elicit a response
   * from every player in the game.
   */
  ALL_ONCE: { all: Stage.NULL, minMoves: 1, maxMoves: 1 },

  /**
   * OTHERS
   *
   * The turn stays with one player, and every *other* player can play (in any order)
   * until the phase ends.
   */
  OTHERS: { others: Stage.NULL },

  /**
   * OTHERS_ONCE
   *
   * The turn stays with one player, and every *other* player can play (once, and in any order).
   * This is typically used in a phase where you want to elicit a response
   * from every *other* player in the game.
   */
  OTHERS_ONCE: { others: Stage.NULL, minMoves: 1, maxMoves: 1 },
};
