package mara.mybox.controller;

import com.vladsch.flexmark.html2md.converter.*;
import com.vladsch.flexmark.util.data.MutableDataSet;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.StringWriter;
import java.net.URI;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.ToolBar;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.input.DragEvent;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.web.HTMLEditor;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebErrorEvent;
import javafx.scene.web.WebEvent;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Callback;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import mara.mybox.data.BrowserHistory;
import mara.mybox.data.VisitHistory;
import mara.mybox.db.TableBrowserBypassSSL;
import mara.mybox.db.TableBrowserHistory;
import mara.mybox.fxml.FxmlControl;
import static mara.mybox.fxml.FxmlControl.badStyle;
import mara.mybox.fxml.FxmlImageManufacture;
import mara.mybox.fxml.FxmlStage;
import mara.mybox.fxml.RecentVisitMenu;
import mara.mybox.image.ImageManufacture;
import mara.mybox.image.file.ImageFileWriters;
import mara.mybox.tools.FileTools;
import mara.mybox.tools.HtmlTools;
import mara.mybox.tools.NetworkTools;
import mara.mybox.tools.PdfTools;
import mara.mybox.value.AppVariables;
import static mara.mybox.value.AppVariables.MyboxDataPath;
import static mara.mybox.value.AppVariables.logger;
import static mara.mybox.value.AppVariables.message;
import mara.mybox.value.CommonFxValues;
import mara.mybox.value.CommonValues;

/**
 * @Author Mara
 * @CreateDate 2018-7-31
 * @Description
 * @License Apache License Version 2.0
 */
public class HtmlEditorController extends BaseController {

    private final String HtmlImagePathKey, HtmlSnapDelayKey, HtmlLastUrlsKey, HtmlPdfPathKey;
    private WebEngine webEngine;
    private int delay, fontSize, orginalStageHeight, orginalStageY, orginalStageWidth;
    protected int lastHtmlLen, lastCodesLen;
    private boolean isOneImage;
    private URI uri;
    private List<File> snaps;
    private LoadingController loadingController;
    private float zoomScale;
    protected boolean loadSynchronously, isFrameSet, notChangedAfterLoad;
    protected SimpleBooleanProperty fileChanged;
    protected int cols, rows;
    protected int lastTextLen;
    protected SnapshotParameters snapParameters;
    protected int snapFileWidth, snapFileHeight, snapsTotal,
            snapImageWidth, snapImageHeight, snapTotalHeight, snapHeight, snapStep, dpi;
    protected double snapScale;

    @FXML
    private Button snapshotButton;
    @FXML
    private HTMLEditor htmlEditor;
    @FXML
    private WebView webView;
    @FXML
    private TextArea codesArea, markdownArea;
    @FXML
    private TabPane tabPane;
    @FXML
    private Tab editorTab, codesTab, browserTab, markdownTab;
    @FXML
    private ComboBox<String> urlBox, delayBox, dpiSelector;
    @FXML
    private ScrollPane scrollPane;
    @FXML
    private ToolBar snapBar;
    @FXML
    private ToggleGroup snapGroup;
    @FXML
    private CheckBox windowSizeCheck;
    @FXML
    protected TextField bottomText;

    public HtmlEditorController() {
        baseTitle = AppVariables.message("HtmlEditor");

        SourceFileType = VisitHistory.FileType.Html;
        SourcePathType = VisitHistory.FileType.Html;
        TargetPathType = VisitHistory.FileType.Html;
        TargetFileType = VisitHistory.FileType.Html;
        AddFileType = VisitHistory.FileType.Html;
        AddPathType = VisitHistory.FileType.Html;

        sourcePathKey = "HtmlFilePath";
        HtmlImagePathKey = "HtmlImagePath";
        HtmlSnapDelayKey = "HtmlSnapDelay";
        HtmlLastUrlsKey = "HtmllastUrl";
        HtmlPdfPathKey = "PdfFilePath";

        sourceExtensionFilter = CommonFxValues.HtmlExtensionFilter;
        targetExtensionFilter = sourceExtensionFilter;

    }

