/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      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.apache.camel.parser.helper;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.parser.model.CoverageData;

/**
 * Helper to provide route coverage details.
 */
public final class RouteCoverageHelper {

    private RouteCoverageHelper() {
    }

    /**
     * Parses the dumped route coverage data and creates a line by line coverage data
     *
     * @param  directory the directory with the dumped route coverage data
     * @param  routeId   the route id to gather, must not be null.
     * @return           line by line coverage data
     */
    public static List<CoverageData> parseDumpRouteCoverageByRouteId(String directory, String routeId) throws Exception {
        List<CoverageData> answer = new ArrayList<>();

        if (routeId == null) {
            return answer;
        }
        File[] files = new File(directory).listFiles(f -> f.getName().endsWith(".xml"));
        if (files == null) {
            return answer;
        }

        CamelCatalog catalog = new DefaultCamelCatalog(true);

        for (File file : files) {
            try (FileInputStream fis = new FileInputStream(file)) {
                Document dom = XmlLineNumberParser.parseXml(fis);
                NodeList routes = dom.getElementsByTagName("route");
                for (int i = 0; i < routes.getLength(); i++) {
                    Node route = routes.item(i);
                    Node n = route.getAttributes().getNamedItem("id");
                    String id = n != null ? n.getNodeValue() : null;
                    if (routeId.equals(id)) {
                        // parse each route and build a List<CoverageData> for line by line coverage data
                        AtomicInteger counter = new AtomicInteger();
                        parseRouteData(catalog, route, answer, counter, routeId);
                    }
                }
            }
        }

        return answer;
    }

    /**
     * Parses the dumped route coverage data and creates a line by line coverage data
     *
     * @param  directory      the directory with the dumped route coverage data
     * @param  sourceLocation the location of the route (filename:line)
     * @return                line by line coverage data
     */
    public static List<CoverageData> parseDumpRouteCoverageByLineNumber(String directory, String sourceLocation)
            throws Exception {
        List<CoverageData> answer = new ArrayList<>();

        if (sourceLocation == null) {
            return answer;
        }

        File[] files = new File(directory).listFiles(f -> f.getName().endsWith(".xml"));
        if (files == null) {
            return answer;
        }

        CamelCatalog catalog = new DefaultCamelCatalog(true);

        for (File file : files) {
            try (FileInputStream fis = new FileInputStream(file)) {
                Document dom = XmlLineNumberParser.parseXml(fis);
                NodeList routes = dom.getElementsByTagName("route");
                for (int i = 0; i < routes.getLength(); i++) {
                    Node route = routes.item(i);
                    Node n = route.getAttributes().getNamedItem("id");
                    String id = n != null ? n.getNodeValue() : null;
                    n = route.getAttributes().getNamedItem("sourceLocation");
                    String loc = n != null ? n.getNodeValue() : null;
                    if (sourceLocation.equals(loc)) {
                        // parse each route and build a List<CoverageData> for line by line coverage data
                        AtomicInteger counter = new AtomicInteger();
                        List<CoverageData> list = new ArrayList<>();
                        parseRouteData(catalog, route, list, counter, id);
                        if (id != null && !list.isEmpty()) {
                            list.get(0).setRouteId(id);
                        }
                        answer.addAll(list);
                    }
                }
            }
        }

        return answer;
    }

    public static Map<String, List<CoverageData>> parseDumpRouteCoverageByClassAndTestMethod(String directory)
            throws Exception {
        Map<String, List<CoverageData>> answer = new LinkedHashMap<>();

        File[] files = new File(directory).listFiles(f -> f.getName().endsWith(".xml"));
        if (files == null) {
            return answer;
        }

        CamelCatalog catalog = new DefaultCamelCatalog(true);

        for (File file : files) {
            try (FileInputStream fis = new FileInputStream(file)) {
                Document dom = XmlLineNumberParser.parseXml(fis);
                NodeList routes = dom.getElementsByTagName("route");
                for (int i = 0; i < routes.getLength(); i++) {
                    Node route = routes.item(i);
                    Node n = route.getAttributes().getNamedItem("id");
                    String id = n != null ? n.getNodeValue() : null;
                    // parse each route and build a List<CoverageData> for line by line coverage data
                    AtomicInteger counter = new AtomicInteger();
                    List<CoverageData> data = new ArrayList<>();
                    parseRouteData(catalog, route, data, counter, id);
                    // create a key which is based on the file name without extension
                    String key = file.getName();
                    // strip .xml extension
                    key = key.substring(0, key.length() - 4);
                    // is there existing data
                    List<CoverageData> existing = answer.get(key);
                    if (existing != null) {
                        existing.addAll(data);
                    } else {
                        answer.put(key, data);
                    }
                }
            }
        }

        return answer;
    }

    private static void parseRouteData(
            CamelCatalog catalog, Node node, List<CoverageData> data, AtomicInteger counter, String routeId) {
        // must be a known EIP model
        String key = node.getNodeName();
        boolean valid = catalog.findModelNames().contains(key); // skip route as we use from instead
        if (!valid) {
            return;
        }

        // inlined error handler, on completion etc should be skipped (and currently not supported in route coverage)
        boolean skip = "onException".equals(key) || "onCompletion".equals(key)
                || "intercept".equals(key) || "interceptFrom".equals(key) || "interceptSendToEndpoint".equals(key);

        if (skip) {
            return;
        }

        // only calculate for elements within the route or children of policy/transaction
        if (!"route".equals(key) && !"policy".equals(key) && !"transacted".equals(key)) {
            int count = 0;
            Node total = node.getAttributes().getNamedItem("exchangesTotal");
            if (total != null) {
                count = Integer.parseInt(total.getNodeValue());
            }
            CoverageData holder = data.size() > counter.get() ? data.get(counter.get()) : null;
            if (holder != null && holder.getNode().equals(key)) {
                count += holder.getCount();
            }

            String id = null;
            Node nid = node.getAttributes().getNamedItem("id");
            if (nid != null) {
                id = nid.getNodeValue();
            }
            int lineNumber = 0;
            Node ln = node.getAttributes().getNamedItem("sourceLineNumber");
            if (ln != null) {
                lineNumber = Integer.parseInt(ln.getNodeValue());
            }

            CoverageData cd = new CoverageData(id, key, count, routeId);
            cd.setLineNumber(lineNumber);
            if (holder == null) {
                // add new
                data.add(counter.get(), cd);
            } else {
                // replace existing
                data.set(counter.get(), cd);
            }
            // advance counter
            counter.incrementAndGet();
        }

        // any children
        NodeList children = node.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if (child instanceof Element) {
                parseRouteData(catalog, child, data, counter, routeId);
            }
        }
    }

}
