package net.anxuiz.tourney;

import java.time.Instant;
import java.util.Collection;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import net.anxuiz.tourney.event.EntrantRegisterEvent;
import net.anxuiz.tourney.event.EntrantUnregisterEvent;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.entity.Player;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.docs.Entrant;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.virtual.MatchDoc;
import tc.oc.api.tourney.TeamUtils;
import tc.oc.commons.core.IterableUtils;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.util.TimeUtils;
import tc.oc.pgm.match.Party;
import tc.oc.pgm.match.inject.MatchScoped;
import tc.oc.pgm.teams.Team;

@MatchScoped
public class TeamManager {

    private final Logger logger;
    private final Tourney tourney;
    private final EventBus eventBus;
    private final BukkitUserStore userStore;
    private final Set<Team> teams;
    private final Random random = new Random();
    private final HashBiMap<Team, Entrant> teamMap = HashBiMap.create();

    @Inject TeamManager(Loggers loggers, Tourney tourney, EventBus eventBus, BukkitUserStore userStore, Set<Team> teams) {
        this.logger = loggers.get(getClass());
        this.tourney = tourney;
        this.eventBus = eventBus;
        this.userStore = userStore;
        this.teams = teams;
    }

    /**
     * Maps the specified {@link Team} to the specified {@link Entrant}.
     *
     * @param team          The {@link Team} to map.
     * @param entrant The {@link Entrant} to map the {@link Team} to.
     * @return Whether or not the mapping was successful.
     */
    public boolean mapTeam(Team team, Entrant entrant) {
        logger.info("Mapping entrant '" + entrant.team().name() +
                    "' to map team '" + team.getInfo().getDefaultName() + "'");

        if(tourney.getState() == TourneyState.DISABLED) {
            tourney.setState(TourneyState.ENABLED_WAITING_FOR_TEAMS);
        }

        eventBus.callEvent(new EntrantRegisterEvent(team, entrant), event -> {
            teamMap.put(team, entrant);
            team.setLeagueTeamId(entrant.team()._id());
            team.setName(entrant.team().name());
        });

        if(allTeamsMapped()) {
            tourney.setState(TourneyState.ENABLED_WAITING_FOR_READY);
        }

        return true;
    }

    /**
     * Attempts to retrieve the appropriate {@link Team} for the specified {@link Entrant}.
     *
     * @param entrant The entrant to retrieve a team for.
     * @return The appropriate {@link Team}, or <code>null</code> if none was found.
     */
    public @Nullable Team entrantToTeam(final Entrant entrant) {
        return this.teamMap.inverse().get(entrant);
    }

    /**
     * Retrieves the appropriate {@link Entrant} for the specified {@link Team}.
     *
     * @param team The team to retrieve an entrant for.
     * @return The appropriate {@link Entrant}, or <code>null</code> if none was found.
     */
    public @Nullable Entrant teamToEntrant(final Party team) {
        return team instanceof Team ? this.teamMap.get(team) : null;
    }

    /**
     * Un-maps the specified {@link Team}.
     *
     * @param team The team to un-map.
     * @return Whether or not the team was unregistered.
     */
    public boolean unmap(final Team team) {
        Entrant entrant = teamMap.get(Preconditions.checkNotNull(team, "Team"));
        if (entrant == null) return false;

        if(allTeamsMapped()) {
            tourney.setState(TourneyState.ENABLED_WAITING_FOR_TEAMS);
        }

        eventBus.callEvent(new EntrantUnregisterEvent(team, entrant), event -> {
            teamMap.remove(team);
            team.setName(null);
        });

        if(teamMap.isEmpty()) {
            tourney.setState(TourneyState.DISABLED);
        }

        return true;
    }

    /**
     * Un-maps the specified {@link Entrant}.
     *
     * @param entrant The entrant to un-map.
     * @return Whether or not the team was unregistered.
     */
    public boolean unmap(final Entrant entrant) {
        return this.unmap(this.entrantToTeam(entrant));
    }

    public BiMap<Team, Entrant> mappedTeams() {
        return Maps.unmodifiableBiMap(teamMap);
    }

