/*--------------------------------------------------------------*
  Copyright (C) 2006-2020 OpenSim Ltd.

  This file is distributed WITHOUT ANY WARRANTY. See the file
  'License' for details on this and other legal matters.
*--------------------------------------------------------------*/

package org.omnetpp.scave;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.omnetpp.common.util.StringUtils;
import org.omnetpp.common.util.XmlUtils;
import org.omnetpp.scave.charttemplates.ChartTemplateRegistry;
import org.omnetpp.scave.model.Analysis;
import org.omnetpp.scave.model.Chart;
import org.omnetpp.scave.model.Chart.ChartType;
import org.omnetpp.scave.model.ChartTemplate;
import org.omnetpp.scave.model.Folder;
import org.omnetpp.scave.model.InputFile;
import org.omnetpp.scave.model.Property;
import org.omnetpp.scave.model2.ScaveModelUtil;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Loads an Analysis model (object hierarchy) from an analysis (.anf) file.
 *
 * Uses hand-written logic to extract the model from the XML DOM of
 * the analysis file, to understand not only the latest file format,
 * but also some slightly different, development variants of it.
 *
 * For loading legacy analysis files (saved by OMNeT++ versions older than 6.0),
 * see LegacyAnalysisLoader.
 */
public class AnalysisLoader {

    /**
     * Helper method to extract the text content of an element - excluding
     * any indentation around a CDATA section in it, if present. If there
     * is no CDATA section in the element, return the entire text contents.
     */
    private static String extractCdataOrTextContent(Element scriptElement) {
        int n = scriptElement.getChildNodes().getLength();
        for (int k = 0; k < n; ++k) {
            Node childNode = scriptElement.getChildNodes().item(k);
            // to make sure the indentation before the CDATA section is not included (it can mess up the script)
            if (childNode.getNodeType() == Node.CDATA_SECTION_NODE) {
                return childNode.getTextContent();
            }
        }
        // just return the entire text content of the <script> tag
        return scriptElement.getTextContent();
    }

    /**
     * Loads an Analysis model from the given XML DOM root node, resolving
     * chart template IDs to references in the given registry.
     */
    public static Analysis loadNewAnalysis(Node rootNode, ChartTemplateRegistry chartTemplateRegistry) {

        if (!rootNode.getNodeName().equals("analysis"))
            throw new RuntimeException("Invalid top level node: " + rootNode.getNodeName());

        Analysis analysis = new Analysis();

        Node versionNode = rootNode.getAttributes().getNamedItem("version");

        if (versionNode == null)
            throw new RuntimeException("Unspecified analysis file version");

        if (!"2".equals(versionNode.getNodeValue()))
            throw new RuntimeException("invalid analysis file version: " + versionNode.getNodeValue());

        NodeList topLevelNodes = rootNode.getChildNodes();

        for (int i = 0; i < topLevelNodes.getLength(); ++i) {
            Node node = topLevelNodes.item(i);

            if ("inputs".equals(node.getNodeName())) {
                NodeList inputNodes = node.getChildNodes();
                for (int j = 0; j < inputNodes.getLength(); ++j) {
                    Node inputNode = inputNodes.item(j);
                    if ("input".equals(inputNode.getNodeName())) {
                        Node patternNode = inputNode.getAttributes().getNamedItem("pattern");
                        InputFile input = new InputFile(patternNode.getNodeValue());

                        analysis.getInputs().addInput(input);
                    } else if ("#text".equals(inputNode.getNodeName())) {
                        if (!inputNode.getNodeValue().trim().isEmpty())
                            throw new RuntimeException("Unexpected text content: " + inputNode.getNodeValue());
                    } else {
                        throw new RuntimeException("Invalid child node: " + inputNode.getNodeName());
                    }
                }
            }
            else if ("charts".equals(node.getNodeName())) {
                loadAnalysisItems(node, analysis.getRootFolder(), chartTemplateRegistry);
            }
            else if ("#text".equals(node.getNodeName())) {
                if (!node.getNodeValue().trim().isEmpty())
                    throw new RuntimeException("Unexpected text content: " + node.getNodeValue());
            }
            else {
                throw new RuntimeException("Invalid child node: " + node.getNodeName());
            }
        }

        return analysis;
    }

