package tc.oc.pgm.scoreboard;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import org.apache.commons.lang.StringUtils;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
import java.time.Duration;
import tc.oc.commons.bukkit.chat.ComponentRenderers;
import tc.oc.commons.bukkit.chat.NameStyle;
import tc.oc.commons.bukkit.util.NullCommandSender;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.scheduler.Task;
import tc.oc.pgm.Config;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.destroyable.Destroyable;
import tc.oc.pgm.events.FeatureChangeEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchPlayerDeathEvent;
import tc.oc.pgm.events.MatchResultChangeEvent;
import tc.oc.pgm.events.MatchScoreChangeEvent;
import tc.oc.pgm.events.PartyAddEvent;
import tc.oc.pgm.events.PartyRemoveEvent;
import tc.oc.pgm.events.PartyRenameEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.ffa.Tribute;
import tc.oc.pgm.goals.Goal;
import tc.oc.pgm.goals.GoalMatchModule;
import tc.oc.pgm.goals.ProximityGoal;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalProximityChangeEvent;
import tc.oc.pgm.goals.events.GoalStatusChangeEvent;
import tc.oc.pgm.goals.events.GoalTouchEvent;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.Party;
import tc.oc.pgm.score.ScoreMatchModule;
import tc.oc.pgm.spawns.events.ParticipantSpawnEvent;
import tc.oc.pgm.teams.events.TeamRespawnsChangeEvent;
import tc.oc.pgm.victory.VictoryMatchModule;
import tc.oc.pgm.wool.MonumentWool;
import tc.oc.pgm.wool.MonumentWoolFactory;

import static tc.oc.commons.core.util.Nullables.castOrNull;

@ListenerScope(MatchScope.LOADED)
public class SidebarMatchModule extends MatchModule implements Listener {

    public static final int MAX_ROWS = 16;      // Max rows on the scoreboard
    public static final int MAX_PREFIX = 16;    // Max chars in a team prefix
    public static final int MAX_SUFFIX = 16;    // Max chars in a team suffix

    @Inject private List<MonumentWoolFactory> wools;

    private final String legacyTitle;

    protected final Map<Party, Sidebar> sidebars = new HashMap<>();
    protected final Map<Goal, BlinkTask> blinkingGoals = new HashMap<>();

    private class Sidebar {
        private static final String IDENTIFIER = "pgm";

        private final Scoreboard scoreboard;
        private final Objective objective;

        // Each row has its own scoreboard team
        protected final String[] rows = new String[MAX_ROWS];
        protected final int[] scores = new int[MAX_ROWS];
        protected final Team[] teams = new Team[MAX_ROWS];
        protected final String[] players = new String[MAX_ROWS];

        private Sidebar(Party party) {
            this.scoreboard = getMatch().needMatchModule(ScoreboardMatchModule.class).getScoreboard(party);
            this.objective = this.scoreboard.registerNewObjective(IDENTIFIER, "dummy");
            this.objective.setDisplayName(legacyTitle);
            this.objective.setDisplaySlot(DisplaySlot.SIDEBAR);

            for(int i = 0; i < MAX_ROWS; ++i) {
                this.rows[i] = null;
                this.scores[i] = -1;

                this.players[i] = String.valueOf(ChatColor.COLOR_CHAR) + (char) i;

                this.teams[i] = this.scoreboard.registerNewTeam(IDENTIFIER + "-row-" + i);
                this.teams[i].setPrefix("");
                this.teams[i].setSuffix("");
                this.teams[i].addEntry(this.players[i]);
            }
        }

        public Scoreboard getScoreboard() {
            return this.scoreboard;
        }

        public Objective getObjective() {
            return this.objective;
        }

