/**
 *  Copyright 2019-2022 The ModiTect authors
 *
 *  Licensed 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.moditect.deptective.internal.handler;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.tools.FileObject;
import javax.tools.JavaFileManager;
import javax.tools.StandardLocation;

import org.moditect.deptective.internal.export.DotSerializer;
import org.moditect.deptective.internal.graph.Cycle;
import org.moditect.deptective.internal.graph.GraphUtils;
import org.moditect.deptective.internal.log.DeptectiveMessages;
import org.moditect.deptective.internal.log.Log;
import org.moditect.deptective.internal.model.Component;
import org.moditect.deptective.internal.model.IdentifiableComponent;
import org.moditect.deptective.internal.model.PackageAssignedToMultipleComponentsException;
import org.moditect.deptective.internal.model.PackageDependencies;
import org.moditect.deptective.internal.model.ReadKind;
import org.moditect.deptective.internal.options.ReportingPolicy;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;

/**
 * Validates a project's package relationships against a given description of allowed references in
 * {@code deptective.json}.
 *
 * @author Gunnar Morling
 */
public class PackageReferenceValidator implements PackageReferenceHandler {

    private final Log log;
    private final PackageDependencies allowedPackageDependencies;
    private final JavaFileManager jfm;
    private final ReportingPolicy reportingPolicy;
    private final ReportingPolicy unconfiguredPackageReportingPolicy;
    private final ReportingPolicy cycleReportingPolicy;
    private final Map<String, Boolean> reportedUnconfiguredPackages;
    private final PackageDependencies.Builder actualPackageDependencies;

    private boolean createDotFile;
    private String currentPackageName;
    private Component currentComponent;

    public PackageReferenceValidator(JavaFileManager jfm, PackageDependencies packageDependencies,
            ReportingPolicy reportingPolicy, ReportingPolicy unconfiguredPackageReportingPolicy,
            ReportingPolicy cycleReportingPolicy, boolean createDotFile,
            Log log) {
        this.log = log;
        this.allowedPackageDependencies = packageDependencies;
        this.jfm = jfm;
        this.reportingPolicy = reportingPolicy;
        this.unconfiguredPackageReportingPolicy = unconfiguredPackageReportingPolicy;
        this.cycleReportingPolicy = cycleReportingPolicy;
        this.reportedUnconfiguredPackages = new HashMap<>();
        this.actualPackageDependencies = PackageDependencies.builder();
        this.createDotFile = createDotFile;
    }

    @Override
    public boolean configIsValid() {
        if (allowedPackageDependencies == null) {
            log.report(ReportingPolicy.ERROR, DeptectiveMessages.NO_DEPTECTIVE_CONFIG_FOUND);
            return false;
        }

        return true;
    }

    @Override
    public boolean onEnteringCompilationUnit(CompilationUnitTree tree) {
        ExpressionTree packageNameTree = tree.getPackageName();

        // TODO deal with default package
        if (packageNameTree == null) {
            return false;
        }

        String packageName = packageNameTree.toString();
        currentPackageName = packageName;

        try {
            currentComponent = allowedPackageDependencies.getComponentByPackage(packageName);

            if (currentComponent == null) {
                reportUnconfiguredPackageIfNeeded(tree, packageName);
            }
        }
        catch (PackageAssignedToMultipleComponentsException e) {
            log.report(
                    ReportingPolicy.ERROR,
                    DeptectiveMessages.PACKAGE_CONTAINED_IN_MULTIPLE_COMPONENTS,
                    String.join(
                            ", ",
                            e.getMatchingComponents()
                                    .stream()
                                    .map(Component::getName)
                                    .sorted()
                                    .collect(Collectors.toList())
                    ),
                    packageName
            );

            createDotFile = false;
            return false;
        }

        return true;
    }

    @Override
    public void onPackageReference(Tree referencingNode, String referencedPackageName) {
        if (isIgnoredDependency(referencedPackageName)) {
            return;
        }

        Component referencedComponent = allowedPackageDependencies.getComponentByPackage(referencedPackageName);

        if (referencedComponent == null) {
            referencedComponent = Component.builder(referencedPackageName).build();
        }

        if (currentComponent == null) {
            actualPackageDependencies.addRead(
                    Component.builder(currentPackageName).build().getName(),
                    referencedComponent.getName(),
                    ReadKind.UKNOWN
            );
        }
        else if (currentComponent.allowedToRead(referencedComponent)) {
            actualPackageDependencies.addRead(
                    currentComponent.getName(),
                    referencedComponent.getName(),
                    ReadKind.ALLOWED
            );
        }
        else {
            actualPackageDependencies.addRead(
                    currentComponent.getName(),
                    referencedComponent.getName(),
                    ReadKind.DISALLOWED
            );

            log.report(
                    reportingPolicy,
                    (com.sun.tools.javac.tree.JCTree) referencingNode,
                    DeptectiveMessages.ILLEGAL_PACKAGE_DEPENDENCY,
                    currentComponent.getName(),
                    referencedPackageName
            );
        }
    }

    @Override
    public void onCompletingCompilation() {
        log.useSource(null);

        List<Cycle<IdentifiableComponent>> cycles = GraphUtils.detectCycles(allowedPackageDependencies.getComponents());

        if (!cycles.isEmpty()) {
            String cyclesAsString = "- " + cycles.stream()
                    .map(Cycle::toString)
                    .collect(Collectors.joining("," + System.lineSeparator() + "- "));

            log.report(cycleReportingPolicy, DeptectiveMessages.CYCLE_IN_ARCHITECTURE, cyclesAsString);
        }

        if (!createDotFile) {
            return;
        }

        actualPackageDependencies.updateFromCycles(cycles);

        DotSerializer serializer = new DotSerializer();
        actualPackageDependencies.build().serialize(serializer);

        try {
            FileObject output = jfm.getFileForOutput(StandardLocation.SOURCE_OUTPUT, "", "deptective.dot", null);
            log.note(DeptectiveMessages.GENERATED_DOT_REPRESENTATION, output.toUri());
            Writer writer = output.openWriter();
            writer.append(serializer.serialize());
            writer.close();
        }
        catch (IOException e) {
            throw new RuntimeException("Failed to write deptective.dot file", e);
        }
    }

    private boolean isIgnoredDependency(String referencedPackageName) {
        return "java.lang".equals(referencedPackageName) ||
                allowedPackageDependencies.isWhitelisted(referencedPackageName) ||
                currentPackageName.equals(referencedPackageName) ||
                referencedPackageName.isEmpty();
    }

    private void reportUnconfiguredPackageIfNeeded(CompilationUnitTree tree, String packageName) {
        boolean reportedBefore = Boolean.TRUE.equals(reportedUnconfiguredPackages.get(packageName));

        if (!reportedBefore) {
            log.report(
                    unconfiguredPackageReportingPolicy,
                    (com.sun.tools.javac.tree.JCTree) tree,
                    DeptectiveMessages.PACKAGE_NOT_CONFIGURED, packageName
            );

            reportedUnconfiguredPackages.put(packageName, true);
        }
    }
}