    protected static void loadAnalysisItems(Node node, Folder container, ChartTemplateRegistry chartTemplateRegistry) {
        NodeList chartNodes = node.getChildNodes();

        for (int j = 0; j < chartNodes.getLength(); ++j) {
            Node chartNode = chartNodes.item(j);

            if ("chart".equals(chartNode.getNodeName())) {
                Chart chart = loadChart((Element)chartNode, chartTemplateRegistry);
                container.add(chart);
            }
            else if ("folder".equals(chartNode.getNodeName())) {
                Folder folder = loadFolder((Element)chartNode, chartTemplateRegistry);
                container.add(folder);
            }
            else if ("#text".equals(chartNode.getNodeName())) {
                if (!chartNode.getNodeValue().trim().isEmpty())
                    throw new RuntimeException("Unexpected text content: " + chartNode.getNodeValue());
            }
            else {
                throw new RuntimeException("Invalid child node: " + chartNode.getNodeName());
            }
        }
    }

    protected static Chart loadChart(Element chartNode, ChartTemplateRegistry chartTemplateRegistry) {
        Chart chart = new Chart();

        String chartType = chartNode.getAttribute("type");
        try {
            ChartType type = ChartType.valueOf(chartType);
            chart.setType(type);
        }
        catch (IllegalArgumentException e) {
            switch (chartType) {
                case "MatplotlibChart": chart.setType(ChartType.MATPLOTLIB); break;
                case "BarChart": chart.setType(ChartType.BAR); break;
                case "LineChart": chart.setType(ChartType.LINE); break;
                case "ScatterChart": chart.setType(ChartType.LINE); break; // for backward compatibility
                case "HistogramChart": chart.setType(ChartType.HISTOGRAM); break;
                default: throw new RuntimeException("Unknown chart type: " + chartType);
            }
        }

        String idAttr = chartNode.getAttribute("id");
        if (!idAttr.isEmpty())
            chart.setIdIfUnique(Integer.parseInt(idAttr));
        chart.setName(chartNode.getAttribute("name"));

        String templateAttr = chartNode.getAttribute("template");
        if (!templateAttr.isEmpty())
            chart.setTemplateID(templateAttr);

        String iconPathAttr = chartNode.getAttribute("icon");
        if (!iconPathAttr.isEmpty())
            chart.setIconPath(iconPathAttr);
        else {
            ChartTemplate template = chartTemplateRegistry.findTemplateByID(chart.getTemplateID());
            if (template != null)
                chart.setIconPath(template.getIconPath());
        }

        String scriptAttr = chartNode.getAttribute("script");
        Element scriptElement = XmlUtils.getFirstChildElementWithTag(chartNode, "script");

        if (!scriptAttr.isEmpty())
            chart.setScript(scriptAttr);
        else {
            if (scriptElement != null)
                chart.setScript(extractCdataOrTextContent(scriptElement));
            else
                chart.setScript(StringUtils.stripEnd(chartNode.getTextContent(), " "));
        }

        Element formElement = XmlUtils.getFirstChildElementWithTag(chartNode, "form");
        if (formElement != null) {
            List<Chart.DialogPage> pages = Arrays.asList(new Chart.DialogPage[] {
                    new Chart.DialogPage("PROPERTIES", "Properties", formElement.getTextContent()) });
            chart.setDialogPages(pages);
        }
        else {
            ArrayList<Chart.DialogPage> pages = new ArrayList<>();

            List<Element> pageNodes = XmlUtils.getChildElementsWithTag(chartNode, "dialogPage");
            for (Element pageNode : pageNodes) {
                String id = pageNode.getAttribute("id");
                String label = pageNode.getAttribute("label");
                String xswtForm = extractCdataOrTextContent(pageNode);
                Chart.DialogPage page = new Chart.DialogPage(id, label, xswtForm);
                pages.add(page);
            }
            chart.setDialogPages(pages);
        }

        List<Element> propNodes = XmlUtils.getChildElementsWithTag(chartNode, "property");
        for (Element propNode : propNodes) {
            String name = propNode.getAttribute("name");
            String value = propNode.getAttribute("value");
            Property prop = new Property(name, value);
            chart.addProperty(prop);
        }

        String resultTypesAttr = chartNode.getAttribute("resultTypes");
        if (!resultTypesAttr.isEmpty())
            chart.setSupportedResultTypes(ScaveModelUtil.parseResultTypes(resultTypesAttr));

        String createdWithAttr = chartNode.getAttribute("createdWith");
        if (!createdWithAttr.isEmpty())
            chart.setCreatedWith(createdWithAttr);

        return chart;
    }

    protected static Folder loadFolder(Element folderNode, ChartTemplateRegistry chartTemplateRegistry) {
        Folder folder = new Folder();
        String idAttr = folderNode.getAttribute("id");
        if (!idAttr.isEmpty())
            folder.setIdIfUnique(Integer.parseInt(idAttr));
        folder.setName(folderNode.getAttribute("name"));

        loadAnalysisItems(folderNode, folder, chartTemplateRegistry);

        return folder;
    }

}
