/*
 * 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.region.dynamic;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonWriter;
import com.sk89q.worldedit.EditSession;
import de.steamwar.bausystem.region.FlagStorage;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.RegionBackups;
import de.steamwar.bausystem.region.dynamic.normal.NormalFlagStorage;
import de.steamwar.bausystem.region.dynamic.normal.display.MicroWarGear21DisplayRegion;
import de.steamwar.bausystem.region.dynamic.normal.display.MiniWarGear21DisplayRegion;
import de.steamwar.bausystem.region.dynamic.normal.display.WarGear21DisplayRegion;
import de.steamwar.bausystem.region.dynamic.normal.display.WarShip21DisplayRegion;
import de.steamwar.bausystem.region.dynamic.normal.work.MicroWarGear21WorkRegion;
import de.steamwar.bausystem.region.dynamic.normal.work.MiniWarGear21WorkRegion;
import de.steamwar.bausystem.region.dynamic.normal.work.WarGear21WorkRegion;
import de.steamwar.bausystem.region.dynamic.normal.work.WarShip21WorkRegion;
import de.steamwar.bausystem.region.dynamic.path.PathRegion;
import de.steamwar.bausystem.region.dynamic.spawn.SpawnPathRegion;
import de.steamwar.bausystem.region.dynamic.spawn.SpawnRegion;
import de.steamwar.bausystem.region.dynamic.special.DrySpecialRegion;
import de.steamwar.bausystem.region.dynamic.special.WetSpecialRegion;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.utils.FlatteningWrapper;
import de.steamwar.bausystem.utils.PasteBuilder;
import lombok.Cleanup;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;

import javax.annotation.Nullable;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;

@UtilityClass
public class RegionDataRepository {

    public static final String META_FILE_NAME = "meta.json";
    public static final String FLAGS_FILE_NAME = "flags.json";
    public static final String REGION_SCHEM_FILE_NAME = "region.schem";
    public static final String BACKUP_DIRECTORY = "backup";
    private static File regionDataFolder = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "regions");
    private static Map<String, Function<RegionConstructorData, DynamicRegion>> regionCreators = new HashMap<>();
    static {
        regionCreators.put(SpawnRegion.class.getSimpleName(), SpawnRegion::new);
        regionCreators.put(SpawnPathRegion.class.getSimpleName(), SpawnPathRegion::new);
        regionCreators.put(PathRegion.class.getSimpleName(), PathRegion::new);
        regionCreators.put(DrySpecialRegion.class.getSimpleName(), DrySpecialRegion::new);
        regionCreators.put(WetSpecialRegion.class.getSimpleName(), WetSpecialRegion::new);
        regionCreators.put(WarGear21WorkRegion.class.getSimpleName(), WarGear21WorkRegion::new);
        regionCreators.put(WarGear21DisplayRegion.class.getSimpleName(), WarGear21DisplayRegion::new);
        regionCreators.put("WarGear21Region", WarGear21WorkRegion::new); // TODO: Legacy because of rename
        regionCreators.put(MiniWarGear21WorkRegion.class.getSimpleName(), MiniWarGear21WorkRegion::new);
        regionCreators.put(MiniWarGear21DisplayRegion.class.getSimpleName(), MiniWarGear21DisplayRegion::new);
        regionCreators.put("MiniWarGear21Region", MiniWarGear21WorkRegion::new); // TODO: Legacy because of rename
        regionCreators.put(WarShip21WorkRegion.class.getSimpleName(), WarShip21WorkRegion::new);
        regionCreators.put(WarShip21DisplayRegion.class.getSimpleName(), WarShip21WorkRegion::new);
        regionCreators.put("WarShip21Region", WarShip21WorkRegion::new); // TODO: Legacy because of rename
        regionCreators.put(MicroWarGear21WorkRegion.class.getSimpleName(), MicroWarGear21WorkRegion::new);
        regionCreators.put(MicroWarGear21DisplayRegion.class.getSimpleName(), MicroWarGear21DisplayRegion::new);
        regionCreators.put("MicroWarGear21Region", MicroWarGear21WorkRegion::new); // TODO: Legacy because of rename
    }

    static {
        regionDataFolder.mkdirs();
    }

    private File getRegionDirectory(Region region) {
        File file = new File(regionDataFolder, region.getID().toString());
        file.mkdirs();
        return file;
    }

    @SneakyThrows
    public List<DynamicRegion> loadRegions() {
        File[] files = regionDataFolder.listFiles();
        List<DynamicRegion> regions = new ArrayList<>();
        for (File file : files) {
            File metaFile = new File(file, META_FILE_NAME);
            if (!metaFile.exists()) continue;
            JsonObject jsonObject = JsonParser.parseReader(new FileReader(metaFile)).getAsJsonObject();
            RegionConstructorData regionConstructorData = new RegionConstructorData(jsonObject, UUID.fromString(file.getName()));
            Function<RegionConstructorData, DynamicRegion> constructor = regionCreators.get(regionConstructorData.regionClass);
            if (constructor == null) continue;
            regions.add(constructor.apply(regionConstructorData));
        }
        return regions;
    }

    public void saveRegion(DynamicRegion region) {
        File file = getRegionDirectory(region);
        file = new File(file, META_FILE_NAME);
        region.getRegionConstructorData()
                .write(file);
    }

    public void deleteRegion(DynamicRegion region) {
        deleteDir(getRegionDirectory(region));
    }

    public void loadFlagStorage(Region region, DefaultFlagStorage storage) {
        File file = getRegionDirectory(region);
        file = new File(file, FLAGS_FILE_NAME);
        loadFlagStorage(file, storage);
        if (!file.exists()) saveFlagStorage(region, storage);
        storage.setSaveOperation(currentStorage -> {
            saveFlagStorage(region, currentStorage);
        });
    }

    @SneakyThrows
    private void loadFlagStorage(File file, FlagStorage storage) {
        if (file == null || !file.exists()) return;
        JsonObject jsonObject = JsonParser.parseReader(new FileReader(file)).getAsJsonObject();
        for (Flag flag : Flag.getFlags()) {
            if (!jsonObject.has(flag.name())) continue;
            Flag.Value<?> value;
            try {
                value = flag.valueOfValue(jsonObject.get(flag.name()).getAsString());
            } catch (IllegalArgumentException e) {
                continue;
            }
            storage.getBackedMap().put(flag, value);
        }
    }

    private void saveFlagStorage(Region region, FlagStorage storage) {
        File file = getRegionDirectory(region);
        file = new File(file, FLAGS_FILE_NAME);
        saveFlagStorage(file, storage);
    }

    @SneakyThrows
    public void saveFlagStorage(File file, FlagStorage storage) {
        file.getParentFile().mkdirs();

        @Cleanup
        JsonWriter jsonWriter = new JsonWriter(new FileWriter(file));
        jsonWriter.setIndent("  ");
        jsonWriter.beginObject();
        for (Map.Entry<Flag<?>, Flag.Value<?>> entry : storage.getBackedMap().entrySet()) {
            if (entry.getKey() == Flag.CHANGED) continue;
            jsonWriter.name(entry.getKey().name());
            jsonWriter.value(entry.getValue().name());
        }
        jsonWriter.endObject();
    }

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd'_'HH:mm:ss");
    public RegionBackups getBackups(DynamicRegion region) {
        if (!region.getType().isCreateBackup()) {
            return RegionBackups.EMPTY;
        }

        File directory = new File(getRegionDirectory(region), BACKUP_DIRECTORY);
        directory.mkdirs();
        return new RegionBackups() {
            private List<Backup> backups = new ArrayList<>();

            {
                File[] files = directory.listFiles();
                if (files != null) {
                    for (File file : files) {
                        backups.add(new BackupImpl(file, region));
                    }
                }
                backups.sort(null);
            }

            @Override
            @SneakyThrows
            public Optional<Backup> create(BackupType backupType) {
                String name = LocalDateTime.now().format(formatter);
                File backupDirectory = new File(directory, name);
                backupDirectory.mkdirs();

                Point minPoint = region.getArea().getMinPoint(false);
                Point maxPoint = region.getArea().getMaxPoint(false);
                boolean success = FlatteningWrapper.impl.backup(minPoint, maxPoint, new File(backupDirectory, REGION_SCHEM_FILE_NAME));
                if (!success) {
                    deleteDir(backupDirectory);
                    return Optional.empty();
                }

                int count = 0;
                for (int i = backups.size() - 1; i >= 0; i--) {
                    Backup backup = backups.get(i);
                    if (backup.getType() == backupType) {
                        if (count >= backupType.maxBackups - 1) {
                            backup.delete();
                            backups.remove(i);
                            continue;
                        }
                        count++;
                    }
                }

                new File(backupDirectory, backupType.name()).createNewFile();
                saveFlagStorage(new File(backupDirectory, FLAGS_FILE_NAME), region.getFlags());
                Backup backup = new BackupImpl(backupDirectory, region);
                backups.add(backup);
                return Optional.of(backup);
            }

            @Override
            public @NonNull List<Backup> list() {
                return backups;
            }

            @Nullable
            @Override
            public Backup get(String name) {
                for (Backup backup : backups) {
                    if (backup.getName().equals(name)) {
                        return backup;
                    }
                }
                return null;
            }
        };
    }

    private static class BackupImpl extends RegionBackups.Backup {
        private final File file;
        private final DynamicRegion region;

        public BackupImpl(File file, DynamicRegion region) {
            super(getType(file), file.getName(), getFlags(file));
            this.file = file;
            this.region = region;
        }

        private static RegionBackups.BackupType getType(File file) {
            for (RegionBackups.BackupType type : RegionBackups.BackupType.values()) {
                if (new File(file, type.name()).exists()) return type;
            }
            throw new IllegalArgumentException("Unknown backup type");
        }

        private static FlagStorage getFlags(File file) {
            NormalFlagStorage storage = new NormalFlagStorage();
            loadFlagStorage(new File(file, FLAGS_FILE_NAME), storage);
            return storage;
        }

        @Override
        public boolean load() {
            if (!file.exists()) return false;
            EditSession editSession = new PasteBuilder(new PasteBuilder.FileProvider(new File(file, REGION_SCHEM_FILE_NAME)))
                    .pastePoint(region.getArea().getMinPoint(false))
                    .minPoint(region.getArea().getMinPoint(false))
                    .maxPoint(region.getArea().getMaxPoint(false))
                    .run();
            region.getHistory().remember(editSession);
            region.getFlags().setSaveOperation(null);
            region.setFlags((DefaultFlagStorage) getFlags());
            region.getFlags().setSaveOperation(storage -> saveFlagStorage(region, storage));
            return true;
        }

        @Override
        public void delete() {
            deleteDir(file);
        }

        @Override
        public long getCreationTime() {
            return file.lastModified();
        }
    }

    @SneakyThrows
    private static void deleteDir(File file) {
        Files.walkFileTree(file.toPath(), new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}
