/*
 * This file is a part of the SteamWar software.
 *
 * Copyright (C) 2020  SteamWar.de-Serverteam
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.bausystem.world.regions;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.sk89q.worldedit.EditSession;
import de.steamwar.bausystem.commands.CommandTNT.TNTMode;
import de.steamwar.bausystem.world.Color;
import de.steamwar.bausystem.world.SizedStack;
import de.steamwar.sql.NoClipboardException;
import de.steamwar.sql.SchematicNode;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.logging.Level;

public class Region {

    private static final List<Region> regions = new ArrayList<>();
    static boolean buildArea = false;
    static boolean extensionArea = false;
    private static JsonObject regionsObject = new JsonObject();

    static {
        File regionsFile = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "regions.json");
        if (regionsFile.exists()) {
            try {
                regionsObject = new JsonParser().parse(new FileReader(regionsFile)).getAsJsonObject();
            } catch (JsonSyntaxException | IOException e) {
                Bukkit.getLogger().log(Level.WARNING, "Item JSON error");
            }
        }

        YamlConfiguration config = new YamlConfiguration();
        try {
            config.load(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "sections.yml"));
        } catch (InvalidConfigurationException | IOException e) {
            Bukkit.getLogger().log(Level.SEVERE, "Failed to load sections.yml", e);
        }

        ConfigurationSection prototypes = config.getConfigurationSection("prototypes");
        assert prototypes != null;
        for (String prototype : prototypes.getKeys(false)) {
            new Prototype(Objects.requireNonNull(prototypes.getConfigurationSection(prototype)));
        }

        ConfigurationSection regions = config.getConfigurationSection("regions");
        assert regions != null;
        for (String region : regions.getKeys(false)) {
            new Region(Objects.requireNonNull(regions.getConfigurationSection(region)));
        }
    }

    public static boolean buildAreaEnabled() {
        return buildArea;
    }

    public static boolean extensionAreaEnabled() {
        return extensionArea;
    }

    public static Region getRegion(Location location) {
        for (Region region : regions) {
            if (region.inRegion(location, RegionType.NORMAL, RegionExtensionType.NORMAL)) {
                return region;
            }
        }
        return GlobalRegion.getInstance();
    }

    public static void setGlobalColor(Color color) {
        for (Region region : regions) {
            region.setColor(color);
        }
    }

    public static void save() {
        File colorsFile = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "regions.json");
        if (!colorsFile.exists()) {
            try {
                colorsFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
        }
        try (FileOutputStream fileOutputStream = new FileOutputStream(colorsFile)) {
            fileOutputStream.write(regionsObject.toString().getBytes());
        } catch (IOException e) {
            e.printStackTrace();
            // Ignored
        }
    }

    private final String name;
    final Point minPoint;
    private final Prototype prototype;
    private final String optionsLinkedWith; // nullable
    private Region linkedRegion = null; // nullable
    private SizedStack<EditSession> undosessions;
    private SizedStack<EditSession> redosessions;

    private JsonObject regionOptions = new JsonObject();

    @Getter
    private TNTMode tntMode = Region.buildAreaEnabled() ? TNTMode.ONLY_TB : TNTMode.OFF;

    @Getter
    private boolean freeze = false;

    @Getter
    private boolean fire = false;

    @Getter
    private boolean protect = false;

    @Getter
    private Color color = Color.YELLOW;

    private Region(ConfigurationSection config) {
        name = config.getName();
        minPoint = new Point(config.getInt("minX"), config.getInt("minY"), config.getInt("minZ"));
        prototype = Prototype.prototypes.get(config.getString("prototype"));
        optionsLinkedWith = config.getString("optionsLinkedWith", null);
        if (!hasTestblock()) tntMode = TNTMode.OFF;
        regions.add(this);
        load();
    }

    public Region(String name) {
        this.name = name;
        this.minPoint = new Point(0, 0, 0);
        this.prototype = null;
        this.optionsLinkedWith = null;
        tntMode = TNTMode.OFF;
        load();
    }

    private void load() {
        if (regionsObject.has(name)) {
            regionOptions = regionsObject.getAsJsonObject(name);
            if (regionOptions.has("tnt")) {
                String tntName = regionsObject.getAsJsonObject(name).getAsJsonPrimitive("tnt").getAsString();
                try {
                    tntMode = TNTMode.valueOf(tntName);
                } catch (Exception e) {
                    // Ignored
                }
            }

            if (regionOptions.has("fire")) {
                fire = regionOptions.getAsJsonPrimitive("fire").getAsBoolean();
            }

            if (regionOptions.has("freeze")) {
                freeze = regionOptions.getAsJsonPrimitive("freeze").getAsBoolean();
            }

            if (regionOptions.has("protect")) {
                protect = regionOptions.getAsJsonPrimitive("protect").getAsBoolean();
            }

            if (regionOptions.has("color")) {
                String colorName = regionOptions.getAsJsonPrimitive("color").getAsString();
                try {
                    color = Color.valueOf(colorName);
                } catch (Exception e) {
                    // Ignored
                }
            }
        } else {
            regionsObject.add(name, regionOptions);
        }
    }

    public void setColor(Color color) {
        this.color = color;
        regionOptions.add("color", new JsonPrimitive(color.name()));
    }

    private void setLinkedRegion(Consumer<Region> regionConsumer) {
        if (optionsLinkedWith == null) {
            return;
        }
        if (linkedRegion != null) {
            regionConsumer.accept(linkedRegion);
            return;
        }
        for (Region region : regions) {
            if (region.name.equals(name)) {
                linkedRegion = region;
                regionConsumer.accept(linkedRegion);
                return;
            }
        }
    }

    public void setTntMode(TNTMode tntMode) {
        this.tntMode = tntMode;
        setLinkedRegion(region -> {
            region.tntMode = tntMode;
            region.regionOptions.add("tnt", new JsonPrimitive(tntMode.name()));
        });
        regionOptions.add("tnt", new JsonPrimitive(tntMode.name()));
    }

    public void setFreeze(boolean freeze) {
        this.freeze = freeze;
        setLinkedRegion(region -> {
            region.freeze = freeze;
            region.regionOptions.add("freeze", new JsonPrimitive(freeze));
        });
        regionOptions.add("freeze", new JsonPrimitive(freeze));
    }

    public void setFire(boolean fire) {
        this.fire = fire;
        setLinkedRegion(region -> {
            region.fire = fire;
            region.regionOptions.add("fire", new JsonPrimitive(fire));
        });
        regionOptions.add("fire", new JsonPrimitive(fire));
    }

    public void setProtect(boolean protect) {
        this.protect = protect;
        setLinkedRegion(region -> {
            region.protect = protect;
            region.regionOptions.add("protect", new JsonPrimitive(protect));
        });
        regionOptions.add("protect", new JsonPrimitive(protect));
    }

    public Point getMinPoint(RegionType regionType, RegionExtensionType regionExtensionType) {
        switch (regionType) {
            case BUILD:
                return prototype.buildArea.getMinPoint(this, regionExtensionType);
            case TESTBLOCK:
                return prototype.testblock.getMinPoint(this, regionExtensionType);
            default:
            case NORMAL:
                return prototype.getMinPoint(this, regionExtensionType);
        }
    }

    public Point getMaxPoint(RegionType regionType, RegionExtensionType regionExtensionType) {
        switch (regionType) {
            case BUILD:
                return prototype.buildArea.getMaxPoint(this, regionExtensionType);
            case TESTBLOCK:
                return prototype.testblock.getMaxPoint(this, regionExtensionType);
            default:
            case NORMAL:
                return prototype.getMaxPoint(this, regionExtensionType);
        }
    }

    public boolean inRegion(Location l, RegionType regionType, RegionExtensionType regionExtensionType) {
        switch (regionType) {
            case BUILD:
                return prototype.buildArea.inRegion(this, l, regionExtensionType);
            case TESTBLOCK:
                return prototype.testblock.inRegion(this, l, regionExtensionType);
            default:
            case NORMAL:
                return prototype.inRegion(this, l, regionExtensionType);
        }
    }

    public boolean hasBuildRegion() {
        return prototype.buildArea != null;
    }

    public void reset(SchematicNode schem, boolean ignoreAir) throws IOException, NoClipboardException {
        initSessions();
        undosessions.push(prototype.reset(this, schem, ignoreAir, color));
    }

    public boolean hasTestblock() {
        return prototype.hasTestblock();
    }

    public void resetTestblock(SchematicNode schem, boolean reset) throws IOException, NoClipboardException {
        initSessions();
        undosessions.push(prototype.resetTestblock(this, schem, color, reset));
    }

    public boolean hasProtection() {
        return prototype.hasProtection();
    }

    public void protect(SchematicNode schem) throws IOException, NoClipboardException {
        initSessions();
        undosessions.push(prototype.protect(this, schem));
    }

    public int getProtectYLevel() {
        return getMinPoint(RegionType.TESTBLOCK, RegionExtensionType.NORMAL).getY();
    }

    public boolean hasExtensionArea(RegionType regionType) {
        switch (regionType) {
            case BUILD:
                return prototype.buildArea.extensionPrototypeArea;
            case TESTBLOCK:
                return prototype.testblock.extensionPrototypeArea;
            default:
            case NORMAL:
                return prototype.extensionPrototypeArea;
        }
    }

    private void initSessions() {
        if (undosessions == null) {
            undosessions = new SizedStack<>(20);
            redosessions = new SizedStack<>(20);
        }
    }

    public boolean undo() {
        initSessions();
        EditSession session = null;
        try {
            session = undosessions.pop();
            if (session == null) {
                return false;
            }
            session.undo(session);
            redosessions.push(session);
            return true;
        } finally {
            if (session != null) {
                session.flushQueue();
            }
        }
    }

    public boolean redo() {
        initSessions();
        EditSession session = null;
        try {
            session = redosessions.pop();
            if (session == null) {
                return false;
            }
            session.redo(session);
            undosessions.push(session);
            return true;
        } finally {
            if (session != null) {
                session.flushQueue();
            }
        }
    }

}
