package tc.oc.lobby.bukkit;

import java.util.ArrayList;
import java.util.Collection;
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.Optional;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Consumer;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;

import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.material.MaterialData;
import tc.oc.api.docs.Arena;
import tc.oc.api.docs.Game;
import tc.oc.api.docs.Server;
import tc.oc.api.games.GameStore;
import tc.oc.commons.bukkit.chat.ComponentRenderContext;
import tc.oc.commons.bukkit.event.BlockPunchEvent;
import tc.oc.commons.bukkit.format.GameFormatter;
import tc.oc.commons.bukkit.format.ServerFormatter;
import tc.oc.commons.bukkit.teleport.Navigator;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.ChunkLocation;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.bukkit.util.Vectors;
import tc.oc.commons.core.chat.ChatUtils;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.chat.Components;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.CacheUtils;

public class SignUpdater implements Listener, PluginFacet {

    private static final int SIGN_WIDTH = 90;
    private static final ImmutableSet<Material> PORTAL_MATERIALS = ImmutableSet.of(Material.PORTAL);

    private final Logger logger;
    private final Collection<World> initialWorlds;
    private final ComponentRenderContext renderer;
    private final GameStore games;
    private final GameFormatter.Dark gameFormatter;
    private final Navigator navigator;

    private final Set<ChunkLocation> chunks = new HashSet<>();
    private final Map<Location, SignHandle> signs = new HashMap<>();
    private final Map<Player, SignHandle> hovering = new WeakHashMap<>();

    @Inject SignUpdater(Loggers loggers,
                        Collection<World> initialWorlds,
                        ComponentRenderContext renderer,
                        GameStore games,
                        GameFormatter.Dark gameFormatter,
                        Navigator navigator) {
        this.logger = loggers.get(getClass());
        this.initialWorlds = initialWorlds;
        this.renderer = renderer;
        this.games = games;
        this.gameFormatter = gameFormatter;
        this.navigator = navigator;
    }

    @Override
    public void enable() {
        initialWorlds.forEach(world -> Stream.of(world.getLoadedChunks()).forEach(this::load));
    }

    @Override
    public void disable() {
        hovering.forEach((player, sign) -> sign.hover(player, false));
        hovering.clear();
    }

    private @Nullable SignHandle createSign(Sign blockState) {
        String prev = "";
        final List<Navigator.Connector> connectors = new ArrayList<>();

        for(BaseComponent component : blockState.lines()) {
            String line = component.toPlainText();
            if(line.endsWith("\\")) {
                prev += line.substring(0, line.length() - 1);
            } else {
                final Navigator.Connector connector = navigator.parseConnector(prev + line);
                if(connector != null) {
                    connectors.add(connector);
                }
            }
        }

        return connectors.isEmpty() ? null : new SignHandle(blockState, navigator.combineConnectors(connectors));
    }

    void load(Chunk chunk) {
        if(chunks.add(ChunkLocation.of(chunk))) {
            for(BlockState blockState : chunk.getTileEntities()) {
                if(blockState instanceof Sign) {
                    final SignHandle sign = createSign((Sign) blockState);
                    if(sign != null) {
                        signs.put(blockState.getLocation(), sign);
                    }
                }
            }

            logger.fine(() -> "Loaded chunk " + chunk.getX() + "," + chunk.getZ() +" with " + signs.size());
        }
    }

    @EventHandler
    private void onChunkLoad(ChunkLoadEvent event) {
        load(event.getChunk());
    }

    private Optional<SignHandle> sign(Location location) {
        return Optional.ofNullable(signs.get(location));
    }