        private void setRow(int maxScore, int row, @Nullable String text) {
            if(row < 0 || row >= MAX_ROWS) return;

            int score = text == null ? -1 : maxScore - row - 1;
            if(this.scores[row] != score) {
                this.scores[row] = score;

                if(score == -1) {
                    this.scoreboard.resetScores(this.players[row]);
                } else {
                    this.objective.getScore(this.players[row]).setScore(score);
                }
            }

            if(!Objects.equals(this.rows[row], text)) {
                this.rows[row] = text;

                if(text != null) {
                    /*
                     Split the row text into prefix and suffix, limited to 16 chars each. Because the player name
                     is a color code, we have to restore the color at the split in the suffix. We also have to be
                     careful not to split in the middle of a color code.
                    */
                    int split = MAX_PREFIX - 1; // Start by assuming there is a color code right on the split
                    if(text.length() < MAX_PREFIX || text.charAt(split) != ChatColor.COLOR_CHAR) {
                        // If there isn't, we can fit one more char in the prefix
                        split++;
                    }

                    // Split and truncate the text, and restore the color in the suffix
                    String prefix = StringUtils.substring(text, 0, split);
                    String lastColors = org.bukkit.ChatColor.getLastColors(prefix);
                    String suffix =  lastColors + StringUtils.substring(text, split, split + MAX_SUFFIX - lastColors.length());
                    this.teams[row].setPrefix(prefix);
                    this.teams[row].setSuffix(suffix);
                }
            }
        }
    }

    public SidebarMatchModule(Match match, BaseComponent title) {
        super(match);
        this.legacyTitle = StringUtils.left(
            ComponentRenderers.toLegacyText(
                new Component(title, ChatColor.AQUA),
                NullCommandSender.INSTANCE
            ),
            32
        );
    }

    private boolean hasScores() {
        return getMatch().getMatchModule(ScoreMatchModule.class) != null;
    }

    private boolean isBlitz() {
        return getMatch().getMatchModule(BlitzMatchModule.class) != null;
    }

    private boolean isCompactWool() {
        final int woolTeams = (int) wools.stream()
                                         .map(MonumentWoolFactory::getOwner)
                                         .distinct()
                                         .count();
        return !wools.isEmpty() && MAX_ROWS < woolTeams * 2 - 1 + wools.size();
    }

    private void addSidebar(Party party) {
        logger.fine("Adding sidebar for party " + party);
        sidebars.put(party, new Sidebar(party));
    }

    @Override
    public void load() {
        super.load();
        for(Party party : getMatch().getParties()) addSidebar(party);
        renderSidebarDebounce();
    }

    @Override
    public void enable() {
        super.enable();
        renderSidebarDebounce();
    }

    @Override
    public void disable() {
        for(BlinkTask task : ImmutableSet.copyOf(this.blinkingGoals.values())) {
            task.stop();
        }
    }

    @EventHandler
    public void addParty(PartyAddEvent event) {
        addSidebar(event.getParty());
        renderSidebarDebounce();
    }

