/*
 * 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.eclipse.aether.internal.test.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.DefaultDependencyNode;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.version.InvalidVersionSpecificationException;
import org.eclipse.aether.version.VersionScheme;

/**
 * Creates a dependency graph from a text description. <h2>Definition</h2> Each (non-empty) line in the input defines
 * one node of the resulting graph:
 *
 * <pre>
 * line      ::= (indent? ("(null)" | node | reference))? comment?
 * comment   ::= "#" rest-of-line
 * indent    ::= "|  "*  ("+" | "\\") "- "
 * reference ::= "^" id
 * node      ::= coords (range)? space (scope("&lt;" premanagedScope)?)? space "optional"? space
 *                  ("relocations=" coords ("," coords)*)? ("(" id ")")?
 * coords    ::= groupId ":" artifactId (":" extension (":" classifier)?)? ":" version
 * </pre>
 *
 * The special token {@code (null)} may be used to indicate an "empty" root node with no dependency.
 * <p>
 * If {@code indent} is empty, the line defines the root node. Only one root node may be defined. The level is
 * calculated by the distance from the beginning of the line. One level is three characters of indentation.
 * <p>
 * The {@code ^id} syntax allows to reuse a previously built node to share common sub graphs among different parent
 * nodes.
 * <h2>Example</h2>
 *
 * <pre>
 * gid:aid:ver
 * +- gid:aid2:ver scope
 * |  \- gid:aid3:ver        (id1)    # assign id for reference below
 * +- gid:aid4:ext:ver scope
 * \- ^id1                            # reuse previous node
 * </pre>
 *
 * <h2>Multiple definitions in one resource</h2>
 * <p>
 * By using {@link #parseMultiResource(String)}, definitions divided by a line beginning with "---" can be read from the
 * same resource. The rest of the line is ignored.
 * <h2>Substitutions</h2>
 * <p>
 * You may define substitutions (see {@link #setSubstitutions(String...)},
 * {@link #DependencyGraphParser(String, Collection)}). Every '%s' in the definition will be substituted by the next
 * String in the defined substitutions.
 * <h3>Example</h3>
 *
 * <pre>
 * parser.setSubstitutions( &quot;foo&quot;, &quot;bar&quot; );
 * String def = &quot;gid:%s:ext:ver\n&quot; + &quot;+- gid:%s:ext:ver&quot;;
 * </pre>
 *
 * The first node will have "foo" as its artifact id, the second node (child to the first) will have "bar" as its
 * artifact id.
 */
public class DependencyGraphParser {

    private final VersionScheme versionScheme;

    private final String prefix;

    private Collection<String> substitutions;

    /**
     * Create a parser with the given prefix and the given substitution strings.
     *
     * @see DependencyGraphParser#parseResource(String)
     */
    public DependencyGraphParser(String prefix, Collection<String> substitutions) {
        this.prefix = prefix;
        this.substitutions = substitutions;
        versionScheme = new TestVersionScheme();
    }

    /**
     * Create a parser with the given prefix.
     *
     * @see DependencyGraphParser#parseResource(String)
     */
    public DependencyGraphParser(String prefix) {
        this(prefix, Collections.emptyList());
    }

    /**
     * Create a parser with an empty prefix.
     */
    public DependencyGraphParser() {
        this("");
    }

    /**
     * Parse the given graph definition.
     */
    public DependencyNode parseLiteral(String dependencyGraph) throws IOException {
        BufferedReader reader = new BufferedReader(new StringReader(dependencyGraph));
        DependencyNode node = parse(reader);
        reader.close();
        return node;
    }

    /**
     * Parse the graph definition read from the given classpath resource. If a prefix is set, this method will load the
     * resource from 'prefix + resource'.
     */
    public DependencyNode parseResource(String resource) throws IOException {
        URL res = this.getClass().getClassLoader().getResource(prefix + resource);
        if (res == null) {
            throw new IOException("Could not find classpath resource " + prefix + resource);
        }
        return parse(res);
    }