    public Optional<SignHandle> nearestSign(Location location) {
        return signs.values().stream()
                    .filter(sign -> sign.connector().isVisible())
                    .min(Comparator.comparing(sign -> sign.distanceSquared(location)));
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void move(final PlayerMoveEvent event) {
        Player player = event.getPlayer();
        Location to = event.getTo();
        Location from = event.getFrom();
        Block toBlock = to.getBlock();
        Block fromBlock = from.getBlock();

        handleLook(player, to);

        if(PORTAL_MATERIALS.contains(toBlock.getType()) && !PORTAL_MATERIALS.contains(fromBlock.getType())) {
            nearestSign(event.getTo()).ifPresent(
                sign -> sign.connector().teleport(player)
            );
        }
    }

    private void handleLook(Player player, Location loc) {
        final RayBlockIntersection hit = player.getWorld().rayTraceBlock(loc.clone().add(0, player.getEyeHeight(), 0), player.getBlockReach(), true, false);
        final SignHandle newSign = hit == null ? null : sign(hit.getBlock().getLocation()).orElse(null);
        final SignHandle oldSign = hovering.get(player);

        if(!Objects.equals(oldSign, newSign)) {
            if(oldSign != null) {
                hovering.remove(player);
                oldSign.hover(player, false);
            }
            if(newSign != null) {
                hovering.put(player, newSign);
                newSign.hover(player, true);
            }
        }
    }

    private void handleBlockClick(Player player, Block block) {
        if(block != null && block.getState() instanceof Sign) {
            sign(block.getLocation()).ifPresent(
                sign -> sign.connector().teleport(player)
            );
        }
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void rightClick(final PlayerInteractEvent event) {
        handleBlockClick(event.getPlayer(), event.getClickedBlock());
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void leftClick(final BlockPunchEvent event) {
        handleBlockClick(event.getPlayer(), event.getBlock());
    }

    class SignHandle {
        private final Location location;
        private final MaterialData material;
        private final Navigator.Connector connector;

        private final LoadingCache<BaseComponent, NMSHacks.FakeArmorStand> hoverEntities;

        private final Map<Player, NMSHacks.FakeArmorStand> hovering = new WeakHashMap<>();

        private final Consumer<Navigator.Connector> observer = c -> paint();

        public SignHandle(Sign sign, Navigator.Connector connector) {
            this.location = sign.getLocation();
            this.material = sign.getMaterialData();
            this.connector = connector;

            hoverEntities = CacheUtils.newCache(
                description -> new NMSHacks.FakeArmorStand(this.location.getWorld(), description.toLegacyText())
            );

            connector.startObserving(observer);

            paint();

            logger.fine("Created " + this);
        }

        @Override
        public String toString() {
            return getClass().getSimpleName() +
                   "{location=(" + Vectors.format(location.position(), "%.0f") +
                   ") connector=" + connector +
                   "}";
        }

        public double distanceSquared(Location to) {
            return this.location.getWorldId().equals(to.getWorldId()) ? this.location.distanceSquared(to) : Double.POSITIVE_INFINITY;
        }

        public Navigator.Connector connector() {
            return connector;
        }

        public void hover(Player player, boolean over) {
            if(over) {
                final BaseComponent description = connector.description();
                if(description != null) {
                    final NMSHacks.FakeArmorStand entity = hoverEntities.getUnchecked(renderer.render(new Component(description, ChatColor.DARK_AQUA), player));
                    hovering.put(player, entity);
                    entity.spawn(player, BlockUtils.center(location));
                }

            } else {
                final NMSHacks.FakeArmorStand entity = hovering.remove(player);
                if(entity != null) {
                    entity.destroy(player);
                }
            }
        }

        private void paint() {
            final BaseComponent[] lines = new BaseComponent[4];

            if(connector.isVisible()) {
                final Object mapped = connector.mappedTo();
                if(Navigator.DEFAULT_MAPPING.equals(mapped)) {
                    renderDefault(lines);
                } else if(mapped instanceof Server) {
                    renderServer(lines, (Server) mapped);
                } else if(mapped instanceof Arena) {
                    renderArena(lines, (Arena) mapped);
                }
            }

            final BlockState block = location.getBlock().getState();
            if(lines[0] != null) {
                final Sign sign;
                if(block instanceof Sign) {
                    sign = (Sign) block;
                } else {
                    block.setMaterialData(material);
                    block.update(true, false);
                    sign = (Sign) location.getBlock().getState();
                }

                for(int i = 0; i < 4; i++) {
                    sign.setLine(i, renderer.render(lines[i], Bukkit.getConsoleSender()));
                }
                block.update(true, false);

            } else if(block.getMaterial() != Material.AIR) {
                block.setMaterial(Material.AIR);
                block.update(true, false);
            }
        }

        void renderDefault(BaseComponent[] lines) {
            lines[0] = Components.blank();
            lines[1] = new Component(new TranslatableComponent("servers.backToLobby"), ChatColor.DARK_BLUE);
            lines[2] = Components.blank();
            lines[3] = Components.blank();
        }

        void renderServer(BaseComponent[] lines, Server server) {
            final ServerFormatter formatter = ServerFormatter.dark;

            lines[0] = formatter.name(server);

            if(formatter.isRestarting(server)) {
                lines[1] = Components.blank();
                lines[2] = Components.blank();
                lines[3] = formatter.onlineStatus(server);
            } else {
                lines[1] = formatter.playerCounts(server, false);

                if(server.current_match() != null && server.current_match().map() != null) {
                    final ChatColor color = formatter.matchStatusColor(server);
                    final List<String> matchText = ChatUtils.wordWrap(server.current_match().map().name(), SIGN_WIDTH);

                    if(matchText.size() == 1) {
                        lines[2] = Components.blank();
                        lines[3] = new Component(matchText.get(0), color);
                    } else {
                        lines[2] = new Component(matchText.get(0), color);
                        lines[3] = new Component(matchText.get(1), color);
                    }
                } else {
                    lines[2] = Components.blank();
                    lines[3] = Components.blank();
                }
            }
        }

        void renderArena(BaseComponent[] lines, Arena arena) {
            final Game game = games.byId(arena.game_id());

            lines[0] = new Component(game.name(), ChatColor.BLACK, ChatColor.BOLD);
            lines[1] = Components.blank();
            lines[2] = gameFormatter.playingCount(arena);
            lines[3] = gameFormatter.waitingCount(arena);
        }
    }
}
