package com.narrowtux.fmm.model;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.sat4j.core.VecInt;
import org.sat4j.minisat.SolverFactory;
import org.sat4j.specs.ContradictionException;
import org.sat4j.specs.ISolver;
import org.sat4j.tools.ModelIterator;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

public class Modpack {
    private static final int RECURSION_LIMIT = 20;

    private StringProperty name = new SimpleStringProperty();
    private ObservableSet<ModReference> mods = FXCollections.observableSet(new LinkedHashSet<ModReference>());
    private ObjectProperty<Path> path = new SimpleObjectProperty<>();

    public Modpack(String name, Path path) {
        setName(name);
        setPath(path);

        nameProperty().addListener((obs, ov, nv) -> {
            // sanity check. no slashes allowed
            if (nv == null || nv.isEmpty() || nv.contains("/")) {
                setName(ov);
                return;
            }
            Path newPath = getPath().getParent().resolve(nv);
            try {
                Files.move(getPath(), newPath);
                setPath(newPath);
                for (ModReference mod : getMods()) {
                    String fileName = mod.getMod().getPath().getFileName().toString();
                    Path modNewPath = mod.getMod().getPath().getParent().getParent().resolve(nv).resolve(fileName);
                    mod.getMod().setPath(modNewPath);
                }
            } catch (IOException e) {
                e.printStackTrace();
                setName(ov);
            }
        });
    }

    public String getName() {
        return name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public StringProperty nameProperty() {
        return name;
    }

    public Path getPath() {
        return path.get();
    }

    public ObjectProperty<Path> pathProperty() {
        return path;
    }

    public void setPath(Path path) {
        this.path.set(path);
    }

    public ObservableSet<ModReference> getMods() {
        return mods;
    }

    @Override
    public String toString() {
        return "Modpack{" +
                "name=" + getName() +
                ", mods=" + mods +
                '}';
    }

    public void writeModList() {
        writeModList(false);
    }

    public void writeModList(boolean writeVersion) {
        writeModList(getPath().resolve("mod-list.json"), writeVersion, getMods().toArray(new ModReference[0]));
    }

    public static void writeModList(Path file, boolean writeVersion, ModReference ... mods) {
        JsonObject root = new JsonObject();
        JsonArray modList = new JsonArray();
        JsonObject baseMod = new JsonObject();
        baseMod.addProperty("name", "base");
        baseMod.addProperty("enabled", true);
        modList.add(baseMod);
        for (ModReference mod : mods) {
            JsonObject modInfo = new JsonObject();
            modInfo.addProperty("name", mod.getMod().getName());
            modInfo.addProperty("enabled", mod.getEnabled());
            if (writeVersion) {
                modInfo.addProperty("version", mod.getMod().getVersion().toString());
            }
            modList.add(modInfo);
        }
        root.add("mods", modList);
        try {
            Gson gson = new Gson();
            String json = gson.toJson(root);
            FileWriter writer = new FileWriter(file.toFile());
            writer.write(json);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public List<Set<Mod>> resolveDependencies() {
        Set<Mod> confirmedMods = getMods().stream().map(ModReference::getMod).collect(Collectors.toSet());
        try {
            return resolveDependencies(0, confirmedMods);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public List<Set<Mod>> resolveDependencies(int recursionInterval, Set<Mod> confirmedMods) throws Exception {
        if (recursionInterval > RECURSION_LIMIT) {
            throw new Exception("Recursion limit reached");
        }

        recursionInterval ++;
        List<Set<Mod>> solutions = new LinkedList<>();

        List<ModDependency> deps = new LinkedList<>();
        confirmedMods.forEach(mod -> deps.addAll(mod.getDependencies().stream().filter(dep -> !dep.getOptional()).collect(Collectors.toList())));
        for (Mod mod : confirmedMods) {
            List<ModDependency> satisfied = new LinkedList<>();
            for (ModDependency dep : deps) {
                if (dep.getDependencyName().equals(mod.getName())) {
                    if (dep.getMatchedVersion() == null || dep.getMatchedVersion().matches(mod.getVersion())) {
                        satisfied.add(dep);
                    }
                }
            }
            deps.removeAll(satisfied);
        }
        if (deps.isEmpty()) {
            solutions.add(confirmedMods);
            return solutions;
        }
        ISolver solver = SolverFactory.newDefault();
        solver.setKeepSolverHot(true);

        Set<String> names = deps.stream().map(ModDependency::getDependencyName).collect(Collectors.toSet());
        List<Mod> matchedMods = new LinkedList<>();
        Datastore store = Datastore.getInstance();

        final boolean[] modNotFound = {false};
        deps.stream().forEach(dep -> {
            final int[] found = {0};
            VecInt clause = new VecInt();
            store.getMods().values().stream()
                    .filter(mod -> dep.getMatchedVersion() == null || dep.getMatchedVersion().matches(mod.getVersion()))
                    .filter(mod -> dep.getDependencyName().equals(mod.getName()))
                    .forEach(mod -> {
                        found[0]++;
                        if (!matchedMods.contains(mod)) {
                            matchedMods.add(mod);
                        }
                        int literal = matchedMods.indexOf(mod) + 1;
                        if (!clause.contains(literal)) {
                            clause.push(literal);
                        }
                    });
            if (found[0] == 0) {
                modNotFound[0] = true;
                System.out.println(getName() + ": couldn't find mod for " + dep);
            }
            try {
                solver.addClause(clause);
            } catch (ContradictionException e) {
                e.printStackTrace();
            }
        });
        if (modNotFound[0]) {
            System.out.println(getName() + ": one or more mods couldn't be found");
            return null;
        }

        for (String name : names) {
            VecInt vector = new VecInt();
            matchedMods.stream().filter(mod -> mod.getName().equals(name)).forEach(mod -> {
                int literal = matchedMods.indexOf(mod) + 1;
                if (!vector.contains(literal)) {
                    vector.push(literal);
                }
            });
            try {
                solver.addExactly(vector, 1);
            } catch (ContradictionException e) {
                System.out.println(vector);
                e.printStackTrace();
                return null;
            }
        }

        ModelIterator iter = new ModelIterator(solver);
        for (int interval = 0; interval < RECURSION_LIMIT; interval++) {
            if (iter.isSatisfiable()) {
                Set<Mod> solution = new HashSet<>(confirmedMods);
                for (int v : iter.model()) {
                    if (v > 0) {
                        Mod mod = matchedMods.get(v - 1);
                        solution.add(mod);
                    }
                }
                List<Set<Mod>> result = resolveDependencies(recursionInterval, solution);
                if (result.size() != 0) {
                    solutions.addAll(result);
                }
            } else {
                return solutions;
            }
        }
        return solutions;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Modpack modpack = (Modpack) o;

        if (!name.equals(modpack.name)) return false;
        return path.equals(modpack.path);

    }

    @Override
    public int hashCode() {
        int result = name.hashCode();
        result = 31 * result + path.hashCode();
        return result;
    }
}