package games.strategy.triplea.delegate;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import games.strategy.engine.data.CompositeChange;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.Territory;
import games.strategy.engine.data.Unit;
import games.strategy.engine.data.UnitType;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.message.IRemote;
import games.strategy.engine.random.IRandomStats.DiceType;
import games.strategy.triplea.MapSupport;
import games.strategy.triplea.Properties;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.util.TuvUtils;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
import games.strategy.util.ThreadUtil;
import games.strategy.util.Tuple;

/**
 * This delegate sets up the game according to Risk rules, with a few allowed customizations.
 * Either divide all neutral territories between players randomly, or let them pick one by one.
 * After that, any remaining units get placed one by one.
 * (Note that m_player may not be used here, because this delegate is not run by any player [it is null])
 */
@MapSupport
public class RandomStartDelegate extends BaseTripleADelegate {
  private static final int UNITS_PER_PICK = 1;
  protected PlayerID m_currentPickingPlayer = null;

  @Override
  public void start() {
    super.start();
    setupBoard();
  }

  @Override
  public void end() {
    super.end();
    m_currentPickingPlayer = null;
  }

  @Override
  public boolean delegateCurrentlyRequiresUserInput() {
    return !(Match.noneMatch(getData().getMap().getTerritories(), getTerritoryPickableMatch())
        && Match.noneMatch(getData().getPlayerList().getPlayers(), getPlayerCanPickMatch()));
  }

  @Override
  public Serializable saveState() {
    final RandomStartExtendedDelegateState state = new RandomStartExtendedDelegateState();
    state.superState = super.saveState();
    state.m_currentPickingPlayer = this.m_currentPickingPlayer;
    return state;
  }

  @Override
  public void loadState(final Serializable state) {
    final RandomStartExtendedDelegateState s = (RandomStartExtendedDelegateState) state;
    super.loadState(s.superState);
    this.m_currentPickingPlayer = s.m_currentPickingPlayer;
  }