    /**
     * Parse multiple graphs in one resource, divided by "---".
     */
    public List<DependencyNode> parseMultiResource(String resource) throws IOException {
        URL res = this.getClass().getClassLoader().getResource(prefix + resource);
        if (res == null) {
            throw new IOException("Could not find classpath resource " + prefix + resource);
        }

        BufferedReader reader = new BufferedReader(new InputStreamReader(res.openStream(), StandardCharsets.UTF_8));

        List<DependencyNode> ret = new ArrayList<>();
        DependencyNode root = null;
        while ((root = parse(reader)) != null) {
            ret.add(root);
        }
        return ret;
    }

    /**
     * Parse the graph definition read from the given URL.
     */
    public DependencyNode parse(URL resource) throws IOException {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8));
            return parse(reader);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                    reader = null;
                }
            } catch (final IOException e) {
                // Suppressed due to an exception already thrown in the try block.
            }
        }
    }

    private DependencyNode parse(BufferedReader in) throws IOException {
        Iterator<String> substitutionIterator = (substitutions != null) ? substitutions.iterator() : null;

        String line = null;

        DependencyNode root = null;
        DependencyNode node = null;
        int prevLevel = 0;

        Map<String, DependencyNode> nodes = new HashMap<>();
        LinkedList<DependencyNode> stack = new LinkedList<>();
        boolean isRootNode = true;

        while ((line = in.readLine()) != null) {
            line = cutComment(line);

            if (isEmpty(line)) {
                // skip empty line
                continue;
            }

            if (isEOFMarker(line)) {
                // stop parsing
                break;
            }

            while (line.contains("%s")) {
                if (!substitutionIterator.hasNext()) {
                    throw new IllegalStateException("not enough substitutions to fill placeholders");
                }
                line = line.replaceFirst("%s", substitutionIterator.next());
            }

            LineContext ctx = createContext(line);
            if (prevLevel < ctx.getLevel()) {
                // previous node is new parent
                stack.add(node);
            }

            // get to real parent
            while (prevLevel > ctx.getLevel()) {
                stack.removeLast();
                prevLevel -= 1;
            }

            prevLevel = ctx.getLevel();

            if (ctx.getDefinition() != null && ctx.getDefinition().reference != null) {
                String reference = ctx.getDefinition().reference;
                DependencyNode child = nodes.get(reference);
                if (child == null) {
                    throw new IllegalStateException("undefined reference " + reference);
                }
                node.getChildren().add(child);
            } else {

                node = build(isRootNode ? null : stack.getLast(), ctx, isRootNode);

                if (isRootNode) {
                    root = node;
                    isRootNode = false;
                }

                if (ctx.getDefinition() != null && ctx.getDefinition().id != null) {
                    nodes.put(ctx.getDefinition().id, node);
                }
            }
        }

        return root;
    }

    private boolean isEOFMarker(String line) {
        return line.startsWith("---");
    }

    private static boolean isEmpty(String line) {
        return line == null || line.isEmpty();
    }

    private static String cutComment(String line) {
        int idx = line.indexOf('#');

        if (idx != -1) {
            line = line.substring(0, idx);
        }

        return line;
    }

    private DependencyNode build(DependencyNode parent, LineContext ctx, boolean isRoot) {
        NodeDefinition def = ctx.getDefinition();
        if (!isRoot && parent == null) {
            throw new IllegalStateException("dangling node: " + def);
        } else if (ctx.getLevel() == 0 && parent != null) {
            throw new IllegalStateException("inconsistent leveling (parent for level 0?): " + def);
        }

        DefaultDependencyNode node;
        if (def != null) {
            DefaultArtifact artifact = new DefaultArtifact(def.coords, def.properties);
            Dependency dependency = new Dependency(artifact, def.scope, def.optional);
            node = new DefaultDependencyNode(dependency);
            int managedBits = 0;
            if (def.premanagedScope != null) {
                managedBits |= DependencyNode.MANAGED_SCOPE;
                node.setData("premanaged.scope", def.premanagedScope);
            }
            if (def.premanagedVersion != null) {
                managedBits |= DependencyNode.MANAGED_VERSION;
                node.setData("premanaged.version", def.premanagedVersion);
            }
            node.setManagedBits(managedBits);
            if (def.relocations != null) {
                List<Artifact> relocations = new ArrayList<>();
                for (String relocation : def.relocations) {
                    relocations.add(new DefaultArtifact(relocation));
                }
                node.setRelocations(relocations);
            }
            try {
                node.setVersion(versionScheme.parseVersion(artifact.getVersion()));
                node.setVersionConstraint(
                        versionScheme.parseVersionConstraint(def.range != null ? def.range : artifact.getVersion()));
            } catch (InvalidVersionSpecificationException e) {
                throw new IllegalArgumentException("bad version: " + e.getMessage(), e);
            }
        } else {
            node = new DefaultDependencyNode((Dependency) null);
        }

        if (parent != null) {
            parent.getChildren().add(node);
        }

        return node;
    }

    public String dump(DependencyNode root) {
        StringBuilder ret = new StringBuilder();

        List<NodeEntry> entries = new ArrayList<>();

        addNode(root, 0, entries);

        for (NodeEntry nodeEntry : entries) {
            char[] level = new char[(nodeEntry.getLevel() * 3)];
            Arrays.fill(level, ' ');

            if (level.length != 0) {
                level[level.length - 3] = '+';
                level[level.length - 2] = '-';
            }

            String definition = nodeEntry.getDefinition();

            ret.append(level).append(definition).append("\n");
        }

        return ret.toString();
    }

    private void addNode(DependencyNode root, int level, List<NodeEntry> entries) {

        NodeEntry entry = new NodeEntry();
        Dependency dependency = root.getDependency();
        StringBuilder defBuilder = new StringBuilder();
        if (dependency == null) {
            defBuilder.append("(null)");
        } else {
            Artifact artifact = dependency.getArtifact();

            defBuilder
                    .append(artifact.getGroupId())
                    .append(":")
                    .append(artifact.getArtifactId())
                    .append(":")
                    .append(artifact.getExtension())
                    .append(":")
                    .append(artifact.getVersion());
            if (dependency.getScope() != null && (!"".equals(dependency.getScope()))) {
                defBuilder.append(":").append(dependency.getScope());
            }

            Map<String, String> properties = artifact.getProperties();
            if (!(properties == null || properties.isEmpty())) {
                for (Map.Entry<String, String> prop : properties.entrySet()) {
                    defBuilder.append(";").append(prop.getKey()).append("=").append(prop.getValue());
                }
            }
        }

        entry.setDefinition(defBuilder.toString());
        entry.setLevel(level++);

        entries.add(entry);

        for (DependencyNode node : root.getChildren()) {
            addNode(node, level, entries);
        }
    }

    private static class NodeEntry {
        int level;

        String definition;

        Map<String, String> properties;

        public int getLevel() {
            return level;
        }

        public void setLevel(int level) {
            this.level = level;
        }

        public String getDefinition() {
            return definition;
        }

        public void setDefinition(String definition) {
            this.definition = definition;
        }

        public Map<String, String> getProperties() {
            return properties;
        }

        public void setProperties(Map<String, String> properties) {
            this.properties = properties;
        }
    }

    private static LineContext createContext(String line) {
        LineContext ctx = new LineContext();
        String definition;

        String[] split = line.split("- ");
        if (split.length == 1) // root
        {
            ctx.setLevel(0);
            definition = split[0];
        } else {
            ctx.setLevel((int) Math.ceil((double) split[0].length() / (double) 3));
            definition = split[1];
        }

        if ("(null)".equalsIgnoreCase(definition)) {
            return ctx;
        }

        ctx.setDefinition(new NodeDefinition(definition));

        return ctx;
    }

    static class LineContext {
        NodeDefinition definition;

        int level;

        public NodeDefinition getDefinition() {
            return definition;
        }

        public void setDefinition(NodeDefinition definition) {
            this.definition = definition;
        }

        public int getLevel() {
            return level;
        }

        public void setLevel(int level) {
            this.level = level;
        }
    }

    public Collection<String> getSubstitutions() {
        return substitutions;
    }

    public void setSubstitutions(Collection<String> substitutions) {
        this.substitutions = substitutions;
    }

    public void setSubstitutions(String... substitutions) {
        setSubstitutions(Arrays.asList(substitutions));
    }
}
