package jace.ide;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

import jace.applesoft.ApplesoftHandler;
import jace.assembly.AssemblyHandler;
import javafx.application.Platform;
import javafx.concurrent.Worker;
import javafx.scene.control.TextInputDialog;
import javafx.scene.web.PromptData;
import javafx.scene.web.WebView;
import netscape.javascript.JSObject;

/**
 *
 * @author blurry
 */
@SuppressWarnings("all")
public class Program {

    public static String CODEMIRROR_EDITOR = "/codemirror/editor.html";

    public enum DocumentType {

        applesoft(new ApplesoftHandler(), "textfile", "*.bas"), assembly(new AssemblyHandler(), "textfile", "*.a", "*.s", "*.asm"), plain(new TextHandler(), "textfile", "*.txt"), hex(new TextHandler(), "textfile", "*.bin", "*.raw");

        static DocumentType fromFile(File file) {
            String name = file.getName();
            String ext = name.substring(name.lastIndexOf('.')).toLowerCase();
            ext = "*" + ext;
            for (DocumentType type : values()) {
                if (type.extensions.contains(ext)) {
                    return type;
                }
            }
            return DocumentType.plain;
        }
        String modeName;
        LanguageHandler languageHandler;
        List<String> extensions;

        DocumentType(LanguageHandler handler, String mode, String... defaultExtensions) {
            languageHandler = handler;
            modeName = mode;
            extensions = Arrays.asList(defaultExtensions);
        }
    }

    public enum Option {

        mode, value, theme, indentUnit, smartIndent, tabSize, indentWithTabs, electricChars, specialChars,
        specialCharPlaceHolder, rtlMoveVisually, keyMap, extraKeys, lineWrapping,
        lineNumbers, firstLineNumber, lineNumberFormatter, gutters, fixedGutter,
        scrollbarStyle, coverGutterNextToScrollbar, inputStyle, readOnly, showCursorWhenSelecting,
        lineWiseCopyCut, undoDepth, historyEventDelay, tabindex, autofocus
    }

    DocumentType fileType;
    boolean showLineNumbers;
    Map<Option, Object> options;
    File targetFile = null;
    String filename = "Untitled File";
    JSObject codeMirror;
    WebView editor;
    CompileResult lastResult;

    public Program(DocumentType type, Map<Option, Object> globalOptions) {
        fileType = type;
        options = new EnumMap<>(Option.class);
        options.putAll(globalOptions);
        options.put(Option.mode, fileType.modeName);
        options.put(Option.lineNumbers, true);
    }

    public String getName() {
        return filename;
    }

    public DocumentType getType() {
        return fileType;
    }

    public LanguageHandler getHandler() {
        return fileType.languageHandler;
    }

    public String getValue() {
        return (String) codeMirror.call("getValue");
    }

    public void setValue(String value) {
        codeMirror.call("setValue", value);
    }

    public Optional<File> getFile() {
        return Optional.ofNullable(targetFile);
    }

    public void initEditor(WebView editor, File sourceFile, boolean isBlank) {
        this.editor = editor;
        targetFile = sourceFile;
        if (targetFile != null) {
            filename = targetFile.getName();
        }

        editor.getEngine().getLoadWorker().stateProperty().addListener(
                (value, old, newState) -> {
                    if (newState == Worker.State.SUCCEEDED) {
                        JSObject document = (JSObject) editor.getEngine().executeScript("window");
                        document.setMember("java", this);
                        Platform.runLater(()->createEditor(isBlank));
                    }
                });

        editor.getEngine().setPromptHandler((PromptData prompt) -> {
            TextInputDialog dialog = new TextInputDialog(prompt.getDefaultValue());
            dialog.setTitle("Jace IDE");
            dialog.setHeaderText("Respond and press OK, or Cancel to abort");
            dialog.setContentText(prompt.getMessage());
            return dialog.showAndWait().orElse(null);
        });

        editor.getEngine().load(getClass().getResource(CODEMIRROR_EDITOR).toExternalForm());
    }

    public void createEditor(boolean isBlank) {
        String document = targetFile == null ? isBlank ? "" : getHandler().getNewDocumentContent() : getFileContents(targetFile);
        String optionString = buildOptions();
        editor.getEngine().executeScript("var codeMirror = CodeMirror(document.body, " + optionString + ");");
        codeMirror = (JSObject) editor.getEngine().executeScript("codeMirror");
        Platform.runLater(() -> setValue(document));
//        setValue(document);
    }

    public String getFileContents(File sourceFile) {
        if (sourceFile != null && sourceFile.exists() && sourceFile.isFile()) {
            BufferedReader reader = null;
            try {
                StringBuilder input = new StringBuilder();
                reader = new BufferedReader(new FileReader(sourceFile));
                reader.lines().forEach((line) -> {
                    input.append(line);
                    input.append("\n");
                });
                return input.toString();
            } catch (IOException ex) {
                Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } catch (IOException ex) {
                    Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return "";
    }

    public void save(File newTarget) {
        FileWriter writer = null;
        if (newTarget == null && targetFile == null) {
            return;
        }
        if (newTarget != null) {
            targetFile = newTarget;
        }
        filename = targetFile.getName();
        try {
            writer = new FileWriter(targetFile, false);
            writer.append(getValue());
            writer.close();
        } catch (IOException ex) {
            Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException ex) {
                Logger.getLogger(Program.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    public boolean isChanged() {
        return (Boolean) codeMirror.call("isClean");
    }

    private String buildOptions() {
        StringBuilder builder = new StringBuilder();
        builder.append("{");
        options.forEach((Option o, Object v) -> {
            if (builder.length() >= 2) {
                builder.append(",\n");
            }
            builder.append(o.name()).append(":");
            if (v instanceof String) {
                builder.append('"').append(v).append('"');
            } else {
                builder.append(v);
            }
        });
        builder.append("}");
        return builder.toString();
    }

    public void execute() throws Exception {
        lastResult = getHandler().compile(this);
        manageCompileResult(lastResult);
        if (lastResult.isSuccessful()) {
            getHandler().execute(lastResult);
        } else {
            StringBuilder error = new StringBuilder("Compilation failed:\n");
            lastResult.getErrors().forEach((line, message) -> error.append("Line %d: %s%n".formatted(line, message)));
            lastResult.getOtherMessages().forEach(message -> error.append(message).append("\n"));
            getHandler().clean(lastResult);
            throw new Exception(error.toString());
        }
    }

    public void test() {
        lastResult = getHandler().compile(this);
        manageCompileResult(lastResult);
        getHandler().clean(lastResult);
    }

    protected void manageCompileResult(CompileResult lastResult) {
        editor.getEngine().executeScript("clearHighlights()");
        lastResult.getWarnings().forEach((line, message)
                -> editor.getEngine().executeScript("highlightLine(" + line + ",false,\"" + escapeString(message) + "\");")
        );
        lastResult.getErrors().forEach((line, message)
                -> editor.getEngine().executeScript("highlightLine(" + line + ",true,\"" + escapeString(message) + "\");")
        );
    }

    private String escapeString(Object message) {
        return String.valueOf(message).replaceAll("\\\"", "&quot;");
    }

    public void log(String message) {
        System.out.println(message);
    }
}
