package org.limine.snapper.processes;

import org.limine.snapper.formats.json1.ImageDetail;
import org.limine.snapper.formats.json1.KernelEntry;
import org.limine.snapper.formats.json1.SnapperEntry;
import org.limine.snapper.formats.json1.SnapshotEntry;
import org.limine.snapper.formats.limine8.LimineKey;
import org.limine.snapper.objects.Config;
import org.limine.snapper.objects.TreeNode;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class LimineReader {

    private final String limineConfigPath;
    private final TreeNode rootNode;
    private final Config config;
    private final TreeNode targetOsNode;

    public LimineReader(Config config) {
        this(config, false);
    }

    public LimineReader(Config config, boolean askUserIfOSnotFound) {
        this.config = config;
        this.limineConfigPath = Utility.buildPath(config.espPath(), Config.LIMINE_CONFIG_FILE);

        TreeNode root = this.readLimineConfig();
        String osName = Config.TARGET_OS_NAME;
        TreeNode os = root.findOsNode(config.machineID(), osName, 1);

        // User gives input in the terminal manually.
        if (os == null && askUserIfOSnotFound) {
            do {
                String warn = "Target OS name '" + osName + "' was not found in " + config.espPath() + "/" + Config.LIMINE_CONFIG_FILE;
                Utility.warnMessage(warn);
                osName = Utility.cleanName(Utility.userInput("Enter OS name (must match config): "));
                //re-load changed limine.conf
                root = this.readLimineConfig();
                os = root.findOsNode(config.machineID(), osName, 1);
            } while (os == null);
        }

        if (os == null) {
            String errMessage = "Target OS name '" + osName + "' was not found in " + config.espPath() + "/" + Config.LIMINE_CONFIG_FILE + ", or TARGET_OS_NAME is missing in " + Config.DEFAULT_CONFIG_FILE_PATH;
            Utility.exitWithError(errMessage);
        } else {
            // Set a target OS name if the matching machine-ID is found in limine.conf
            Config.TARGET_OS_NAME = os.cleanName;
        }

        rootNode = root;
        targetOsNode = os;
    }

    public TreeNode getRootNode() {
        return rootNode;
    }

    public TreeNode getTargetOsNode() {
        return targetOsNode;
    }

    private TreeNode readLimineConfig() {
        TreeNode root = new TreeNode("root", 0, null);
        try {
            List<String> lines = Files.readAllLines(Paths.get(limineConfigPath));
            this.parseLinesToTree(lines, root, 0, lines.size());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return root;
    }

    public boolean isTargetOsNotFound() {
        return targetOsNode == null;
    }

    public boolean isSnapshotsNodeFound() {
        if (targetOsNode == null) {
            return false;
        }
        return isSnapshotsNodeInsideOS() || isSnapshotsNodeOutsideOS();
    }

    public boolean isSnapshotsNodeInsideOS() {
        return targetOsNode.searchNodeByName(LimineKey.SNAPSHOTS_INSIDE_OS_NODE.toString(), 1);
    }

    public boolean isSnapshotsNodeOutsideOS() {
        return rootNode.searchNodeByName(LimineKey.SNAPSHOTS_OUTSIDE_OS_NODE.toString(), 1);
    }

    /**
     * Create a current Limine entry for a new snapshot only. It requires the latest Snapper entry.
     */
    public SnapshotEntry createCurrentLimineEntry(SnapperEntry lastSnapperEntry) {
        // Create a current OS entry
        List<KernelEntry> kernelEntries = this.createKernelEntries(targetOsNode, lastSnapperEntry, config);
        return new SnapshotEntry(lastSnapperEntry, kernelEntries, null);
    }

    /**
     * Create a current Limine entry for the restore only. It does not need a Snapper entry.
     */
    public SnapshotEntry createCurrentLimineEntry() {
        // Create a current OS entry without Snapper
        SnapperEntry snapperEntry = new SnapperEntry(0, null, null, null);
        return this.createCurrentLimineEntry(snapperEntry);
    }

    /**
     * Create multiple kernel entries of your OS
     */
    private List<KernelEntry> createKernelEntries(TreeNode targetOsNode, SnapperEntry lastSnapperEntry, Config config) {

        List<KernelEntry> kernelEntries = new ArrayList<>();
        HashMap<String, ImageDetail> deduplicateImages = new HashMap<>();
        KernelReader kernelReader = new KernelReader(config, deduplicateImages);
        for (TreeNode kernelNode : targetOsNode.nodes) {
            // Ignore reading "//Snapshots" node.
            if (kernelNode.configLines.isEmpty() || LimineKey.SNAPSHOTS_INSIDE_OS_NODE.toString().equals(kernelNode.name.trim())) {
                continue;
            }
            String leadingSpaces = Utility.getLeadingSpaces(kernelNode.name);
            KernelEntry kernelEntry = kernelReader.createKernelEntry(kernelNode, leadingSpaces, lastSnapperEntry.snapshotID(), kernelNode.configLines);
            // If the kernel entry is not valid, then throw it away.
            if (kernelReader.isBootConfigValid()) {
                kernelEntries.add(kernelEntry);
            } else {
                kernelReader.resetBootConfigValid();
            }
        }
        return kernelEntries;
    }

    private int parseLinesToTree(List<String> lines, TreeNode currentNode, int startIndex, int endIndex) {

        int index = startIndex;
        while (index < endIndex) {
            String line = lines.get(index);
            String trimLine = line.trim();
            if (trimLine.startsWith(LimineKey.ENTRY_KEY.toString())) {
                int depth = calculateDepth(trimLine);
                TreeNode childNode = new TreeNode(line, depth, currentNode.name);
                currentNode.addNode(childNode);

                // Find the range for the child node
                int nextIndex = this.findNextNodeIndex(lines, index + 1, endIndex, depth);
                index = this.parseLinesToTree(lines, childNode, index + 1, nextIndex);

            } else {
                // Check if the line defines a macro
                MacroReplacer.parseMacro(line);
                currentNode.addConfigLine(line);
                index++;
            }
        }
        return index;
    }

    private int findNextNodeIndex(List<String> lines, int startIndex, int endIndex, int currentDepth) {
        for (int i = startIndex; i < endIndex; i++) {
            String line = lines.get(i).trim();
            if (line.startsWith(LimineKey.ENTRY_KEY.toString()) && calculateDepth(line) <= currentDepth) {
                return i;
            }
        }
        return endIndex;
    }

    private int calculateDepth(String line) {
        int depth = 0;
        while (depth < line.length() && line.startsWith(LimineKey.ENTRY_KEY.toString(), depth)) {
            depth++;
        }
        return depth;
    }
}