  protected void setupBoard() {
    final GameData data = getData();
    final boolean randomTerritories = Properties.getTerritoriesAreAssignedRandomly(data);
    final Match<Territory> pickableTerritoryMatch = getTerritoryPickableMatch();
    final Match<PlayerID> playerCanPickMatch = getPlayerCanPickMatch();
    final List<Territory> allPickableTerritories =
        Matches.getMatches(data.getMap().getTerritories(), pickableTerritoryMatch);
    final List<PlayerID> playersCanPick = new ArrayList<>();
    playersCanPick.addAll(Matches.getMatches(data.getPlayerList().getPlayers(), playerCanPickMatch));
    // we need a main event
    if (!playersCanPick.isEmpty()) {
      m_bridge.getHistoryWriter().startEvent("Assigning Territories");
    }
    // for random:
    final int[] hitRandom = (!randomTerritories ? new int[0]
        : m_bridge.getRandom(allPickableTerritories.size(), allPickableTerritories.size(), null, DiceType.ENGINE,
            "Picking random territories"));
    int i = 0;
    int pos = 0;
    // divvy up territories
    while (!allPickableTerritories.isEmpty() && !playersCanPick.isEmpty()) {
      if (m_currentPickingPlayer == null || !playersCanPick.contains(m_currentPickingPlayer)) {
        m_currentPickingPlayer = playersCanPick.get(0);
      }
      if (!ThreadUtil.sleep(250)) {
        return;
      }
      Territory picked;
      if (randomTerritories) {
        pos += hitRandom[i];
        i++;
        final IntegerMap<UnitType> costs = TuvUtils.getCostsForTuv(m_currentPickingPlayer, data);
        final List<Unit> units = new ArrayList<>(m_currentPickingPlayer.getUnits().getUnits());
        Collections.sort(units, new UnitCostComparator(costs));
        final Set<Unit> unitsToPlace = new HashSet<>();
        unitsToPlace.add(units.get(0));
        picked = allPickableTerritories.get(pos % allPickableTerritories.size());
        final CompositeChange change = new CompositeChange();
        change.add(ChangeFactory.changeOwner(picked, m_currentPickingPlayer));
        final Collection<Unit> factoryAndInfrastructure =
            Matches.getMatches(unitsToPlace, Matches.unitIsInfrastructure());
        if (!factoryAndInfrastructure.isEmpty()) {
          change.add(OriginalOwnerTracker.addOriginalOwnerChange(factoryAndInfrastructure, m_currentPickingPlayer));
        }
        change.add(ChangeFactory.removeUnits(m_currentPickingPlayer, unitsToPlace));
        change.add(ChangeFactory.addUnits(picked, unitsToPlace));
        m_bridge.getHistoryWriter().addChildToEvent(m_currentPickingPlayer.getName() + " receives territory "
            + picked.getName() + " with units " + MyFormatter.unitsToTextNoOwner(unitsToPlace), picked);
        m_bridge.addChange(change);
      } else {
        Tuple<Territory, Set<Unit>> pick;
        Set<Unit> unitsToPlace;
        while (true) {
          pick = getRemotePlayer(m_currentPickingPlayer).pickTerritoryAndUnits(
              new ArrayList<>(allPickableTerritories),
              new ArrayList<>(m_currentPickingPlayer.getUnits().getUnits()), UNITS_PER_PICK);
          picked = pick.getFirst();
          unitsToPlace = pick.getSecond();
          if (!allPickableTerritories.contains(picked)
              || !m_currentPickingPlayer.getUnits().getUnits().containsAll(unitsToPlace)
              || unitsToPlace.size() > UNITS_PER_PICK || (unitsToPlace.size() < UNITS_PER_PICK
                  && unitsToPlace.size() < m_currentPickingPlayer.getUnits().getUnits().size())) {
            getRemotePlayer(m_currentPickingPlayer).reportMessage("Chosen territory or units invalid!",
                "Chosen territory or units invalid!");
          } else {
            break;
          }
        }
        final CompositeChange change = new CompositeChange();
        change.add(ChangeFactory.changeOwner(picked, m_currentPickingPlayer));
        final Collection<Unit> factoryAndInfrastructure =
            Matches.getMatches(unitsToPlace, Matches.unitIsInfrastructure());
        if (!factoryAndInfrastructure.isEmpty()) {
          change.add(OriginalOwnerTracker.addOriginalOwnerChange(factoryAndInfrastructure, m_currentPickingPlayer));
        }
        change.add(ChangeFactory.removeUnits(m_currentPickingPlayer, unitsToPlace));
        change.add(ChangeFactory.addUnits(picked, unitsToPlace));
        m_bridge.getHistoryWriter().addChildToEvent(m_currentPickingPlayer.getName() + " picks territory "
            + picked.getName() + " and places in it " + MyFormatter.unitsToTextNoOwner(unitsToPlace), unitsToPlace);
        m_bridge.addChange(change);
      }
      allPickableTerritories.remove(picked);
      final PlayerID lastPlayer = m_currentPickingPlayer;
      m_currentPickingPlayer = getNextPlayer(playersCanPick, m_currentPickingPlayer);
      if (!playerCanPickMatch.match(lastPlayer)) {
        playersCanPick.remove(lastPlayer);
      }
      if (playersCanPick.isEmpty()) {
        m_currentPickingPlayer = null;
      }
    }
    // place any remaining units
    while (!playersCanPick.isEmpty()) {
      if (m_currentPickingPlayer == null || !playersCanPick.contains(m_currentPickingPlayer)) {
        m_currentPickingPlayer = playersCanPick.get(0);
      }
      final List<Territory> territoriesToPickFrom = data.getMap().getTerritoriesOwnedBy(m_currentPickingPlayer);
      Tuple<Territory, Set<Unit>> pick;
      Territory picked;
      Set<Unit> unitsToPlace;
      while (true) {
        pick = getRemotePlayer(m_currentPickingPlayer).pickTerritoryAndUnits(
            new ArrayList<>(territoriesToPickFrom),
            new ArrayList<>(m_currentPickingPlayer.getUnits().getUnits()), UNITS_PER_PICK);
        picked = pick.getFirst();
        unitsToPlace = pick.getSecond();
        if (!territoriesToPickFrom.contains(picked)
            || !m_currentPickingPlayer.getUnits().getUnits().containsAll(unitsToPlace)
            || unitsToPlace.size() > UNITS_PER_PICK || (unitsToPlace.size() < UNITS_PER_PICK
                && unitsToPlace.size() < m_currentPickingPlayer.getUnits().getUnits().size())) {
          getRemotePlayer(m_currentPickingPlayer).reportMessage("Chosen territory or units invalid!",
              "Chosen territory or units invalid!");
        } else {
          break;
        }
      }
      final CompositeChange change = new CompositeChange();
      final Collection<Unit> factoryAndInfrastructure =
          Matches.getMatches(unitsToPlace, Matches.unitIsInfrastructure());
      if (!factoryAndInfrastructure.isEmpty()) {
        change.add(OriginalOwnerTracker.addOriginalOwnerChange(factoryAndInfrastructure, m_currentPickingPlayer));
      }
      change.add(ChangeFactory.removeUnits(m_currentPickingPlayer, unitsToPlace));
      change.add(ChangeFactory.addUnits(picked, unitsToPlace));
      m_bridge.getHistoryWriter().addChildToEvent(m_currentPickingPlayer.getName() + " places "
          + MyFormatter.unitsToTextNoOwner(unitsToPlace) + " in territory " + picked.getName(), unitsToPlace);
      m_bridge.addChange(change);
      final PlayerID lastPlayer = m_currentPickingPlayer;
      m_currentPickingPlayer = getNextPlayer(playersCanPick, m_currentPickingPlayer);
      if (!playerCanPickMatch.match(lastPlayer)) {
        playersCanPick.remove(lastPlayer);
      }
      if (playersCanPick.isEmpty()) {
        m_currentPickingPlayer = null;
      }
    }
  }

  protected PlayerID getNextPlayer(final List<PlayerID> playersCanPick, final PlayerID currentPlayer) {
    int index = playersCanPick.indexOf(currentPlayer);
    if (index == -1) {
      return null;
    }
    index++;
    if (index >= playersCanPick.size()) {
      index = 0;
    }
    return playersCanPick.get(index);
  }

  public Match<Territory> getTerritoryPickableMatch() {
    return Match.allOf(Matches.territoryIsLand(), Matches.territoryIsNotImpassable(),
        Matches.isTerritoryOwnedBy(PlayerID.NULL_PLAYERID), Matches.territoryIsEmpty());
  }

  private static Match<PlayerID> getPlayerCanPickMatch() {
    return Match.of(player -> {
      if (player == null || player.equals(PlayerID.NULL_PLAYERID)) {
        return false;
      }
      if (player.getUnits().isEmpty()) {
        return false;
      }
      return !player.getIsDisabled();
    });
  }

  @Override
  public Class<? extends IRemote> getRemoteType() {
    return null;
  }


  static class UnitCostComparator implements Comparator<Unit> {
    private final IntegerMap<UnitType> m_costs;

    public UnitCostComparator(final IntegerMap<UnitType> costs) {
      m_costs = costs;
    }

    public UnitCostComparator(final PlayerID player, final GameData data) {
      m_costs = TuvUtils.getCostsForTuv(player, data);
    }

    @Override
    public int compare(final Unit u1, final Unit u2) {
      return m_costs.getInt(u1.getType()) - m_costs.getInt(u2.getType());
    }
  }
}
