/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

package org.kie.server.router.proxy.aggragate;

import static org.kie.server.router.utils.Helper.read;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringReader;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.jboss.logging.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


public abstract class XMLResponseAggregator implements ResponseAggregator {

    private static final Logger log = Logger.getLogger(XMLResponseAggregator.class);

    private static final String SORT_XSLT = read(XMLResponseAggregator.class.getResourceAsStream("/sort.xsl"));
    private static final String PAGE_XSLT = read(XMLResponseAggregator.class.getResourceAsStream("/page.xsl"));

    public XMLResponseAggregator() {
    }

    protected boolean supports(String expectedType, Object... acceptTypes) {
        for (Object acceptType : acceptTypes ) {
            if (acceptType == null) {
                continue;
            }
            boolean found = acceptType.toString().toLowerCase().contains(expectedType);
            if (found) {
                return true;
            }
        }

        return false;
    }

    public String aggregate(List<String> data) {
        try {
            if (data == null || data.isEmpty()) {
                return null;
            }
            List<String> nodes = knownNames();
            Document document = data.stream().map(xml -> {
                return newDoc(xml);
            })
                    .filter(d -> d != null)
                    .reduce((source, target) -> {
                        deepMerge(source, target, nodes, target);
                        return target;
                    }).get();

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            Result output = new StreamResult(out);
            Source input = new DOMSource(document);

            transformer.transform(input, output);

            return new String(out.toByteArray());
        } catch (Exception e) {
            log.errorf("Failed to aggregate xml responses of %s", data, e);
            throw new RuntimeException(e);
        }
    }

    @Override
    public String aggregate(List<String> data, String sortBy, boolean ascending, Integer page, Integer pageSize) {
        try {
            if (data == null || data.isEmpty()) {
                return null;
            }

            List<String> nodes = knownNames();
            Document document = data.stream().map(xml -> {
                return newDoc(xml);
            })
                    .filter(d -> d != null)
                    .reduce((source, target) -> {
                        deepMerge(source, target, nodes, target);
                        return target;
                    }).get();

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Transformer transformer = null;

            String root = document.getDocumentElement().getNodeName();
            String sortNode = getElementLevel(root);
            if (sortBy != null && !sortBy.trim().isEmpty()) {

                transformer = sort(getRootNode(root), sortNode, sortBy, ascending, document);

                DOMResult toutput = new DOMResult();
                Source input = new DOMSource(document);
                transformer.transform(input, toutput);

                document = (Document) toutput.getNode();

            }
            transformer = page(getRootNode(root), sortNode, page, pageSize, document);

            Result output = new StreamResult(out);
            transformer.transform(new DOMSource(document), output);

            return new String(out.toByteArray());
        } catch (Exception e) {
            log.errorf("Failed to aggregate xml responses of %s", data, e);
            throw new RuntimeException(e);
        }
    }

    protected Transformer sort(String root, String level, String fieldName, boolean ascending, Document source) throws Exception {
        String order = "ascending";
        if (!ascending) {
            order = "descending";
        }

        StreamSource xsltSort = new StreamSource(new StringReader(SORT_XSLT
                .replaceAll("KIE_ROOT", root)
                .replaceAll("LEVEL", level)
                .replaceAll("KIE_SORT_BY", sortBy(fieldName))
                .replaceAll("KIE_ORDER", order)));

        return TransformerFactory.newInstance().newTransformer(xsltSort);

    }

    protected Transformer page(String root, String node, Integer start, Integer size, Document source) throws Exception {

        Integer actualStart = start;
        Integer actualSize = size;
        if (actualStart > 0) {
            actualStart = actualStart * size;

            actualSize = actualStart + size;
        }

        log.debug("Start " + actualStart + " size " + actualSize);
        StreamSource xsltSort = new StreamSource(new StringReader(PAGE_XSLT
                .replaceAll("LEVEL", node)
                .replaceAll("START", actualStart.toString())
                .replaceAll("SIZE", actualSize.toString())));

        return TransformerFactory.newInstance().newTransformer(xsltSort);

    }

    public Document newDoc(String xml) {
        try (ByteArrayInputStream stream = new ByteArrayInputStream(xml.getBytes("UTF-8"))) {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setNamespaceAware(true);
            Document doc = factory.newDocumentBuilder().parse(stream);
            return doc;
        } catch (Exception e) {
            log.errorf("Failed to create xml document of %s", xml, e);
            return null;
        }
    }

    protected void deepMerge(Node sourceNode, Node targetNode, List<String> nodes, Document target) {
        if (targetNode == null) {
            return;
        }
        NodeList sourceNodeList = sourceNode.getChildNodes();
        NodeList targetNodeList = targetNode.getChildNodes();

        for (int i = 0; i < sourceNodeList.getLength(); i++) {
            sourceNode = sourceNodeList.item(i);
            targetNode = targetNodeList.item(i);

            if (!nodes.contains(sourceNode.getNodeName())) {
                deepMerge(sourceNode, targetNode, nodes, target);
            }
            if (targetNode == null) {
                return;
            }
            // found correct node to copy it's content
            NodeList children = sourceNode.getChildNodes();
            copyNodes(children, target, targetNode);
        }
    }

    protected abstract void copyNodes(NodeList children, Document target, Node targetNode);

    protected abstract List<String> knownNames();

    protected abstract String getElementLevel(String rootNode);

    protected abstract String getRootNode(String rootNode);

    protected abstract String sortBy(String fieldName);

}
