package mara.mybox.controller;

import java.io.File;
import java.io.FileWriter;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.input.MouseEvent;
import mara.mybox.db.DerbyBase;
import mara.mybox.db.data.DataNode;
import mara.mybox.db.data.DataNodeTag;
import mara.mybox.db.data.DataNodeTools;
import mara.mybox.db.data.VisitHistory;
import mara.mybox.db.table.BaseNodeTable;
import static mara.mybox.db.table.BaseNodeTable.RootID;
import mara.mybox.db.table.TableDataNodeTag;
import mara.mybox.dev.MyBoxLog;
import mara.mybox.fxml.FxTask;
import mara.mybox.fxml.WindowTools;
import mara.mybox.fxml.style.HtmlStyles;
import mara.mybox.tools.CsvTools;
import mara.mybox.tools.FileNameTools;
import mara.mybox.tools.HtmlWriteTools;
import mara.mybox.tools.TextTools;
import static mara.mybox.value.AppValues.Indent;
import mara.mybox.value.Fxmls;
import static mara.mybox.value.Languages.message;
import mara.mybox.value.UserConfig;
import org.apache.commons.csv.CSVPrinter;

/**
 * @Author Mara
 * @CreateDate 2021-4-30
 * @License Apache License Version 2.0
 */
public class DataTreeExportController extends BaseDataTreeHandleController {

    protected TableDataNodeTag nodeTagsTable;
    protected DataNode sourceNode;
    protected long sourceid;
    protected File treeXmlFile, treeHtmlFile, treeJsonFile, listJsonFile,
            listHtmlFile, listXmlFile, listCsvFile, framesetFile, framesetNavFile;
    protected FileWriter treeHtmlWriter, treeXmlWriter, treeJsonWriter,
            listJsonWriter, listHtmlWriter, listXmlWriter, framesetNavWriter;
    protected CSVPrinter csvPrinter;
    protected String hierarchyNumber, language;
    protected int count, level, childrenNumber;
    protected Charset charset;
    protected Stack<Integer> childrenNumberStack;

    @FXML
    protected CheckBox idCheck, hierarchyCheck, timeCheck, tagsCheck, orderCheck,
            parentCheck, dataCheck, treeXmlCheck, treeHtmlCheck, treeJsonCheck,
            listJsonCheck, listCsvCheck, listXmlCheck, listHtmlCheck, framesetCheck,
            dataFormatCheck;
    @FXML
    protected ComboBox<String> charsetSelector;
    @FXML
    protected TextArea styleInput;
    @FXML
    protected Label nodeLabel;

    public void setParameters(BaseDataTreeController parent, DataNode node) {
        try {
            if (parent == null) {
                close();
                return;
            }
            dataController = parent;
            parentController = parent;

            setData(dataController.nodeTable, node != null ? node : dataController.rootNode);

        } catch (Exception e) {
            MyBoxLog.error(e);
        }
    }

    public void setData(BaseNodeTable table, DataNode node) {
        try {
            if (table == null) {
                close();
                return;
            }
            nodeTable = table;
            nodeTagsTable = new TableDataNodeTag(nodeTable);
            dataName = nodeTable.getTableName();
            baseName = baseName + "_" + dataName;

            sourceNode = node != null ? node : nodeTable.getRoot();
            if (sourceNode == null) {
                close();
                return;
            }
            sourceid = sourceNode.getNodeid();

            baseTitle = nodeTable.getTreeName() + " - "
                    + message("Export") + " : " + sourceNode.getTitle();
            chainName = sourceNode.shortDescription();

            setControls();

        } catch (Exception e) {
            MyBoxLog.error(e);
        }
    }

