package com.tangzhixiong.md2html;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.TimeUnit;

public class Utility {
    public static void clean(String inputPath) {
        clean(inputPath, inputPath);
    }
    public static void clean(String inputPath, String outputPath) {
        if (!Config.hasIconv) {
            return;
        }
        try {
            File temp = File.createTempFile("iconv-"+outputPath.hashCode()+"-", ".txt");
            System.out.println(temp.getCanonicalPath());
            ProcessBuilder pb = new ProcessBuilder().command(new String[]{
                    "iconv",
                    "-f", "utf-8",
                    "-t", "utf-8",
                    "-c", inputPath
            });
            pb.redirectOutput(temp);
            Process p = pb.start();
            p.waitFor(5, TimeUnit.SECONDS);
            Files.move(temp.toPath(), new File(outputPath).toPath(), StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception e) {
        }
    }

    public static void printInclusionLogs() {
        if (Bundle.inclusionLogs.size() > 0) {
            System.out.println("Inclusion statistics:");
            for (ArrayList<String> inclusionLog : Bundle.inclusionLogs) {
                System.out.println("    "+inclusionLog);
            }
        }
    }
    public static void copyResources(String srcDirPath, String dstDirPath) {
        final ArrayDeque<File> queue = new ArrayDeque<>();
        File srcDir = new File(srcDirPath);
        File dstDir = new File(dstDirPath);
        if (!srcDir.exists() || !srcDir.isDirectory()) {
            return;
        }
        if (!dstDir.exists()) { dstDir.mkdirs(); }
        if (dstDir.isFile()) { return; }
        try {
            srcDirPath = srcDir.getCanonicalPath();
            dstDirPath = dstDir.getCanonicalPath();
        } catch (Exception e) {
            return;
        }
        queue.add(srcDir);
        while (!queue.isEmpty()) {
            File pwd = queue.poll();
            final File[] entries;
            try {
                entries = pwd.listFiles();
            } catch (NullPointerException e) { continue; }
            for (final File entry: entries) {
                if (entry.isFile()) {
                    try {
                        final String srcFilePath = entry.getCanonicalPath();
                        final String dstFilePath = dstDirPath + srcFilePath.substring(srcDirPath.length());
                        mappingFile(srcFilePath, dstFilePath, !Config.silentMode);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else if (entry.isDirectory()) {
                    queue.add(entry);
                }
            }
        }
    }

    public static String resolveToRoot(String fullname, String dirname) {
        String frag = fullname.substring(dirname.length()+1);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < frag.length(); ++i) {
            if (String.valueOf(frag.charAt(i)).equals(File.separator)) {
                sb.append("../");
            }
        }
        return sb.toString();
    }

    // like `mkdir -p $(@D)` in makefile
    public static void mkdirHyphenPDollarAtD(File dest) {
        if (dest.isDirectory()) { return; }
        File atd = dest.getParentFile();
        if (!atd.exists()) {
            if (Config.verboseMode) {
                System.out.println("[/] making directory: "+atd.getAbsolutePath());
            }
            atd.mkdirs();
        }
    }

    public static void md2html(String outputPath) {
        clean(outputPath);
        int idx = outputPath.lastIndexOf(".");
        String suffix = outputPath.substring(idx+1);
        String outputPathHTML = outputPath.substring(0, idx) + ".html";
        String pathBasedOnRoot = outputPath.substring(Bundle.dstDir.length()+1);
        ArrayList<String> cmds = new ArrayList<>();
        {
            cmds.add( "pandoc" ); cmds.add( "-S" ); cmds.add( "-s" );
            cmds.add( "--ascii" );
            cmds.add( "--mathjax" );
            cmds.add( "--variable=rootdir:"+resolveToRoot(outputPath, Bundle.dstDir) );
            cmds.add( "--variable=md2htmldir:"+Bundle.resourceDirName );
            cmds.add( "--variable=thispath:"+pathBasedOnRoot );
            if (Bundle.mdExts.contains(suffix.toLowerCase())) {
                cmds.add( "--variable=ismarkdown:true" );
                cmds.add( "--template="+Bundle.htmltemplatePath );
                cmds.add( "--from=markdown+abbreviations+east_asian_line_breaks+emoji" );
                cmds.add( outputPath );
                cmds.add( Bundle.dotmd2htmlymlPath );
            } else {
                cmds.add( "--variable=ismarkdown:false" );
                cmds.add( "--template="+Bundle.htmltemplatePath );
                cmds.add( outputPath );
            }
            cmds.add( "--output="+outputPathHTML );
        }
        try {
            if (!Config.silentMode) {
                System.out.printf("[P] %s -> %s\n", outputPath, outputPathHTML);
            }
            if (Config.logCommands) {
                System.out.println(cmds);
            }
            Process p = new ProcessBuilder().inheritIO().command(cmds).start();
            try {
                p.waitFor(10, TimeUnit.SECONDS);
                // ArrayList<String> cmds2 = new ArrayList<>(); cmds2.add("cat"); cmds2.add(outputPath);
                // Process p2 = new ProcessBuilder().inheritIO().command(cmds2).start();
                // p2.waitFor(10, TimeUnit.SECONDS);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // copy README.html -> index.html
        if (Config.readmeAsMainIndex && outputPathHTML.equals(Config.dstDirPath+File.separator+"README.html")) {
            String readmeHTML = outputPathHTML;
            String indexHTML = readmeHTML.substring(0, readmeHTML.lastIndexOf("README.html")) + "index.html";
            mappingFile(readmeHTML, indexHTML);
        }
    }

    public static void mappingFile(String inputPath, String outputPath) {
        //  [+] 'D:\tzx\git\md2html\README.md' -> 'D:\tzx\git\md2html-publish\README.html'
        mappingFile(inputPath, outputPath, !Config.silentMode);
    }

    public static void mappingFile(String inputPath, String outputPath, boolean writeLog) {
        File inputFile = new File(inputPath);
        File outputFile = new File(outputPath);
        if (!inputFile.exists()) {
            System.out.println("[L] '"+inputFile.getAbsolutePath()+"' does not exists.");
            return;
        }
        if (!outputFile.exists() || inputFile.lastModified() > outputFile.lastModified()) {
            mkdirHyphenPDollarAtD(outputFile);
            try {
                // expand markdown file
                boolean isMdFile = isMarkdownFile(inputPath);
                if (isMdFile) {
                    // src/dir/file.md -> dst/dir/file.md
                    if (!Config.expandMarkdown) {
                        System.out.println(Config.expandMarkdown);
                        if (writeLog) {
                            System.out.printf("[C] %s -> %s\n", inputPath, outputPath);
                        }
                        Files.copy(inputFile.toPath(), outputFile.toPath()
                                , StandardCopyOption.REPLACE_EXISTING
                                , StandardCopyOption.COPY_ATTRIBUTES);
                    } else {
                        String filename = inputFile.getCanonicalPath();
                        List<String> lines = expandLines(filename);
                        if (writeLog) {
                            System.out.printf("[E] %s -> %s\n", inputPath, outputPath);
                        }
                        dump(lines, outputFile, isMdFile);
                    }
                    // dst/dir/file.md -> dst/dir/file.html
                    md2html(outputPath);
                } else {
                    if (writeLog) {
                        System.out.printf("[C] %s -> %s\n", inputPath, outputPath);
                    }
                    Files.copy(inputFile.toPath(), outputFile.toPath()
                                , StandardCopyOption.REPLACE_EXISTING
                                , StandardCopyOption.COPY_ATTRIBUTES);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            // no need to update
            if (writeLog) {
                System.out.printf("[ ] %s -> %s\n", inputPath, outputPath);
            }
        }
    }

    public static String getDirName(String path) {
        String dirname = ".";
        try {
            File dir = new File(path);
            if (!dir.isDirectory()) {
                dirname = dir.getParentFile().getCanonicalPath();
            } else {
                dirname = dir.getCanonicalPath();
            }
        }
        catch (IOException e) {
            e.printStackTrace();
            return "";
        }
        int pos = dirname.lastIndexOf(File.separator);
        if (0 <= pos && pos+1 < dirname.length()) {
            dirname = dirname.substring(pos+1);
        }
        return dirname;
    }

    public static void updateCodeFragmentIfNecessary(String inputPath, String label, String outputPath) {
        // input, inputFile, file suffix, highlighting code,
        File inputFile = new File(inputPath);
        File outputFile = new File(outputPath);
        if (!inputFile.exists() || !inputFile.isFile() || inputFile.length() > 2048 ) {
            if (!Config.silentMode) {
                System.out.printf("[X] %s -> %s\n", inputPath, outputPath);
            }
            return;
        }
        if (!outputFile.exists() || inputFile.lastModified() > outputFile.lastModified()) {
            Process proc = null;
            try {
                proc = Runtime.getRuntime().exec("pandoc -s -S --ascii");
            }
            catch (IOException e) {
                e.printStackTrace();
            }
            try (
                    FileInputStream fis = new FileInputStream(inputFile);
                    FileOutputStream fos = new FileOutputStream(outputFile);
                    PrintStream ps = new PrintStream(proc.getOutputStream());
            ) {
                if (!Config.silentMode) {
                    System.out.printf("[C] %s -> %s\n", inputPath, outputPath);
                }
                // pipe in
                byte[] buf = new byte[1024];
                int hasRead = 0;
                ps.printf("~~~~~~~~~~~~~~~~~~~~~ {.%s .numberLines}\n", label);
                while ((hasRead = fis.read(buf)) > 0) { ps.write(buf, 0, hasRead) ; }
                ps.print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
                ps.close();
                // pipe out
                BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                    fos.write(line.getBytes());
                    fos.write("\n".getBytes());
                }
                fos.close();
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static boolean canExpandLine(String line, InclusionParams params) {
        if (!line.endsWith("=")) { return false; }
        int idx = line.indexOf("@include <-=");
        if (idx < 0) { return false; }
        try {
            params.pad = line.substring(0, idx);
            params.path = line.substring(idx+"@include <-=".length(), line.length()-1);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    public static List<String> getLinesNaive(String inputPath) {
        try {
            return Files.readAllLines(new File(inputPath).toPath(), Charset.defaultCharset() ); // UTF-8
        } catch (IOException e) {
            try (
                    BufferedReader reader = new BufferedReader(new FileReader(new File(inputPath)));
            ) {
                String line;
                ArrayList<String> lines = new ArrayList<>();
                while((line = reader.readLine()) != null) {
                    lines.add(line);
                }
                return lines;
            } catch (IOException ioe) {
            }
        }
        return new ArrayList<String>();
    }

    public static String getExt(String path) {
        int idx = path.lastIndexOf(".");
        if (idx >=0) {
            return path.substring(idx+1);
        }
        return "";
    }

    public static boolean isMarkdownFile(String path) {
        return Bundle.markupExts.contains(getExt(path));
    }

    public static List<String> expandLines(String inputPath) {
        if (Config.expandMarkdown) {
            String ext = getExt(inputPath);
            return expandLines(inputPath, new InclusionParams(), Bundle.markupExts.contains(ext));
        } else {
            return getLinesNaive(inputPath);
        }
    }

    public static List<String> expandLines(String inputPath, InclusionParams params, boolean needExpansion) {
        if (!needExpansion) {
            return getLinesNaive(inputPath);
        }
        ArrayList<String> lines = new ArrayList<>();
        File file = new File(inputPath);
        if (!file.isFile() || !file.canRead()) { return lines; }
        String filename = null;
        String basename = null;
        try {
            filename = file.getCanonicalPath();
            basename = file.getParentFile().getCanonicalPath();
        } catch (Exception e) {
            e.printStackTrace();
            return lines;
        }
        if (params.parents.contains(filename)) {
            String errLog = params.getErrorLog(filename);
            System.err.printf("Loop detected, %s will not be included.\n", inputPath);
            System.err.println(errLog);
            lines.add(errLog); // print to markdown, so you can check these errors
            Bundle.inclusionLogs.add(params.getInclusionInfo());
            return lines;
        }
        try (
                Scanner scanner = new Scanner(file);
        ) {
            params.parents.add(filename);
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                InclusionParams paramsAnother = new InclusionParams();
                if (canExpandLine(line, paramsAnother)) {
                    String otherfilepath = basename+File.separator+paramsAnother.path;
                    List<String> moreLines = null;
                    try {
                        otherfilepath = new File(otherfilepath).getCanonicalPath();
                        if (filename.equals(otherfilepath)) {
                            moreLines = getLinesNaive(otherfilepath);
                        } else {
                            moreLines = expandLines(otherfilepath, params, isMarkdownFile(otherfilepath));
                        }
                        if (moreLines != null && moreLines.size() > 0) {
                            Bundle.inclusionLogs.add(params.getInclusionInfo());
                            ArrayList<String> log = new ArrayList<>();
                            for (String ml: moreLines) {
                                lines.add(params.pad + paramsAnother.pad + ml);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        continue;
                    }
                } else {
                    lines.add(line);
                }
            }
            scanner.close();
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        finally {
            params.parents.remove(inputPath);
        }
        return lines;
    }

    public static void dump(List<String> lines, File outputFile) {
        // typically not a markdown file
        dump(lines, outputFile, false);
    }

    public static void dump(List<String> lines, File outputFile, boolean isMarkdownFile) {
        mkdirHyphenPDollarAtD(outputFile);
        try (
            FileOutputStream fos = new FileOutputStream(outputFile);
        ) {
            if (isMarkdownFile && Config.foldMarkdown) {
                // twist a little bit for each line
                for (String line: lines) {
                    if (line.endsWith(" -<")) {
                        fos.write(line.substring(0, line.length() - 3).getBytes());
                        fos.write(" `@`{.fold}".getBytes());
                    } else if (line.endsWith(" +<")) {
                        fos.write(line.substring(0, line.length() - 3).getBytes());
                        fos.write(" `@`{.foldable}".getBytes());
                    } else {
                        fos.write(line.getBytes());
                    }
                    fos.write("\n".getBytes());
                }
            } else {
                // if not markdown file, or not fold markdown, just write out
                for (String line: lines) {
                    fos.write(line.getBytes());
                    fos.write("\n".getBytes());
                }
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void extractResourceFile(String resourcePath, String outputPath) {
        File outputFile = new File(outputPath);
        Utility.mkdirHyphenPDollarAtD(outputFile);
        try (
                InputStream is = Main.class.getResourceAsStream(resourcePath);
                FileOutputStream fos = new FileOutputStream(outputFile);
        ) {
            if (is == null) { return; }
            byte[] buf = new byte[1024];
            int hasRead = 0;
            while ((hasRead = is.read(buf)) > 0) {
                fos.write(buf, 0, hasRead) ;
            }
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static ArrayList<String> listing() {
        ArrayList<String> lines = new ArrayList<>();
        Vector<String> files = Bundle.getFiles();
        if (files.isEmpty()) {
            return lines;
        }
        for (int i = 1; i < files.size(); ++i) {
            lines.add(files.elementAt(i).substring(1+Bundle.srcDir.length()));
        }
        return lines;
    }
}

class InclusionParams {
    public final static String flag = "@include <-=";
    public String pad;
    public String path;
    public LinkedHashSet<String> parents;
    InclusionParams() {
        pad = "";
        path = "";
        parents = new LinkedHashSet<>();
    }
    public String getErrorLog(String filename) {
        StringBuilder sb = new StringBuilder();
        sb.append("\n```\nLOOP: [ ");
        for (String path: parents) {
            if (path.equals(filename)) {
                sb.append(path); sb.append(" <---\n        ");
            } else {
                sb.append(path); sb.append("\n        ");
            }
        }
        sb.append(filename); sb.append(" <---\n]\n```\n");
        return sb.toString();
    }
    public ArrayList<String> getInclusionInfo() {
        ArrayList<String> log = new ArrayList<>();
        for (String path: parents) {
            log.add(path);
        }
        return log;
    }
}