/*
 * Copyright 2019 Aleksander Jagiełło <[email protected]>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package pl.craftserve.radiation;

import com.google.common.collect.ImmutableSet;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.util.Location;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.internal.platform.WorldGuardPlatform;
import com.sk89q.worldguard.protection.ApplicableRegionSet;
import com.sk89q.worldguard.protection.flags.Flag;
import com.sk89q.worldguard.protection.regions.RegionContainer;
import org.bukkit.ChatColor;
import org.bukkit.Server;
import org.bukkit.boss.BossBar;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.MemoryConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;

public class Radiation implements Listener {
    private final Set<UUID> affectedPlayers = new HashSet<>(128);

    private final Plugin plugin;
    private final Matcher matcher;
    private final Config config;

    private BossBar bossBar;
    private Task task;

    public Radiation(Plugin plugin, Matcher matcher, Config config) {
        this.plugin = Objects.requireNonNull(plugin, "plugin");
        this.matcher = Objects.requireNonNull(matcher, "matcher");
        this.config = Objects.requireNonNull(config, "config");
    }

    public void enable() {
        Server server = this.plugin.getServer();
        this.bossBar = this.config.bar().create(server, ChatColor.DARK_RED);

        this.task = new Task();
        this.task.runTaskTimer(this.plugin, 20L, 20L);

        server.getPluginManager().registerEvents(this, this.plugin);
    }

    public void disable() {
        HandlerList.unregisterAll(this);

        if (this.task != null) {
            this.task.cancel();
        }

        if (this.bossBar != null) {
            this.bossBar.removeAll();
        }

        this.affectedPlayers.clear();
    }

    public boolean addAffectedPlayer(Player player, boolean addBossBar) {
        Objects.requireNonNull(player, "player");

        boolean ok = this.affectedPlayers.add(player.getUniqueId());
        if (ok && addBossBar) {
            boolean contains = this.bossBar.getPlayers().contains(player);

            if (!contains) {
                this.addBossBar(player);
                this.broadcastEscape(player);
            }
        }

        return ok;
    }

    private void addBossBar(Player player) {
        Objects.requireNonNull(player, "player");
        this.bossBar.addPlayer(player);
    }

    private void broadcastEscape(Player player) {
        Objects.requireNonNull(player, "player");
        this.plugin.getLogger().info(player.getName() + " has escaped to radiation zone at " + player.getLocation());

        this.config.escapeMessage().ifPresent(rawMessage -> {
            String message = ChatColor.RED + MessageFormat.format(rawMessage, player.getDisplayName() + ChatColor.RESET);
            for (Player online : this.plugin.getServer().getOnlinePlayers()) {
                if (online.canSee(player)) {
                    online.sendMessage(message);
                }
            }
        });
    }

    public Set<UUID> getAffectedPlayers() {
        return ImmutableSet.copyOf(this.affectedPlayers);
    }

    public boolean removeAffectedPlayer(Player player, boolean removeBossBar) {
        Objects.requireNonNull(player, "player");

        boolean ok = this.affectedPlayers.remove(player.getUniqueId());
        if (removeBossBar) {
            this.removeBossBar(player);
        }

        return ok;
    }

    public void removeBossBar(Player player) {
        Objects.requireNonNull(player, "player");
        this.bossBar.removePlayer(player);
    }

    @EventHandler(priority = EventPriority.MONITOR)
    public void onPlayerQuit(PlayerQuitEvent event) {
        removeAffectedPlayer(event.getPlayer(), true);
    }

    class Task extends BukkitRunnable {
        @Override
        public void run() {
            Server server = plugin.getServer();

            server.getOnlinePlayers().forEach(player -> {
                if (matcher.test(player)) {
                    RadiationEvent event = new RadiationEvent(player);
                    server.getPluginManager().callEvent(event);

                    boolean showBossBar = event.shouldShowWarning();
                    boolean cancel = event.isCancelled();

                    if (!cancel) {
                        this.hurt(player);
                        addAffectedPlayer(player, showBossBar);
                        return;
                    }

                    if (showBossBar) {
                        addBossBar(player);
                    }
                } else {
                    removeAffectedPlayer(player, true);
                }
            });
        }

        private void hurt(Player player) {
            Objects.requireNonNull(player, "player");
            config.effects().forEach(effect -> player.addPotionEffect(effect, true));
        }
    }

    /**
     * Something that tests if the player can be affected by the radiation.
     */
    public interface Matcher extends Predicate<Player> {
    }

    /**
     * Base interface for all matchers using WorldGuard to test the radiation.
     */
    public interface WorldGuardMatcher extends Matcher {
        @Override
        default boolean test(Player player) {
            RegionContainer regionContainer = this.getRegionContainer();
            return regionContainer != null && this.test(player, regionContainer);
        }

        default RegionContainer getRegionContainer() {
            WorldGuardPlatform platform = WorldGuard.getInstance().getPlatform();
            return platform != null ? platform.getRegionContainer() : null;
        }

        boolean test(Player player, RegionContainer regionContainer);
    }

    /**
     * Tests if the given flag matches radiation.
     */
    public static class FlagMatcher implements WorldGuardMatcher {
        private final Flag<Boolean> flag;

        public FlagMatcher(Flag<Boolean> flag) {
            this.flag = Objects.requireNonNull(flag, "flag");
        }

        @Override
        public boolean test(Player player, RegionContainer regionContainer) {
            Location location = BukkitAdapter.adapt(player.getLocation());
            location = location.setY(Math.max(0, Math.min(255, location.getY())));
            ApplicableRegionSet regions = regionContainer.createQuery().getApplicableRegions(location);

            Boolean value = regions.queryValue(WorldGuardPlugin.inst().wrapPlayer(player), this.flag);
            return value != null && value;
        }
    }

    //
    // Config
    //

    public static class Config {
        private final BarConfig bar;
        private final Iterable<PotionEffect> effects;
        private final String escapeMessage;

        public Config(BarConfig bar, Iterable<PotionEffect> effects, String escapeMessage) {
            this.bar = Objects.requireNonNull(bar, "bar");
            this.effects = Objects.requireNonNull(effects, "effects");
            this.escapeMessage = Objects.requireNonNull(escapeMessage, "escapeMessage");
        }

        public Config(ConfigurationSection section) throws InvalidConfigurationException {
            if (section == null) {
                section = new MemoryConfiguration();
            }

            try {
                this.bar = new BarConfig(section.getConfigurationSection("bar"));
            } catch (InvalidConfigurationException e) {
                throw new InvalidConfigurationException("Could not parse bar section in radiation.", e);
            }

            List<PotionEffect> effects = new ArrayList<>();
            ConfigurationSection effectsSection = section.getConfigurationSection("effects");
            if (effectsSection != null) {
                for (String key : effectsSection.getKeys(false)) {
                    if (!effectsSection.isConfigurationSection(key)) {
                        continue;
                    }

                    ConfigurationSection effectSection = effectsSection.getConfigurationSection(key);
                    if (effectSection == null) {
                        continue;
                    }

                    PotionEffectType type = PotionEffectType.getByName(effectSection.getName());
                    if (type == null) {
                        throw new InvalidConfigurationException("Unknown effect type: " + key + ".");
                    }

                    effectSection.set("effect", type.getId());
                    effectSection.set("duration", 20 * 5); // duration, in ticks
                    effectSection.set("amplifier", effectSection.getInt("level", 1) - 1);

                    try {
                        effects.add(new PotionEffect(effectSection.getValues(false)));
                    } catch (NoSuchElementException e) {
                        throw new InvalidConfigurationException("Could not parse effect " + key + ".", e);
                    }
                }
            }

            this.effects = effects;

            String escapeMessage = RadiationPlugin.colorize(section.getString("escape-message"));
            this.escapeMessage = escapeMessage != null && !escapeMessage.isEmpty() ? escapeMessage : null;
        }

        public BarConfig bar() {
            return this.bar;
        }

        public Iterable<PotionEffect> effects() {
            return this.effects;
        }

        public Optional<String> escapeMessage() {
            return Optional.ofNullable(this.escapeMessage);
        }
    }
}