    public void setControls() {
        try {
            setTitle(baseTitle);

            nodeLabel.setText(message("Node") + ": " + chainName);

            dataFormatCheck.setSelected(UserConfig.getBoolean(baseName + "Format", true));
            dataFormatCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Format", dataFormatCheck.isSelected());
                }
            });

            idCheck.setSelected(UserConfig.getBoolean(baseName + "ID", false));
            idCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "ID", idCheck.isSelected());
                }
            });

            hierarchyCheck.setSelected(UserConfig.getBoolean(baseName + "Hierarcy", false));
            hierarchyCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Hierarcy", hierarchyCheck.isSelected());
                }
            });

            timeCheck.setSelected(UserConfig.getBoolean(baseName + "Time", false));
            timeCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Time", timeCheck.isSelected());
                }
            });

            tagsCheck.setSelected(UserConfig.getBoolean(baseName + "Tags", true));
            tagsCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Tags", tagsCheck.isSelected());
                }
            });

            orderCheck.setSelected(UserConfig.getBoolean(baseName + "Order", true));
            orderCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Order", orderCheck.isSelected());
                }
            });

            parentCheck.setSelected(UserConfig.getBoolean(baseName + "Parent", false));
            parentCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Parent", parentCheck.isSelected());
                }
            });

            dataCheck.setSelected(UserConfig.getBoolean(baseName + "Data", true));
            dataCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Data", dataCheck.isSelected());
                }
            });

            treeHtmlCheck.setSelected(UserConfig.getBoolean(baseName + "TreeHtml", true));
            treeHtmlCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "TreeHtml", treeHtmlCheck.isSelected());
                }
            });

            treeXmlCheck.setSelected(UserConfig.getBoolean(baseName + "TreeXml", true));
            treeXmlCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "TreeXml", treeXmlCheck.isSelected());
                }
            });

            treeJsonCheck.setSelected(UserConfig.getBoolean(baseName + "TreeJson", true));
            treeJsonCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "TreeJson", treeJsonCheck.isSelected());
                }
            });

            listHtmlCheck.setSelected(UserConfig.getBoolean(baseName + "ListHtml", false));
            listHtmlCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "ListHtml", listHtmlCheck.isSelected());
                }
            });

            listXmlCheck.setSelected(UserConfig.getBoolean(baseName + "ListXML", false));
            listXmlCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "ListXML", listXmlCheck.isSelected());
                }
            });

            listCsvCheck.setSelected(UserConfig.getBoolean(baseName + "ListCSV", false));
            listCsvCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "ListCSV", listCsvCheck.isSelected());
                }
            });

            listJsonCheck.setSelected(UserConfig.getBoolean(baseName + "ListJson", false));
            listJsonCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "ListJson", listJsonCheck.isSelected());
                }
            });

            framesetCheck.setSelected(UserConfig.getBoolean(baseName + "Frameset", false));
            framesetCheck.selectedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> v, Boolean oldV, Boolean newV) {
                    if (isSettingValues) {
                        return;
                    }
                    UserConfig.setBoolean(baseName + "Frameset", framesetCheck.isSelected());
                }
            });

            List<String> setNames = TextTools.getCharsetNames();
            charsetSelector.getItems().addAll(setNames);
            try {
                charset = Charset.forName(UserConfig.getString(baseName + "Charset", Charset.defaultCharset().name()));
            } catch (Exception e) {
                charset = Charset.defaultCharset();
            }
            charsetSelector.setValue(charset.name());
            charsetSelector.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
                @Override
                public void changed(ObservableValue ov, String oldValue, String newValue) {
                    if (isSettingValues) {
                        return;
                    }
                    charset = Charset.forName(charsetSelector.getSelectionModel().getSelectedItem());
                    UserConfig.setString(baseName + "Charset", charset.name());
                }
            });

            styleInput.setText(UserConfig.getString(baseName + "Style", HtmlStyles.styleValue("Default")));

        } catch (Exception e) {
            MyBoxLog.error(e);
        }
    }

    @Override
    public boolean checkOptions() {
        if (sourceNode == null) {
            close();
            return false;
        }
        treeXmlFile = null;
        treeHtmlFile = null;
        treeJsonFile = null;
        listJsonFile = null;
        listHtmlFile = null;
        listXmlFile = null;
        listCsvFile = null;
        framesetFile = null;
        framesetNavFile = null;
        treeXmlWriter = null;
        treeHtmlWriter = null;
        treeJsonWriter = null;
        listHtmlWriter = null;
        listXmlWriter = null;
        listJsonWriter = null;
        framesetNavWriter = null;
        csvPrinter = null;
        level = count = 0;
        if (!listHtmlCheck.isSelected() && !framesetCheck.isSelected()
                && !treeXmlCheck.isSelected() && !treeHtmlCheck.isSelected()
                && !treeJsonCheck.isSelected() && !listCsvCheck.isSelected()
                && !listXmlCheck.isSelected() && !listJsonCheck.isSelected()) {
            popError(message("NothingSave"));
            return false;
        }

        targetPath = targetPathController.pickFile();
        if (targetPath == null) {
            popError(message("InvalidParameters") + ": " + message("TargetPath"));
            return false;
        }
        return true;
    }

    @FXML
    public void popDefaultStyle(MouseEvent mouseEvent) {
        try {
            List<MenuItem> items = new ArrayList<>();
            MenuItem menu;
            for (HtmlStyles.HtmlStyle style : HtmlStyles.HtmlStyle.values()) {
                menu = new MenuItem(message(style.name()));
                menu.setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent event) {
                        styleInput.setText(HtmlStyles.styleValue(style));
                        UserConfig.setString(baseName + "Style", styleInput.getText());
                    }
                });
                items.add(menu);
            }

            items.add(new SeparatorMenuItem());

            popEventMenu(mouseEvent, items);
        } catch (Exception e) {
            MyBoxLog.error(e);
        }
    }

    @FXML
    public void clearStyle() {
        styleInput.clear();
    }

    @Override
    public boolean doTask(FxTask currentTask) {
        if (sourceNode == null || targetPath == null) {
            return false;
        }
        count = level = 0;
        childrenNumberStack = new Stack();
        childrenNumber = 0;
        try (Connection conn = DerbyBase.getConnection()) {
            chainName = nodeTable.chainName(currentTask, conn, sourceNode);
            showLogs("Export started. Node: " + sourceid + " - " + chainName);
            if (!openWriters()) {
                closeWriters();
                return false;
            }
            conn.setAutoCommit(true);
            exportNode(currentTask, conn, sourceid, chainName, "");
        } catch (Exception e) {
            updateLogs(e.toString());
        }
        return closeWriters();
    }

    protected boolean openWriters() {
        if (sourceNode == null || targetPath == null || chainName == null) {
            return false;
        }
        try {
            String prefix = chainName.replaceAll(DataNode.TitleSeparater, "-");

            if (treeXmlCheck.isSelected()) {
                treeXmlFile = makeTargetFile(prefix + "_tree", ".xml", targetPath);
                if (treeXmlFile != null) {
                    showLogs(message("Writing") + " " + treeXmlFile.getAbsolutePath());
                    treeXmlWriter = new FileWriter(treeXmlFile, charset);
                    StringBuilder s = new StringBuilder();
                    s.append("<?xml version=\"1.0\" encoding=\"")
                            .append(charset.name()).append("\"?>\n")
                            .append("<").append(nodeTable.getTableName()).append(">\n");
                    treeXmlWriter.write(s.toString());
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (listXmlCheck.isSelected()) {
                listXmlFile = makeTargetFile(prefix + "_list", ".xml", targetPath);
                if (listXmlFile != null) {
                    showLogs(message("Writing") + " " + listXmlFile.getAbsolutePath());
                    listXmlWriter = new FileWriter(listXmlFile, charset);
                    StringBuilder s = new StringBuilder();
                    s.append("<?xml version=\"1.0\" encoding=\"")
                            .append(charset.name()).append("\"?>\n")
                            .append("<").append(nodeTable.getTableName()).append(">\n");
                    listXmlWriter.write(s.toString());
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (treeJsonCheck.isSelected()) {
                treeJsonFile = makeTargetFile(prefix + "_tree", ".json", targetPath);
                if (treeJsonFile != null) {
                    showLogs(message("Writing") + " " + treeJsonFile.getAbsolutePath());
                    treeJsonWriter = new FileWriter(treeJsonFile, Charset.forName("UTF-8"));
                    StringBuilder s = new StringBuilder();
                    s.append("{\"").append(nodeTable.getTreeName()).append("\": \n");
                    treeJsonWriter.write(s.toString());
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (listJsonCheck.isSelected()) {
                listJsonFile = makeTargetFile(prefix + "_list", ".json", targetPath);
                if (listJsonFile != null) {
                    showLogs(message("Writing") + " " + listJsonFile.getAbsolutePath());
                    listJsonWriter = new FileWriter(listJsonFile, Charset.forName("UTF-8"));
                    StringBuilder s = new StringBuilder();
                    s.append("{\"").append(nodeTable.getTreeName()).append("\": [\n");
                    listJsonWriter.write(s.toString());
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (listCsvCheck.isSelected()) {
                listCsvFile = makeTargetFile(prefix, ".csv", targetPath);
                if (listCsvFile != null) {
                    showLogs(message("Writing") + " " + listCsvFile.getAbsolutePath());
                    csvPrinter = new CSVPrinter(new FileWriter(listCsvFile, charset), CsvTools.csvFormat());

                    List<String> names = new ArrayList<>();
                    if (idCheck.isSelected()) {
                        names.add(message("NodeID"));
                        names.add(message("ParentID"));
                    }
                    if (parentCheck.isSelected()) {
                        names.add(message("ParentNode"));
                    }
                    if (hierarchyCheck.isSelected()) {
                        names.add(message("HierarchyNumber"));
                    }
                    names.add(message("Title"));
                    if (orderCheck.isSelected()) {
                        names.add(message("OrderNumber"));
                    }
                    if (timeCheck.isSelected()) {
                        names.add(message("Update_Time"));
                    }
                    if (tagsCheck.isSelected()) {
                        names.add(message("Tags"));
                    }
                    if (dataCheck.isSelected()) {
                        for (String name : nodeTable.dataColumnNames()) {
                            names.add(name);
                        }
                    }
                    csvPrinter.printRecord(names);

                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (treeHtmlCheck.isSelected()) {
                treeHtmlFile = makeTargetFile(prefix + "_tree", ".html", targetPath);
                if (treeHtmlFile != null) {
                    showLogs(message("Writing") + " " + treeHtmlFile.getAbsolutePath());
                    treeHtmlWriter = new FileWriter(treeHtmlFile, charset);
                    writeHtmlHead(treeHtmlWriter, chainName);
                    treeHtmlWriter.write(Indent + "<BODY>\n" + Indent + Indent + "<H2>" + chainName + "</H2>\n");
                    treeHtmlWriter.write(DataNodeTools.htmlControls(true));
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (listHtmlCheck.isSelected()) {
                listHtmlFile = makeTargetFile(prefix + "_list", ".html", targetPath);
                if (listHtmlFile != null) {
                    showLogs(message("Writing") + " " + listHtmlFile.getAbsolutePath());
                    listHtmlWriter = new FileWriter(listHtmlFile, charset);
                    writeHtmlHead(listHtmlWriter, chainName);
                    listHtmlWriter.write(Indent + "<BODY>\n" + Indent + Indent + "<H2>" + chainName + "</H2>\n");
                    listHtmlWriter.write(DataNodeTools.htmlControls(false));
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

            if (framesetCheck.isSelected()) {
                framesetFile = makeTargetFile(prefix, "_frameset.html", targetPath);
                if (framesetFile != null) {
                    showLogs(message("Writing") + " " + framesetFile.getAbsolutePath());
                    StringBuilder s;
                    String subPath = FileNameTools.filter(prefix) + "-frameset";
                    File path = new File(targetPath + File.separator + subPath + File.separator);
                    path.mkdirs();
                    framesetNavFile = new File(path.getAbsolutePath() + File.separator + "nav.html");
                    File coverFile = new File(path.getAbsolutePath() + File.separator + "cover.html");
                    try (FileWriter coverWriter = new FileWriter(coverFile, charset)) {
                        writeHtmlHead(coverWriter, chainName);
                        coverWriter.write("<BODY>\n<BR><BR><BR><BR><H1>" + message("Notes") + "</H1>\n</BODY></HTML>");
                        coverWriter.flush();
                    }
                    try (FileWriter framesetWriter = new FileWriter(framesetFile, charset)) {
                        writeHtmlHead(framesetWriter, chainName);
                        s = new StringBuilder();
                        s.append("<FRAMESET border=2 cols=240,240,*>\n")
                                .append("<FRAME name=nav src=\"").append(subPath).append("/").append(framesetNavFile.getName()).append("\" />\n")
                                .append("<FRAME name=booknav />\n")
                                .append("<FRAME name=main src=\"").append(subPath).append("/cover.html\" />\n</HTML>\n");
                        framesetWriter.write(s.toString());
                        framesetWriter.flush();
                    }
                    framesetNavWriter = new FileWriter(framesetNavFile, charset);
                    writeHtmlHead(framesetNavWriter, chainName);
                    s = new StringBuilder();
                    s.append(Indent).append("<BODY>\n");
                    s.append(Indent).append(Indent).append("<H2>").append(chainName).append("</H2>\n");
                    framesetNavWriter.write(s.toString());
                } else if (targetPathController.isSkip()) {
                    showLogs(message("Skipped"));
                }
            }

        } catch (Exception e) {
            updateLogs(e.toString());
            return false;
        }
        return true;
    }

    protected boolean closeWriters() {
        if (sourceNode == null || targetPath == null) {
            return false;
        }
        boolean well = true;

        if (treeXmlWriter != null) {
            try {
                treeXmlWriter.write("</" + nodeTable.getTableName() + ">\n");
                treeXmlWriter.flush();
                treeXmlWriter.close();
                treeXmlWriter = null;
                targetFileGenerated(treeXmlFile, VisitHistory.FileType.XML);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (listXmlWriter != null) {
            try {
                listXmlWriter.write("</" + nodeTable.getTableName() + ">\n");
                listXmlWriter.flush();
                listXmlWriter.close();
                listXmlWriter = null;
                targetFileGenerated(listXmlFile, VisitHistory.FileType.XML);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (treeJsonWriter != null) {
            try {
                treeJsonWriter.write("\n}\n");
                treeJsonWriter.flush();
                treeJsonWriter.close();
                treeJsonWriter = null;
                targetFileGenerated(treeJsonFile, VisitHistory.FileType.JSON);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (listJsonWriter != null) {
            try {
                listJsonWriter.write("\n]}\n");
                listJsonWriter.flush();
                listJsonWriter.close();
                listJsonWriter = null;
                targetFileGenerated(listJsonFile, VisitHistory.FileType.JSON);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (csvPrinter != null) {
            try {
                csvPrinter.flush();
                csvPrinter.close();
                csvPrinter = null;
                targetFileGenerated(listCsvFile, VisitHistory.FileType.CSV);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (treeHtmlWriter != null) {
            try {
                treeHtmlWriter.write(Indent + "</BODY>\n</HTML>\n");
                treeHtmlWriter.flush();
                treeHtmlWriter.close();
                treeHtmlWriter = null;
                targetFileGenerated(treeHtmlFile, VisitHistory.FileType.Html);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (listHtmlWriter != null) {
            try {
                listHtmlWriter.write(Indent + "</BODY>\n</HTML>\n");
                listHtmlWriter.flush();
                listHtmlWriter.close();
                listHtmlWriter = null;
                targetFileGenerated(listHtmlFile, VisitHistory.FileType.Html);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        if (framesetNavWriter != null) {
            try {
                framesetNavWriter.write(Indent + "</BODY>\n</HTML>\n");
                framesetNavWriter.flush();
                framesetNavWriter.close();
                framesetNavWriter = null;
                targetFileGenerated(framesetFile, VisitHistory.FileType.Html);
            } catch (Exception e) {
                updateLogs(e.toString());
                well = false;
            }
        }

        return well;
    }

    public void exportNode(FxTask currentTask, Connection conn, long nodeid,
            String parentChainName, String hierarchyNumber) {
        level++;
        if (conn == null || nodeid < 0) {
            return;
        }
        try {
            count++;

            DataNode node = nodeTable.query(conn, nodeid);
            if (hierarchyCheck.isSelected()) {
                node.setHierarchyNumber(hierarchyNumber);
            }
            String nodeChainName;
            if (parentChainName != null && !parentChainName.isBlank()) {
                nodeChainName = parentChainName + DataNode.TitleSeparater + node.getTitle();
            } else {
                nodeChainName = node.getTitle();
            }
            if (parentCheck.isSelected()) {
                node.setChainName(nodeChainName);
            }
            if (isLogsVerbose()) {
                showLogs("Handling node: " + nodeid + " - " + node.getTitle());
            }
            List<DataNodeTag> tags = null;
            if (tagsCheck.isSelected()) {
                tags = nodeTagsTable.nodeTags(conn, nodeid);
            }
            String nodePrefix = "";
            for (int i = 0; i < level; i++) {
                nodePrefix += Indent;
            }

            if (treeXmlWriter != null) {
                if (nodeid != RootID) {
                    treeXmlWriter.write(nodePrefix + "<TreeNode>\n");
                }
                writeTreeXML(currentTask, conn, node, tags, nodePrefix);
            }
            if (listXmlWriter != null) {
                writeListXML(currentTask, conn, node, tags);
            }
            if (treeJsonWriter != null) {
                writeTreeJson(currentTask, conn, node, tags, nodePrefix);
            }
            if (listJsonWriter != null) {
                writeListJson(currentTask, conn, node, tags);
            }
            if (csvPrinter != null) {
                writeListCsv(currentTask, conn, node, tags);
            }
            if (treeHtmlWriter != null) {
                writeTreeHtml(currentTask, conn, node, tags);
            }
            if (listHtmlWriter != null) {
                writeListHtml(currentTask, conn, node, tags, listHtmlWriter);
            }

            if (currentTask == null || !currentTask.isWorking()) {
                return;
            }

            if (framesetNavWriter != null) {
                String nodeTitle = node.getTitle() + "_" + node.getNodeid();
                File bookNavFile = new File(framesetNavFile.getParent() + File.separator + FileNameTools.filter(nodeTitle) + "_nav.html");
                FileWriter bookNavWriter = new FileWriter(bookNavFile, charset);
                writeHtmlHead(bookNavWriter, nodeTitle);
                bookNavWriter.write(Indent + "<BODY>\n");

                File nodeFile = new File(framesetNavFile.getParent() + File.separator + FileNameTools.filter(nodeTitle) + ".html");
                FileWriter bookWriter = new FileWriter(nodeFile, charset);