    /**
     * Gets all mapped entrants.
     *
     * @return All mapped entrants.
     */
    public Set<Entrant> getEntrants() {
        return this.teamMap.values();
    }

    public @Nullable Entrant getEntrant(String teamName) {
        teamName = TeamUtils.normalizeName(teamName);
        for(Entrant entrant : this.teamMap.values()) {
            if(teamName.equals(entrant.team().name_normalized())) return entrant;
        }
        return null;
    }

    public @Nullable Entrant getEntrant(PlayerId playerId) {
        for(Entrant entrant : this.teamMap.values()) {
            if(entrant.members().contains(playerId)) return entrant;
        }
        return null;
    }

    /**
     * @return The team this player is supposed to be on
     */
    public @Nullable Team getTeam(Player player) {
        return getTeam(userStore.getUser(player));
    }

    public @Nullable Team getTeam(PlayerId playerId) {
        for(Map.Entry<Team, Entrant> entry : this.teamMap.entrySet()) {
            if(entry.getValue().members().contains(playerId)) return entry.getKey();
        }
        return null;
    }

    /**
     * Gets all mapped teams.
     *
     * @return All mapped teams.
     */
    public Set<Team> getMappedTeams() {
        return this.teamMap.keySet();
    }

    /**
     * Empties the map entirely. Useful for cycling between matches.
     *
     * @return The number of entries removed.
     */
    public int clearMap() {
        int size = this.teamMap.size();
        this.teamMap.clear();
        return size;
    }

    /**
     * Assigns entrants to teams. Used on map cycle.
     */
    public void assignTeams(Collection<Entrant> entrants) {
        for (Entrant entrant : entrants) {
            this.assignEntrant(entrant, null);
        }
    }

    /**
     * Assigns a team to the specified entrant.
     *
     * @return The assigned team, or <code>null</code> if the assignment failed for one reason or another.
     */
    public @Nullable Team assignEntrant(Entrant entrant, @Nullable Team matchTeam) {
        return this.assignEntrant(entrant, matchTeam, false);
    }

    /**
     * Assigns a team (or returns the team that would be assigned, depending on the <code>soft</code> parameter) to the
     * specified entrant.
     *
     * @param soft  Whether or not to carry out the assignment.
     * @return The assigned (or would-be assigned) team, or <code>null</code> if the assignment failed for one reason or
     * another.
     */
    public @Nullable Team assignEntrant(Entrant entrant, @Nullable Team matchTeam, boolean soft) {
        if(matchTeam == null) {
            // If matchTeam is not specified, choose one of the available unmapped teams
            final Map<ChatColor, Team> available = Maps.uniqueIndex(Sets.filter(teams,
                                                                                team -> !teamMap.keySet().contains(team)),
                                                                    Team::getColor);
            if(available.isEmpty()) return null;

            // Try to find the most recently used color by this team in this tournament
            final String teamId = entrant.team()._id();
            ChatColor bestColor = null;
            Instant bestColorTime = TimeUtils.INF_PAST;
            for(MatchDoc match : entrant.matches()) {
                if(match.start() != null && match.start().isAfter(bestColorTime)) {
                    teamLoop: for(MatchDoc.Team team : match.competitors()) {
                        if(team.color() != null &&
                           available.containsKey(team.color()) &&
                           teamId.equals(team.league_team_id())) {

                            bestColor = team.color();
                            bestColorTime = match.start();
                            break teamLoop;
                        }
                    }
                }
            }

            // Fallback to random choice
            if(bestColor == null) {
                bestColor = IterableUtils.randomElement(available.keySet(), random);
            }

            return assignEntrant(entrant, available.get(bestColor), soft);

        } else {
            // If given a specific matchTeam, try to assign to that team
            if(this.teamMap.keySet().contains(matchTeam)) {
                return null;
            } else if(!soft) {
                return this.mapTeam(matchTeam, entrant) ? matchTeam : null;
            } else {
                return matchTeam;
            }
        }
    }

    public boolean allTeamsMapped() {
        return teamMap.keySet().containsAll(teams);
    }
}