    @EventHandler
    public void removeParty(PartyRemoveEvent event) {
        logger.fine("Removing sidebar for party " + event.getParty());
        sidebars.remove(event.getParty());
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onPartyChange(PlayerPartyChangeEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onDeath(MatchPlayerDeathEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onSpawn(ParticipantSpawnEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onPartyRename(final PartyRenameEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void scoreChange(final MatchScoreChangeEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void goalTouch(final GoalTouchEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void goalStatusChange(final GoalStatusChangeEvent event) {
        if(event.getGoal() instanceof Destroyable && ((Destroyable) event.getGoal()).getShowProgress()) {
            blinkGoal(event.getGoal(), 3, Duration.ofSeconds(1));
        } else {
            renderSidebarDebounce();
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void goalProximityChange(final GoalProximityChangeEvent event) {
        if(Config.Scoreboard.showProximity()) {
            renderSidebarDebounce();
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void goalComplete(final GoalCompleteEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void goalChange(final FeatureChangeEvent event) {
        if (event.getFeature() instanceof Goal) {
            renderSidebarDebounce();
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void updateRespawnLimit(final TeamRespawnsChangeEvent event) {
        renderSidebarDebounce();
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void resultChange(MatchResultChangeEvent event) {
        renderSidebarDebounce();
    }

    private String renderGoal(Goal<?> goal, @Nullable Competitor competitor, Party viewingParty) {
        StringBuilder sb = new StringBuilder(" ");

        BlinkTask blinkTask = this.blinkingGoals.get(goal);
        if(blinkTask != null && blinkTask.isDark()) {
            sb.append(ChatColor.BLACK);
        } else {
            sb.append(goal.renderSidebarStatusColor(competitor, viewingParty));
        }
        sb.append(goal.renderSidebarStatusText(competitor, viewingParty));

        if(goal instanceof ProximityGoal) {
            sb.append(" ");
            // Show teams their own proximity on shared goals
            Competitor proximityCompetitor = competitor != null ? competitor : castOrNull(viewingParty, Competitor.class);
            sb.append(((ProximityGoal) goal).renderProximity(proximityCompetitor, viewingParty));
        }

        sb.append(" ");
        sb.append(goal.renderSidebarLabelColor(competitor, viewingParty));
        sb.append(goal.renderSidebarLabelText(competitor, viewingParty));

        return sb.toString();
    }

    private String renderScore(Competitor competitor, Party viewingParty) {
        ScoreMatchModule smm = getMatch().needMatchModule(ScoreMatchModule.class);
        String text = ChatColor.WHITE.toString() + (int) smm.getScore(competitor);
        if(smm.hasScoreLimit()) {
            text += ChatColor.DARK_GRAY + "/" + ChatColor.GRAY + smm.getScoreLimit();
        }
        return text;
    }

    private String renderBlitz(Competitor competitor, Party viewingParty) {
        BlitzMatchModule bmm = getMatch().needMatchModule(BlitzMatchModule.class);
        if(competitor instanceof tc.oc.pgm.teams.Team) {
            return ChatColor.WHITE.toString() + bmm.getRemainingPlayers(competitor);
        } else if(competitor instanceof Tribute && bmm.getConfig().getNumLives() > 1) {
            return ChatColor.WHITE.toString() + bmm.lifeManager.getLives(competitor.getPlayers().iterator().next().getPlayerId());
        } else {
            return "";
        }
    }

    private void renderSidebarDebounce() {
        match.getScheduler(MatchScope.LOADED).debounceTask(this::renderSidebar);
    }

    private void renderSidebar() {
        final boolean hasScores = hasScores();
        final boolean isBlitz = isBlitz();
        final GoalMatchModule gmm = match.needMatchModule(GoalMatchModule.class);

        Set<Competitor> competitorsWithGoals = new HashSet<>();
        List<Goal> sharedGoals = new ArrayList<>();

        // Count the rows used for goals
        for(Goal goal : gmm.getGoals()) {
            if(goal.isVisible()) {
                if(goal.isShared()) {
                    sharedGoals.add(goal);
                } else {
                    for(Competitor competitor : gmm.getCompetitors(goal)) {
                        competitorsWithGoals.add(competitor);
                    }
                }
            }
        }

        for(Map.Entry<Party, Sidebar> entry : this.sidebars.entrySet()) {
            Party viewingParty = entry.getKey();
            Sidebar sidebar = entry.getValue();

            List<String> rows = new ArrayList<>(MAX_ROWS);

            // Scores/Blitz
            if(hasScores || isBlitz) {
                for(Competitor competitor : getMatch().needMatchModule(VictoryMatchModule.class).rankedCompetitors()) {
                    String text;
                    if(hasScores) {
                        text = renderScore(competitor, viewingParty);
                    } else {
                        text = renderBlitz(competitor, viewingParty);
                    }
                    if(text.length() != 0) text += " ";
                    rows.add(text + ComponentRenderers.toLegacyText(competitor.getStyledName(NameStyle.GAME), NullCommandSender.INSTANCE));
                }

                if(!competitorsWithGoals.isEmpty() || !sharedGoals.isEmpty()) {
                    // Blank row between scores and goals
                    rows.add("");
                }
            }

            boolean firstTeam = true;

            // Shared goals i.e. not grouped under a specific team
            for(Goal goal : sharedGoals) {
                firstTeam = false;
                rows.add(this.renderGoal(goal, null, viewingParty));
            }

            // Team-specific goals
            List<Competitor> sortedCompetitors = new ArrayList<>(competitorsWithGoals);
            if(viewingParty instanceof Competitor) {
                // Participants see competitors in arbitrary order, with their own at the top
                Collections.sort(sortedCompetitors, Ordering.arbitrary());

                // Bump viewing party to the top of the list
                if(sortedCompetitors.remove(viewingParty)) {
                    sortedCompetitors.add(0, (Competitor) viewingParty);
                }
            } else {
                // Observers see the competitors sorted by closeness to winning
                Collections.sort(sortedCompetitors, match.needMatchModule(VictoryMatchModule.class).victoryOrder());
            }

            for(Competitor competitor : sortedCompetitors) {
                if(!firstTeam) {
                    // Add a blank row between teams
                    rows.add("");
                }
                firstTeam = false;

                // Add a row for the team name
                rows.add(ComponentRenderers.toLegacyText(competitor.getStyledName(NameStyle.GAME),
                                                         NullCommandSender.INSTANCE));

                if(isCompactWool()) {
                    String woolText = " ";
                    boolean firstWool = true;

                    List<Goal> sortedWools = new ArrayList<>(gmm.getGoals(competitor));
                    Collections.sort(sortedWools, new Comparator<Goal>() { @Override public int compare(Goal a, Goal b) {
                            return a.getName().compareToIgnoreCase(b.getName());
                    }});

                    for(Goal goal : sortedWools) {
                        if(goal instanceof MonumentWool && goal.isVisible()) {
                            MonumentWool wool = (MonumentWool) goal;
                            if(!firstWool) {
                                woolText += "   ";
                            }
                            firstWool = false;
                            woolText += wool.renderSidebarStatusColor(competitor, viewingParty);
                            woolText += wool.renderSidebarStatusText(competitor, viewingParty);
                        }
                    }

                    rows.add(woolText);

                } else {
                    // Add a row for each of this team's goals
                    for(Goal goal : gmm.getGoals()) {
                        if(!goal.isShared() && goal.canComplete(competitor) && goal.isVisible()) {
                            rows.add(this.renderGoal(goal, competitor, viewingParty));
                        }
                    }
                }
            }

            // Need at least one row for the sidebar to show
            if(rows.isEmpty()) {
                rows.add("");
            }

            for(int i = 0; i < MAX_ROWS; i++) {
                if(i < rows.size()) {
                    sidebar.setRow(rows.size(), i, rows.get(i));
                } else {
                    sidebar.setRow(rows.size(), i, null);
                }
            }
        }
    }

    public void blinkGoal(Goal goal, float rateHz, @Nullable Duration duration) {
        BlinkTask task = this.blinkingGoals.get(goal);
        if(task != null) {
            task.reset(duration);
        } else {
            this.blinkingGoals.put(goal, new BlinkTask(goal, rateHz, duration));
        }
    }

    public void stopBlinkingGoal(Goal goal) {
        BlinkTask task = this.blinkingGoals.remove(goal);
        if(task != null) task.stop();
    }

    private class BlinkTask implements Runnable {
        private final Task task;
        private final Goal goal;
        private final long intervalTicks;

        private boolean dark;
        private Long ticksRemaining;

        private BlinkTask(Goal goal, float rateHz, @Nullable Duration duration) {
            this.goal = goal;
            this.intervalTicks = (long) (10f / rateHz);
            this.task = getMatch().getScheduler(MatchScope.RUNNING).createRepeatingTask(0, intervalTicks, this);

            this.reset(duration);
        }

        public void reset(@Nullable Duration duration) {
            this.ticksRemaining = duration == null ? null : duration.toMillis() / 50;
        }

        public void stop() {
            this.task.cancel();
            SidebarMatchModule.this.blinkingGoals.remove(this.goal);
            renderSidebarDebounce();
        }

        public boolean isDark() {
            return this.dark;
        }

        @Override
        public void run() {
            if(this.ticksRemaining != null) {
                this.ticksRemaining -= this.intervalTicks;
                if(this.ticksRemaining <= 0) {
                    this.task.cancel();
                    SidebarMatchModule.this.blinkingGoals.remove(this.goal);
                }
            }

            this.dark = !this.dark;
            renderSidebarDebounce();
        }
    }
}