    @Override
    public void initializeNext() {
        try {
            lastCodesLen = lastHtmlLen = 0;
            isSettingValues = false;
            fontSize = 14;
            zoomScale = 1.0f;
            isFrameSet = false;

            initHtmlEdtior();

            initCodeEdtior();

            initBroswer();

            tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
                @Override
                public void changed(ObservableValue<? extends Tab> observable,
                        Tab oldValue, Tab newValue) {
                    isSettingValues = true;
                    try {
                        String contents = "";
                        Object object;

                        if (editorTab.equals(oldValue)) {
                            if (isFrameSet) {
                                object = webEngine.executeScript("document.documentElement.outerHTML");
                            } else {
                                object = htmlEditor.getHtmlText();
                            }
                            if (object != null) {
                                contents = (String) object;
                                isFrameSet = contents.toUpperCase().contains("</FRAMESET>");
                                codesArea.setText(contents);
                                if (!isFrameSet) {
                                    webEngine.loadContent(contents);
                                }
                                html2markdown(contents);
                            }

                        } else if (codesTab.equals(oldValue)) {
                            object = codesArea.getText();
                            if (object != null) {
                                contents = (String) object;
                                isFrameSet = contents.toUpperCase().contains("</FRAMESET>");
                                if (isFrameSet) {
                                    htmlEditor.setHtmlText("<p>" + AppVariables.message("NotSupportFrameSet") + "</p>");
                                } else {
                                    htmlEditor.setHtmlText(contents);
                                    webEngine.loadContent(contents);
                                }
                                html2markdown(contents);
                            }

                        } else if (browserTab.equals(oldValue)) {
//                            object = webEngine.executeScript("document.documentElement.outerHTML");
//                            if (object != null) {
//                                contents = (String) object;
//                                isFrameSet = contents.toUpperCase().contains("</FRAMESET>");
//                                codesArea.setText(contents);
//                                if (isFrameSet) {
//                                    htmlEditor.setHtmlText("<p>" + AppVariables.message("NotSupportFrameSet") + "</p>");
//                                } else {
//                                    htmlEditor.setHtmlText(contents);
//                                }
//                                html2markdown(contents);
//                            }

                        } else if (markdownTab.equals(oldValue)) {

                        }

                    } catch (Exception e) {
                        logger.debug(e.toString());
                    }

                    isSettingValues = false;
                }
            });

