/*
 * Copyright 2006-2011 Dave Griffith, Bas Leijdekkers
 *
 * 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 com.siyeh.ig.packaging;

import com.intellij.analysis.AnalysisScope;
import com.intellij.codeInspection.CommonProblemDescriptor;
import com.intellij.codeInspection.GlobalInspectionContext;
import com.intellij.codeInspection.InspectionManager;
import com.intellij.codeInspection.reference.RefClass;
import com.intellij.codeInspection.reference.RefEntity;
import com.intellij.codeInspection.reference.RefJavaUtil;
import com.intellij.codeInspection.reference.RefPackage;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseGlobalInspection;
import com.siyeh.ig.dependency.DependencyUtils;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public final class DisjointPackageInspection extends BaseGlobalInspection {

  @Override
  public CommonProblemDescriptor @Nullable [] checkElement(
    @NotNull RefEntity refEntity, @NotNull AnalysisScope analysisScope,
    @NotNull InspectionManager inspectionManager,
    @NotNull GlobalInspectionContext globalInspectionContext) {
    if (!(refEntity instanceof RefPackage refPackage)) {
      return null;
    }
    final Set<RefClass> childClasses = StreamEx.of(refPackage.getChildren()).select(RefClass.class).toSet();
    if (childClasses.isEmpty()) {
      return null;
    }
    final Set<Set<RefClass>> components =
      createComponents(refPackage, childClasses);
    if (components.size() == 1) {
      return null;
    }
    final String errorString = InspectionGadgetsBundle
      .message("disjoint.package.problem.descriptor",
               refPackage.getQualifiedName(),
               Integer.valueOf(components.size()));

    return new CommonProblemDescriptor[]{
      inspectionManager.createProblemDescriptor(errorString)};
  }

  private static Set<Set<RefClass>> createComponents(
    RefPackage aPackage, Set<RefClass> classes) {
    final Set<RefClass> allClasses = new HashSet<>(classes);
    final Set<Set<RefClass>> out = new HashSet<>();
    while (!allClasses.isEmpty()) {
      final RefClass seed = allClasses.iterator().next();
      allClasses.remove(seed);
      final Set<RefClass> currentComponent = new HashSet<>();
      currentComponent.add(seed);
      final List<RefClass> pendingClasses = new ArrayList<>();
      pendingClasses.add(seed);
      while (!pendingClasses.isEmpty()) {
        final RefClass classToProcess = pendingClasses.remove(0);
        final Set<RefClass> relatedClasses =
          getRelatedClasses(aPackage, classToProcess);
        for (RefClass relatedClass : relatedClasses) {
          if (!currentComponent.contains(relatedClass) &&
              !pendingClasses.contains(relatedClass)) {
            currentComponent.add(relatedClass);
            pendingClasses.add(relatedClass);
            allClasses.remove(relatedClass);
          }
        }
      }
      out.add(currentComponent);
    }
    return out;
  }

  private static Set<RefClass> getRelatedClasses(RefPackage aPackage,
                                                 RefClass classToProcess) {
    final Set<RefClass> out = new HashSet<>();
    final Set<RefClass> dependencies =
      DependencyUtils.calculateDependenciesForClass(classToProcess);
    for (RefClass dependency : dependencies) {
      if (packageContainsClass(aPackage, dependency)) {
        out.add(dependency);
      }
    }

    final Set<RefClass> dependents =
      DependencyUtils.calculateDependentsForClass(classToProcess);
    for (RefClass dependent : dependents) {
      if (packageContainsClass(aPackage, dependent)) {
        out.add(dependent);
      }
    }
    return out;
  }

  private static boolean packageContainsClass(RefPackage aPackage,
                                              RefClass aClass) {
    return aPackage.equals(RefJavaUtil.getPackage(aClass));
  }
}
