/*
 * 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.paimon.tools.ci.utils.dependency;

import org.apache.paimon.tools.ci.utils.shared.Dependency;
import org.apache.paimon.tools.ci.utils.shared.DependencyTree;
import org.apache.paimon.tools.ci.utils.shared.ParserUtils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/** Parsing utils for the Maven dependency plugin. */
public class DependencyParser {

    private static final Pattern DEPENDENCY_COPY_NEXT_MODULE_PATTERN =
            Pattern.compile(
                    ".*maven-dependency-plugin:[^:]+:copy .* @ (?<module>[^ _]+)(?:_[0-9.]+)? --.*");

    private static final Pattern DEPENDENCY_TREE_NEXT_MODULE_PATTERN =
            Pattern.compile(
                    ".*maven-dependency-plugin:[^:]+:tree .* @ (?<module>[^ _]+)(?:_[0-9.]+)? --.*");

    /** See {@link DependencyParserTreeTest} for examples. */
    private static final Pattern DEPENDENCY_TREE_ITEM_PATTERN =
            Pattern.compile(
                    ".* +"
                            + "(?<groupId>.*?):"
                            + "(?<artifactId>.*?):"
                            + "(?<type>.*?):"
                            + "(?:(?<classifier>.*?):)?"
                            + "(?<version>.*?):"
                            + "(?<scope>[^ ]*)"
                            + "(?<optional> \\(optional\\))?");

    /** See {@link DependencyParserCopyTest} for examples. */
    private static final Pattern DEPENDENCY_COPY_ITEM_PATTERN =
            Pattern.compile(
                    ".* Configured Artifact: +"
                            + "(?<groupId>.*?):"
                            + "(?<artifactId>.*?):"
                            + "(?:(?<classifier>.*?):)?"
                            + "(?<version>.*?):"
                            + "(?:\\?:)?" // unknown cause; e.g.: javax.xml.bind:jaxb-api:?:jar
                            + "(?<type>.*)");

    /**
     * Parses the output of a Maven build where {@code dependency:copy} was used, and returns a set
     * of copied dependencies for each module.
     *
     * <p>The returned dependencies will NEVER contain the scope or optional flag.
     */
    public static Map<String, Set<Dependency>> parseDependencyCopyOutput(Path buildOutput)
            throws IOException {
        return processLines(buildOutput, DependencyParser::parseDependencyCopyOutput);
    }

    /**
     * Parses the output of a Maven build where {@code dependency:tree} was used, and returns a set
     * of dependencies for each module.
     */
    public static Map<String, DependencyTree> parseDependencyTreeOutput(Path buildOutput)
            throws IOException {
        return processLines(buildOutput, DependencyParser::parseDependencyTreeOutput);
    }

    private static <X> X processLines(Path buildOutput, Function<Stream<String>, X> processor)
            throws IOException {
        try (Stream<String> lines = Files.lines(buildOutput)) {
            return processor.apply(lines.filter(line -> line.contains("[INFO]")));
        }
    }

    static Map<String, Set<Dependency>> parseDependencyCopyOutput(Stream<String> lines) {
        return ParserUtils.parsePluginOutput(
                lines,
                DEPENDENCY_COPY_NEXT_MODULE_PATTERN,
                DependencyParser::parseCopyDependencyBlock);
    }

    static Map<String, DependencyTree> parseDependencyTreeOutput(Stream<String> lines) {
        return ParserUtils.parsePluginOutput(
                lines,
                DEPENDENCY_TREE_NEXT_MODULE_PATTERN,
                DependencyParser::parseTreeDependencyBlock);
    }

    private static Set<Dependency> parseCopyDependencyBlock(Iterator<String> block) {
        final Set<Dependency> dependencies = new LinkedHashSet<>();

        Optional<Dependency> parsedDependency = parseCopyDependency(block.next());
        while (parsedDependency.isPresent()) {
            dependencies.add(parsedDependency.get());

            if (block.hasNext()) {
                parsedDependency = parseCopyDependency(block.next());
            } else {
                parsedDependency = Optional.empty();
            }
        }

        return dependencies;
    }

    private static DependencyTree parseTreeDependencyBlock(Iterator<String> block) {
        // discard one line, which only contains the current module name
        block.next();

        if (!block.hasNext()) {
            throw new IllegalStateException("Expected more output from the dependency-plugin.");
        }

        final DependencyTree dependencies = new DependencyTree();

        final Stack<Dependency> parentStack = new Stack<>();
        final Stack<Integer> treeDepthStack = new Stack<>();
        String line = block.next();
        Optional<Dependency> parsedDependency = parseTreeDependency(line);
        while (parsedDependency.isPresent()) {
            int treeDepth = getDepth(line);

            while (!treeDepthStack.isEmpty() && treeDepth <= treeDepthStack.peek()) {
                parentStack.pop();
                treeDepthStack.pop();
            }

            final Dependency dependency = parsedDependency.get();

            if (parentStack.isEmpty()) {
                dependencies.addDirectDependency(dependency);
            } else {
                dependencies.addTransitiveDependencyTo(dependency, parentStack.peek());
            }

            if (treeDepthStack.isEmpty() || treeDepth > treeDepthStack.peek()) {
                treeDepthStack.push(treeDepth);
                parentStack.push(dependency);
            }

            if (block.hasNext()) {
                line = block.next();
                parsedDependency = parseTreeDependency(line);
            } else {
                parsedDependency = Optional.empty();
            }
        }

        return dependencies;
    }

    static Optional<Dependency> parseCopyDependency(String line) {
        Matcher dependencyMatcher = DEPENDENCY_COPY_ITEM_PATTERN.matcher(line);
        if (!dependencyMatcher.find()) {
            return Optional.empty();
        }

        return Optional.of(
                Dependency.create(
                        dependencyMatcher.group("groupId"),
                        dependencyMatcher.group("artifactId"),
                        dependencyMatcher.group("version"),
                        dependencyMatcher.group("classifier")));
    }

    static Optional<Dependency> parseTreeDependency(String line) {
        Matcher dependencyMatcher = DEPENDENCY_TREE_ITEM_PATTERN.matcher(line);
        if (!dependencyMatcher.find()) {
            return Optional.empty();
        }

        return Optional.of(
                Dependency.create(
                        dependencyMatcher.group("groupId"),
                        dependencyMatcher.group("artifactId"),
                        dependencyMatcher.group("version"),
                        dependencyMatcher.group("classifier"),
                        dependencyMatcher.group("scope"),
                        dependencyMatcher.group("optional") != null));
    }

    /**
     * The depths returned by this method do NOT return a continuous sequence.
     *
     * <pre>
     * +- org.apache.paimon:...
     * |  +- org.apache.paimon:...
     * |  |  \- org.apache.paimon:...
     * ...
     * </pre>
     */
    private static int getDepth(String line) {
        final int level = line.indexOf('+');
        if (level != -1) {
            return level;
        }
        return line.indexOf('\\');
    }
}