            fileChanged = new SimpleBooleanProperty(false);
            fileChanged.addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> ov,
                        Boolean old_val, Boolean new_val) {
                    if (isSettingValues) {
                        return;
                    }
                    String t = getBaseTitle();
                    if (sourceFile != null) {
                        t += "  " + sourceFile.getAbsolutePath();
                    }
                    if (fileChanged.getValue()) {
                        getMyStage().setTitle(t + "*");
                    } else {
                        getMyStage().setTitle(t);
                    }
                }
            });

        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    protected void initHtmlEdtior() {
        try {

            // As my testing, only DragEvent.DRAG_EXITED, KeyEvent.KEY_TYPED, KeyEvent.KEY_RELEASED working for HtmlEdior
            htmlEditor.addEventHandler(DragEvent.DRAG_EXITED, new EventHandler<InputEvent>() { // work
                @Override
                public void handle(InputEvent event) {
                    checkHtmlEditorChanged();
                }
            });
            htmlEditor.setOnKeyReleased(new EventHandler<KeyEvent>() {
                @Override
                public void handle(KeyEvent event) {
//                    logger.debug("setOnKeyReleased");
                    checkHtmlEditorChanged();
                }
            });

        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    private void checkHtmlEditorChanged() {
        try {
            String c = htmlEditor.getHtmlText();
            int len = 0;
            if (c != null && !c.isEmpty()) {
                len = htmlEditor.getHtmlText().length();
//        logger.debug(isSettingValues + "  " + len + " " + lastHtmlLen);
                if (!isSettingValues && len != lastHtmlLen) {
                    fileChanged.set(true);
                }
            }
            lastHtmlLen = len;
            bottomText.setText(AppVariables.message("Total") + ": " + len);
        } catch (Exception e) {
            logger.error(e.toString());
        }
    }

    protected void initCodeEdtior() {
        try {

            codesArea.textProperty().addListener(new ChangeListener<String>() {
                @Override
                public void changed(ObservableValue ov, String oldValue,
                        String newValue) {
                    if (!isSettingValues) {
                        fileChanged.set(true);
                    }
                    int len = codesArea.getText().length();
                    lastCodesLen = len;
                    bottomText.setText(AppVariables.message("Total") + ": " + len);
                }
            });

        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    protected void initBroswer() {
        try {
            if (AppVariables.getUserConfigBoolean("SSLBypassAll", false)) {
                NetworkTools.trustAll();
            } else {
                NetworkTools.myBoxSSL();
            }

            urlBox.valueProperty().addListener(new ChangeListener<String>() {
                @Override
                public void changed(ObservableValue<? extends String> ov,
                        String oldValue, String newv) {
                    checkAddress();
                }
            });
            List<String> urls = TableBrowserHistory.recentBrowse();
            if (!urls.isEmpty()) {
                isSettingValues = true;
                urlBox.getItems().addAll(urls);
                isSettingValues = false;
            }

            delayBox.getItems().addAll(Arrays.asList("2", "3", "5", "1", "10"));
            delayBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
                @Override
                public void changed(ObservableValue<? extends String> ov,
                        String oldValue, String newValue) {
                    try {
                        delay = Integer.valueOf(newValue);
                        if (delay > 0) {
                            delay = delay * 1000;
                            AppVariables.setUserConfigValue(HtmlSnapDelayKey, newValue);
                            FxmlControl.setEditorNormal(delayBox);
                        } else {
                            delay = 2000;
                            FxmlControl.setEditorBadStyle(delayBox);
                        }

                    } catch (Exception e) {
                        delay = 2000;
                        FxmlControl.setEditorBadStyle(delayBox);
                    }
                }
            });
            delayBox.getSelectionModel().select(AppVariables.getUserConfigValue(HtmlSnapDelayKey, "2"));

            snapGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
                @Override
                public void changed(ObservableValue<? extends Toggle> ov,
                        Toggle old_toggle, Toggle new_toggle) {
                    checkOneImage();
                }
            });
            checkOneImage();

            List<String> dpiValues = new ArrayList();
            dpiValues.addAll(Arrays.asList("96", "120", "160", "300"));
            String sValue = Toolkit.getDefaultToolkit().getScreenResolution() + "";
            if (dpiValues.contains(sValue)) {
                dpiValues.remove(sValue);
            }
            dpiValues.add(0, sValue);
            sValue = (int) Screen.getPrimary().getDpi() + "";
            if (dpiValues.contains(sValue)) {
                dpiValues.remove(sValue);
            }
            dpiValues.add(sValue);
            dpiSelector.getItems().addAll(dpiValues);
            dpiSelector.getSelectionModel().selectedItemProperty().addListener(
                    (ObservableValue<? extends String> ov, String oldValue, String newValue) -> {
                        try {
                            dpi = Integer.parseInt(newValue);
                            AppVariables.setUserConfigValue("HtmlSnapDPI", dpi + "");
                        } catch (Exception e) {
                            dpi = 96;
                        }
                    });
            dpiSelector.getSelectionModel().select(AppVariables.getUserConfigValue("HtmlSnapDPI", "96"));

            webEngine = webView.getEngine();
            webEngine.setJavaScriptEnabled(true);

            webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
                @Override
                public void changed(ObservableValue ov, State oldState,
                        State newState) {
                    try {
//                        logger.debug(newState.name() + " " + webEngine.getLocation());
                        snapBar.setDisable(true);
                        switch (newState) {
                            case READY:
                                bottomText.setText("");
                                break;
                            case SCHEDULED:
                                bottomText.setText("");
                                break;
                            case RUNNING:
                                bottomText.setText(AppVariables.message("Loading..."));
                                break;
                            case SUCCEEDED:
                                afterPageLoaded();
                                break;
                            case CANCELLED:
                                bottomText.setText(message("Canceled"));
                                if (loadingController != null) {
                                    loadingController.closeStage();
                                    loadingController = null;
                                }
                                break;
                            case FAILED:
                                bottomText.setText(message("Failed"));
                                if (loadingController != null) {
                                    loadingController.closeStage();
                                    loadingController = null;
                                }
                                break;
                            default:
                                bottomText.setText("");
                                break;
                        }

                    } catch (Exception e) {
                        logger.debug(e.toString());
                        if (loadingController != null) {
                            loadingController.closeStage();
                            loadingController = null;
                        }
                    }

                }

            });

            webEngine.setOnAlert(new EventHandler<WebEvent<String>>() {

                @Override
                public void handle(WebEvent<String> event) {
                    logger.debug("setOnAlert " + event.getData());
                }
            });

            webEngine.setOnError(new EventHandler<WebErrorEvent>() {

                @Override
                public void handle(WebErrorEvent event) {
//                    popError(event.getMessage());
//                    logger.debug(event.getMessage());
                }
            });

            webEngine.setConfirmHandler(new Callback<String, Boolean>() {

                @Override
                public Boolean call(String param) {
                    logger.debug("setConfirmHandler " + param);
                    return null;
                }
            });

            webEngine.getLoadWorker().exceptionProperty().addListener(new ChangeListener<Throwable>() {
                @Override
                public void changed(ObservableValue<? extends Throwable> ov,
                        Throwable ot, Throwable nt) {
                    if (nt == null) {
                        return;
                    }
                    try {
                        bottomText.setText(nt.getMessage());
                        if (nt.getMessage().contains("SSL handshake failed")) {
                            String host = new URI(webEngine.getLocation()).getHost();
                            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
                            alert.setTitle(getBaseTitle());
                            alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
                            Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
                            stage.setAlwaysOnTop(true);
                            stage.toFront();
                            if (NetworkTools.isHostCertificateInstalled(host)) {
                                alert.setContentText(host + "\n" + message("SSLInstalledButFailed"));
                                ButtonType buttonBypass = new ButtonType(AppVariables.message("SSLVerificationByPass"));
                                ButtonType buttonCancel = new ButtonType(AppVariables.message("ISee"));
                                alert.getButtonTypes().setAll(buttonBypass, buttonCancel);
                                Optional<ButtonType> result = alert.showAndWait();
                                if (result.get() != buttonBypass) {
                                    return;
                                }
                                TableBrowserBypassSSL.write(host);
                                load();

                            } else {
                                alert.setContentText(host + "\n" + message("SSLCertificatesAskInstall"));
                                ButtonType buttonSure = new ButtonType(AppVariables.message("Sure"));
                                ButtonType buttonCancel = new ButtonType(AppVariables.message("Cancel"));
                                alert.getButtonTypes().setAll(buttonSure, buttonCancel);
                                Optional<ButtonType> result = alert.showAndWait();
                                if (result.get() == null || result.get() != buttonSure) {
                                    return;
                                }
                                String msg = NetworkTools.installCertificateByHost(host, host);
                                if (msg == null) {
                                    msg = host + "\n" + message("SSLCertificateInstalled");
                                }
                                alert.setContentText(msg);
                                ButtonType buttonISee = new ButtonType(AppVariables.message("ISee"));
                                alert.getButtonTypes().setAll(buttonISee);
                                alert.showAndWait();
                            }

                        }
                    } catch (Exception e) {
                        popError(e.toString());
                        logger.debug(e.toString());
                    }
                }
            });

        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    protected void checkAddress() {
        try {
            String address = urlBox.getValue();
            if (isSettingValues || address == null || address.trim().isEmpty()) {
                return;
            }
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (address.startsWith("file:")) {
                            uri = new URI(address);
                        } else if (!address.startsWith("http")) {
                            uri = new URI("http://" + address);
                        } else {
                            uri = new URI(address);
                        }
                        if (uri != null) {
                            load();
                        } else {
                            urlBox.getEditor().setStyle(badStyle);
                        }
                    } catch (Exception e) {
                        logger.error(e.toString());
                        urlBox.getEditor().setStyle(badStyle);
                    }
                }
            });

        } catch (Exception e) {
            logger.error(e.toString());
        }
    }

    protected void afterPageLoaded() {
        try {
            if (loadingController != null) {
                loadingController.closeStage();
                loadingController = null;
            }
            if (uri == null) {
                return;
            }
            bottomText.setText(AppVariables.message("Loaded"));
            snapBar.setDisable(false);

            if (loadSynchronously) {
                try {
                    String contents;
                    Object c = webEngine.executeScript("document.documentElement.outerHTML");
                    if (c == null) {
                        contents = "";
                    } else {
                        contents = (String) c;
                    }
                    isFrameSet = contents.toUpperCase().contains("</FRAMESET>");
                    if (isFrameSet) {
//                                        popError(AppVariables.getMessage("NotSupportFrameSet"));
                        htmlEditor.setHtmlText("<p>" + AppVariables.message("NotSupportFrameSet") + "</p>");
                    } else {
                        htmlEditor.setHtmlText(contents);
                    }
                    codesArea.setText(contents);
                    html2markdown(contents);
                } catch (Exception e) {
                    logger.debug(e.toString());
                }
                loadSynchronously = false;
            }

            if (notChangedAfterLoad) {
                fileChanged.set(false);
                getMyStage().setTitle(getBaseTitle());
                notChangedAfterLoad = false;
            }

            try {
                Object c = webEngine.executeScript("getComputedStyle(document.body).fontSize");
                if (c != null) {
                    String s = (String) c;
                    fontSize = Integer.valueOf(s.substring(0, s.length() - 2));
                } else {
                    fontSize = 14;
                }
            } catch (Exception e) {
                fontSize = 14;
            }

            String address = uri.toString().toLowerCase();
            BrowserHistory his = new BrowserHistory();
            his.setAddress(address);
            his.setVisitTime(new Date().getTime());
            his.setTitle(webEngine.getTitle());
            File path = new File(MyboxDataPath + File.separator + "icons");
            if (!path.exists()) {
                path.mkdirs();
            }

            if (uri.getScheme().startsWith("http")) {
                File file = new File(MyboxDataPath + File.separator + "icons" + File.separator + uri.getHost() + ".png");
                if (!file.exists()) {
                    HtmlTools.readIcon(address, file);
                }
                if (file.exists()) {
                    his.setIcon(file.getAbsolutePath());
                } else {
                    his.setIcon("");
                }
            } else {
                his.setIcon("");
            }
            TableBrowserHistory.write(his);

            isSettingValues = true;
            urlBox.getItems().clear();
            List<String> urls = TableBrowserHistory.recentBrowse();
            if (!urls.isEmpty()) {
                urlBox.getItems().addAll(urls);
                urlBox.getEditor().setText(address);
            }
            isSettingValues = false;
        } catch (Exception e) {
            logger.error(e.toString());
        }
    }

    protected void load() {
        try {
            isFrameSet = false;
            loadSynchronously = true;
            notChangedAfterLoad = true;
            webEngine.getLoadWorker().cancel();
            if (uri == null) {
                return;
            }
            bottomText.setText(AppVariables.message("Loading..."));
            webEngine.load(uri.toString());

        } catch (Exception e) {
            logger.error(e.toString());
        }
    }

    private void checkOneImage() {
        RadioButton selected = (RadioButton) snapGroup.getSelectedToggle();
        if (AppVariables.message("OneImage").equals(selected.getText())) {
            isOneImage = true;
            windowSizeCheck.setDisable(true);
        } else {
            isOneImage = false;
            windowSizeCheck.setDisable(false);
        }
    }

    @Override
    public void sourceFileChanged(final File file) {
        sourceFile = file;
        loadLink(file);

    }

    public void loadLink(String link) {
        urlBox.setValue(link);
    }

    public void loadLink(File file) {
        sourceFile = file;
        loadLink(file.toURI().toString());
    }

    private String getBrowserContents() {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
            transformer.setOutputProperty(OutputKeys.METHOD, "xml");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

            StringWriter stringWriter = new StringWriter();
            transformer.transform(new DOMSource(webEngine.getDocument()), new StreamResult(stringWriter));
            String contents = stringWriter.getBuffer().toString();
            return contents;
        } catch (Exception e) {
            logger.debug(e.toString());
            return null;
        }
    }

    @FXML
    @Override
    public void saveAction() {
        try {
            isSettingValues = true;
            if (sourceFile == null) {
                final File file = chooseSaveFile(AppVariables.getUserConfigPath(targetPathKey),
                        null, targetExtensionFilter, true);
                if (file == null) {
                    return;
                }
                recordFileWritten(file);
                sourceFile = file;
            }
            String contents;
            if (tabPane.getSelectionModel().getSelectedItem().equals(codesTab)) {
                contents = codesArea.getText();
            } else {
                contents = htmlEditor.getHtmlText();
            }
            try ( BufferedWriter out = new BufferedWriter(new FileWriter(sourceFile, Charset.forName("utf-8"), false))) {
                out.write(contents);
                out.flush();
            }
            fileChanged.set(false);
            getMyStage().setTitle(getBaseTitle() + "  " + sourceFile.getAbsolutePath());
            isSettingValues = false;

        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    @FXML
    @Override
    public void saveAsAction() {
        try {
            isSettingValues = true;
            String name = null;
            if (sourceFile != null) {
                name = FileTools.getFilePrefix(sourceFile.getName());
            }
            final File file = chooseSaveFile(AppVariables.getUserConfigPath(targetPathKey),
                    name, targetExtensionFilter, true);
            if (file == null) {
                return;
            }
            recordFileWritten(file);
            sourceFile = file;
            String contents;
            if (AppVariables.message("Editor").equals(tabPane.getSelectionModel().getSelectedItem().getText())) {
                contents = htmlEditor.getHtmlText();
            } else {
                contents = codesArea.getText();
            }
            try ( BufferedWriter out = new BufferedWriter(new FileWriter(sourceFile, Charset.forName("utf-8"), false))) {
                out.write(contents);
                out.flush();
            }
            fileChanged.set(false);
            getMyStage().setTitle(getBaseTitle() + "  " + sourceFile.getAbsolutePath());
            isSettingValues = false;
        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    @FXML
    @Override
    public void createAction() {
        try {
            isSettingValues = true;
            sourceFile = null;
            htmlEditor.setHtmlText("");
            codesArea.setText("");
            lastCodesLen = lastHtmlLen = 0;
            fileChanged.set(false);
            isFrameSet = false;
            getMyStage().setTitle(getBaseTitle());
            isSettingValues = false;
        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    @FXML
    private void snapshot(ActionEvent event) {
        File file;
        if (isOneImage) {
            file = chooseSaveFile(AppVariables.getUserConfigPath(HtmlImagePathKey),
                    null, CommonFxValues.ImageExtensionFilter, true);
        } else {
            file = chooseSaveFile(AppVariables.getUserConfigPath(HtmlPdfPathKey),
                    null, CommonFxValues.PdfExtensionFilter, true);
        }
        if (file == null) {
            return;
        }
        recordFileWritten(file);
        if (isOneImage) {
            AppVariables.setUserConfigValue(HtmlImagePathKey, file.getParent());
        } else {
            AppVariables.setUserConfigValue(HtmlPdfPathKey, file.getParent());
        }
        targetFile = file;

        loadWholePage();

    }

    private void loadWholePage() {
        try {
            orginalStageHeight = (int) getMyStage().getHeight();
            orginalStageY = (int) myStage.getY();
            orginalStageWidth = (int) getMyStage().getWidth();
            Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds();
            myStage.setY(0);
            myStage.setHeight(primaryScreenBounds.getHeight());
//            myStage.setWidth(900);
//            webEngine.executeScript("document.body.style.fontSize = '15px' ;");

            snapshotButton.setDisable(true);
            final int maxDelay = delay * 30;
            final long startTime = new Date().getTime();

            if (loadingController != null) {
                loadingController.closeStage();
                loadingController = null;
            }
            loadingController = FxmlStage.openLoadingStage(myStage, Modality.WINDOW_MODAL, null);

            if (timer != null) {
                timer.cancel();
            }
            timer = new Timer();
            timer.schedule(new TimerTask() {
                int lastHeight = 0, newHeight = -1;

                @Override
                public void run() {
                    boolean quit = false;
                    if (new Date().getTime() - startTime > maxDelay) {
//                        logger.debug(" TimeOver:" + newHeight);
                        quit = true;
                    }
                    if (newHeight == lastHeight) {
//                        logger.debug(" Complete:" + newHeight);
                        quit = true;
                    }
                    if (quit) {
//                        Platform.runLater(new Runnable() {
//                            @Override
//                            public void run() {
//                                if (snapingStage != null) {
//                                    snapingStage.close();
//                                }
//                            }
//                        });
                        this.cancel();
                        return;
                    }

                    lastHeight = newHeight;
                    Platform.runLater(() -> {
                        try {
                            newHeight = (Integer) webEngine.executeScript("document.body.scrollHeight");
                            loadingController.setInfo(AppVariables.message("CurrentPageHeight") + ": " + newHeight);
                            if (newHeight == lastHeight) {
                                loadingController.setInfo(AppVariables.message("ExpandingPage"));
                                startSnap();
                            } else {
                                webEngine.executeScript("window.scrollTo(0," + newHeight + ");");
                            }

                        } catch (Exception e) {
                            logger.error(e.toString());
                            if (loadingController != null) {
                                loadingController.closeStage();
                                loadingController = null;
                            }
                        }
                    });

                }
            }, 0, delay);

        } catch (Exception e) {
            logger.error(e.toString());
        }

    }

    private void startSnap() {
        try {
            webEngine.executeScript("window.scrollTo(0,0 );");
            snapTotalHeight = (Integer) webEngine.executeScript("document.body.scrollHeight");
            snapStep = (Integer) webEngine.executeScript("document.documentElement.clientHeight < document.body.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight");
            snapHeight = 0;
            bottomText.setText(AppVariables.message("SnapingImage..."));

            // http://news.kynosarges.org/2017/02/01/javafx-snapshot-scaling/
            final Bounds bounds = webView.getLayoutBounds();
            snapScale = dpi / Screen.getPrimary().getDpi();
            snapScale = snapScale > 1 ? snapScale : 1;
            snapImageWidth = (int) Math.round(bounds.getWidth() * snapScale);
            snapImageHeight = (int) Math.round(bounds.getHeight() * snapScale);
            snapParameters = new SnapshotParameters();
            snapParameters.setFill(Color.TRANSPARENT);
            snapParameters.setTransform(javafx.scene.transform.Transform.scale(snapScale, snapScale));

            snaps = new ArrayList<>();
            snapsTotal = snapTotalHeight % snapStep == 0
                    ? snapTotalHeight / snapStep : snapTotalHeight / snapStep + 1;
            snapFileWidth = snapFileHeight = 0;

            if (timer != null) {
                timer.cancel();
            }
            timer = new Timer();
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    Platform.runLater(() -> {
                        snap();
                    });
                }
            }, 2000);    // make sure page is loaded before snapping

        } catch (Exception e) {
            logger.error(e.toString());
        }
    }

    private void snap() {
        try {
            if (loadingController == null) {
                return;
            }
            WritableImage snapshot = new WritableImage(snapImageWidth, snapImageHeight);
            snapshot = webView.snapshot(snapParameters, snapshot);
            Image cropped;
            if (snapTotalHeight < snapHeight + snapStep) { // last snap
                cropped = FxmlImageManufacture.cropOutsideFx(snapshot, 0,
                        (int) ((snapStep + snapHeight - snapTotalHeight) * snapScale),
                        (int) snapshot.getWidth() - 1, (int) snapshot.getHeight() - 1);
            } else {
                cropped = snapshot;
            }
            if (cropped.getWidth() > snapFileWidth) {
                snapFileWidth = (int) cropped.getWidth();
            }
            snapFileHeight += cropped.getHeight();
            snapHeight += snapStep;
            File tmpfile = FileTools.getTempFile(".png");
            ImageFileWriters.writeImageFile(SwingFXUtils.fromFXImage(cropped, null), "png", tmpfile.getAbsolutePath());
            snaps.add(tmpfile);
            loadingController.setInfo(AppVariables.message("CurrentPageHeight") + ": " + snapHeight);
            if (snapTotalHeight > snapHeight) {
                webEngine.executeScript("window.scrollTo(0, " + snapHeight + ");");
                if (timer != null) {
                    timer.cancel();
                }
                timer = new Timer();
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        Platform.runLater(() -> {
                            snap();
                        });
                    }
                }, 300);    // make sure page is loaded before snapping

            } else { // last snap
                loadingController.setInfo(AppVariables.message("WritingFile"));
                boolean success = true;
                if (isOneImage) {
                    Runtime r = Runtime.getRuntime();
                    long availableMem = r.maxMemory() - (r.totalMemory() - r.freeMemory()) / (1024 * 1024L);
                    long requiredMem = snapFileWidth * snapFileHeight * 5L / (1024 * 1024) + 200;
                    if (availableMem < requiredMem) {
                        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
                        alert.setTitle(getBaseTitle());
                        alert.setContentText(MessageFormat.format(AppVariables.message("MergedSnapshotTooLarge"), availableMem, requiredMem));
                        alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
                        ButtonType buttonPdf = new ButtonType(AppVariables.message("SaveAsPdf"));
                        ButtonType buttonCancel = new ButtonType(AppVariables.message("Cancel"));
                        alert.getButtonTypes().setAll(buttonPdf, buttonCancel);
                        Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
                        stage.setAlwaysOnTop(true);
                        stage.toFront();

                        Optional<ButtonType> result = alert.showAndWait();
                        if (result.get() == buttonPdf) {
                            success = PdfTools.htmlIntoPdf(snaps, targetFile, windowSizeCheck.isSelected());
                        } else {
                            success = false;
                        }
                    } else {
                        BufferedImage finalImage = ImageManufacture.mergeImagesVertical(snaps, snapFileWidth, snapFileHeight);
                        if (finalImage != null) {
                            String format = FileTools.getFileSuffix(targetFile.getAbsolutePath());
                            ImageFileWriters.writeImageFile(finalImage, format, targetFile.getAbsolutePath());
                        } else {
                            success = false;
                        }
                    }
                } else {
                    success = PdfTools.htmlIntoPdf(snaps, targetFile, windowSizeCheck.isSelected());
                }
                snaps = null;
                if (success && targetFile.exists()) {
                    view(targetFile);
                } else {
                    popFailed();
                }

                webEngine.executeScript("window.scrollTo(0,0 );");
                bottomText.setText("");
                snapshotButton.setDisable(false);

                if (loadingController != null) {
                    loadingController.closeStage();
                    loadingController = null;
                }
                myStage.setY(orginalStageY);
                myStage.setHeight(orginalStageHeight);
            }

        } catch (Exception e) {
            webEngine.executeScript("window.scrollTo(0,0 );");
            popFailed();
            if (loadingController != null) {
                loadingController.closeStage();
                loadingController = null;
            }
        }

    }

    @FXML
    private void zoomIn(ActionEvent event) {
//        ++fontSize;
//        webEngine.executeScript("document.body.style.fontSize = '" + fontSize + "px' ;");
        zoomScale += 0.1f;
        webView.setZoom(zoomScale);
    }

    @FXML
    private void zoomOut(ActionEvent event) {
//        --fontSize;
//        webEngine.executeScript("document.body.style.fontSize = '" + fontSize + "px' ;");
        zoomScale -= 0.1f;
        webView.setZoom(zoomScale);
    }

    @FXML
    private void backAction(ActionEvent event) {
        isFrameSet = false;
        webEngine.executeScript("window.history.back();");
    }

    @FXML
    private void forwardAction(ActionEvent event) {
        isFrameSet = false;
        webEngine.executeScript("window.history.forward();");
    }

    @FXML
    private void refreshAction(ActionEvent event) {
        webEngine.executeScript("location.reload() ;");
    }

    public void switchBroswerTab() {
        tabPane.getSelectionModel().select(browserTab);
    }

    public void loginWeibo() {
        try {
            tabPane.getSelectionModel().select(browserTab);
            isFrameSet = false;
            isSettingValues = true;
            urlBox.setValue("https://weibo.com");
            isSettingValues = false;
            webEngine.load("https://weibo.com");
            popInformation(message("WeiboAfterLogin"), -1);
        } catch (Exception e) {
            logger.debug(e.toString());
        }

    }

    /*
        Markdown
     */
    protected void html2markdown(String contents) {
        if (contents == null || contents.isEmpty()) {
            markdownArea.setText("");
            return;
        }
        synchronized (this) {
            if (task != null) {
                return;
            }
            task = new SingletonTask<Void>() {

                private String md;

                @Override
                protected boolean handle() {
                    try {
                        MutableDataSet options = new MutableDataSet();
                        md = FlexmarkHtmlConverter.builder(options).build().convert(contents);
                        return md != null;
                    } catch (Exception e) {
                        error = e.toString();
                        return false;
                    }
                }

                @Override
                protected void whenSucceeded() {
                    markdownArea.setText(md);
                }

            };
            openHandlingStage(task, Modality.WINDOW_MODAL);
            Thread thread = new Thread(task);
            thread.setDaemon(true);
            thread.start();
        }
    }

    @FXML
    protected void editMarkdown() {
        if (markdownArea.getText().isEmpty()) {
            return;
        }
        MarkdownEditerController controller
                = (MarkdownEditerController) openStage(CommonValues.MarkdownEditorFxml);
        controller.loadMarkdown(markdownArea.getText());
    }

    @FXML
    protected void refreshMarkdown() {
        html2markdown(codesArea.getText());
    }

    @FXML
    protected void saveMarkdown() {
        if (markdownArea.getText().isEmpty()) {
            return;
        }
        synchronized (this) {
            if (task != null) {
                return;
            }

            String name = "";
            if (sourceFile != null) {
                name = FileTools.getFilePrefix(sourceFile.getName());
            }
            final File file = chooseSaveFile(AppVariables.getUserConfigPath("MarkdownFilePath"),
                    name, CommonFxValues.MarkdownExtensionFilter, true);
            if (file == null) {
                return;
            }
            recordFileWritten(file, "MarkdownFilePath",
                    VisitHistory.FileType.Markdown, VisitHistory.FileType.Markdown);

            task = new SingletonTask<Void>() {

                @Override
                protected boolean handle() {
                    return FileTools.writeFile(file, markdownArea.getText()) != null;
                }

            };
            openHandlingStage(task, Modality.WINDOW_MODAL);
            Thread thread = new Thread(task);
            thread.setDaemon(true);
            thread.start();
        }
    }

    @FXML
    public void popSaveMarkdown(MouseEvent event) { //
        if (AppVariables.fileRecentNumber <= 0) {
            return;
        }
        new RecentVisitMenu(this, event) {
            @Override
            public List<VisitHistory> recentFiles() {
                return null;
            }

            @Override
            public List<VisitHistory> recentPaths() {
                return VisitHistory.getRecentPath(VisitHistory.FileType.Markdown);
            }

            @Override
            public void handleSelect() {
                saveMarkdown();
            }

            @Override
            public void handleFile(String fname) {

            }

            @Override
            public void handlePath(String fname) {
                File file = new File(fname);
                if (!file.exists()) {
                    handleSelect();
                    return;
                }
                AppVariables.setUserConfigValue("MarkdownFilePath", fname);
                handleSelect();
            }

        }.pop();
    }

    @Override
    public boolean leavingScene() {
        if (loadingController != null) {
            loadingController.closeStage();
            loadingController = null;
        }
        if (timer != null) {
            timer.cancel();
        }
        if (webEngine != null && webEngine.getLoadWorker() != null) {
            webEngine.getLoadWorker().cancel();
        }
        webEngine = null;

        webView = null;

        return super.leavingScene();

    }

    @Override
    public boolean checkBeforeNextAction() {
//        logger.debug(fileChanged.getValue());

        if (fileChanged == null || !fileChanged.getValue()) {
            return true;
        } else {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle(getMyStage().getTitle());
            alert.setContentText(AppVariables.message("FileChanged"));
            alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
            ButtonType buttonSave = new ButtonType(AppVariables.message("Save"));
            ButtonType buttonNotSave = new ButtonType(AppVariables.message("NotSave"));
            ButtonType buttonCancel = new ButtonType(AppVariables.message("Cancel"));
            alert.getButtonTypes().setAll(buttonSave, buttonNotSave, buttonCancel);
            Stage stage = (Stage) alert.getDialogPane().getScene().getWindow();
            stage.setAlwaysOnTop(true);
            stage.toFront();

            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == buttonSave) {
                saveAction();
                return true;
            } else {
                return result.get() == buttonNotSave;
            }
        }
    }

    /*
        get/set
     */
    public boolean isNotChangedAfterLoad() {
        return notChangedAfterLoad;
    }

    public void setNotChangedAfterLoad(boolean notChangedAfterLoad) {
        this.notChangedAfterLoad = notChangedAfterLoad;
    }

}