/*
 * Copyright 2006-2011 Dave Griffith
 *
 * 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.dependency;

import com.intellij.codeInspection.reference.RefClass;
import com.intellij.codeInspection.reference.RefElement;
import com.intellij.codeInspection.reference.RefEntity;
import com.intellij.codeInspection.reference.RefJavaUtil;
import com.intellij.openapi.util.Key;

import java.util.*;

final class InitializationDependencyUtils {

  private static final Key<Set<RefClass>> INITIALIZATION_DEPENDENT_CLASSES_KEY =
    new Key<>("INITIALIZATION_DEPENDENT_CLASSES");
  private static final Key<Set<RefClass>> INITIALIZATION_DEPENDENCY_CLASSES_KEY =
    new Key<>("INITIALIZATION_DEPENDENT_CLASSES");
  private static final Key<Set<RefClass>> TRANSITIVE_INITIALIZATION_DEPENDENT_CLASSES_KEY =
    new Key<>("TRANSITIVE_INITIALIZATION_DEPENDENT_CLASSES_KEY");
  private static final Key<Set<RefClass>> TRANSITIVE_INITIALIZATION_DEPENDENCY_CLASSES_KEY =
    new Key<>("TRANSITIVE_INITIALIZATION_DEPENDENCY_CLASSES_KEY");

  private InitializationDependencyUtils() {
  }

  public static Set<RefClass> calculateInitializationDependenciesForClass(
    RefClass refClass) {
    final Set<RefClass> dependencies =
      refClass.getUserData(INITIALIZATION_DEPENDENCY_CLASSES_KEY);
    if (dependencies != null) {
      return dependencies;
    }
    final Set<RefClass> newDependencies = new HashSet<>();
    tabulateInitializationDependencyClasses(refClass, newDependencies);
    newDependencies.remove(refClass);
    refClass.putUserData(INITIALIZATION_DEPENDENCY_CLASSES_KEY,
                         newDependencies);
    return newDependencies;
  }

  @SuppressWarnings({"MethodWithMultipleLoops"})
  static void tabulateInitializationDependencyClasses(
    RefElement element, Set<? super RefClass> dependencies) {
    final Collection<RefElement> references = element.getOutReferences();
    final RefJavaUtil refUtil = RefJavaUtil.getInstance();
    for (RefElement reference : references) {
      final RefClass refClass = refUtil.getTopLevelClass(reference);
      if (refClass != null) {
        dependencies.add(refClass);
      }
    }
    final List<RefEntity> children = element.getChildren();
    for (RefEntity child : children) {
      if (child instanceof RefElement) {
        tabulateInitializationDependencyClasses((RefElement)child,
                                                dependencies);
      }
    }
  }

  public static Set<RefClass> calculateTransitiveInitializationDependenciesForClass(
    RefClass refClass) {
    final Set<RefClass> dependencies =
      refClass.getUserData(TRANSITIVE_INITIALIZATION_DEPENDENCY_CLASSES_KEY);
    if (dependencies != null) {
      return dependencies;
    }
    final Set<RefClass> newDependencies = new HashSet<>();
    tabulateTransitiveInitializationDependencyClasses(refClass,
                                                      newDependencies);
    refClass.putUserData(TRANSITIVE_INITIALIZATION_DEPENDENCY_CLASSES_KEY,
                         newDependencies);
    return newDependencies;
  }

  private static void tabulateTransitiveInitializationDependencyClasses(
    RefClass refClass, Set<? super RefClass> newDependencies) {
    final LinkedList<RefClass> pendingClasses = new LinkedList<>();
    final Set<RefClass> processedClasses = new HashSet<>();
    pendingClasses.addLast(refClass);
    while (!pendingClasses.isEmpty()) {
      final RefClass classToProcess = pendingClasses.removeFirst();
      newDependencies.add(classToProcess);
      processedClasses.add(classToProcess);
      final Set<RefClass> dependencies =
        calculateInitializationDependenciesForClass(classToProcess);
      for (RefClass dependency : dependencies) {
        if (!pendingClasses.contains(dependency) &&
            !processedClasses.contains(dependency)) {
          pendingClasses.addLast(dependency);
        }
      }
    }
    newDependencies.remove(refClass);
  }


  public static Set<RefClass> calculateInitializationDependentsForClass(
    RefClass refClass) {
    final Set<RefClass> dependents =
      refClass.getUserData(INITIALIZATION_DEPENDENT_CLASSES_KEY);
    if (dependents != null) {
      return dependents;
    }
    final Set<RefClass> newDependents = new HashSet<>();
    tabulateInitializationDependentClasses(refClass, newDependents);
    newDependents.remove(refClass);
    refClass.putUserData(INITIALIZATION_DEPENDENT_CLASSES_KEY, newDependents);
    return newDependents;
  }

  @SuppressWarnings({"MethodWithMultipleLoops"})
  private static void tabulateInitializationDependentClasses(
    RefElement element, Set<? super RefClass> dependents) {
    final Collection<RefElement> references = element.getInReferences();
    final RefJavaUtil refUtil = RefJavaUtil.getInstance();
    for (RefElement reference : references) {
      final RefClass refClass = refUtil.getTopLevelClass(reference);
      if (refClass != null) {
        dependents.add(refClass);
      }
    }
    final List<RefEntity> children = element.getChildren();
    for (RefEntity child : children) {
      if (child instanceof RefElement) {
        tabulateInitializationDependentClasses((RefElement)child,
                                               dependents);
      }
    }
  }

  public static Set<RefClass> calculateTransitiveInitializationDependentsForClass(
    RefClass refClass) {
    final Set<RefClass> dependents =
      refClass.getUserData(TRANSITIVE_INITIALIZATION_DEPENDENT_CLASSES_KEY);
    if (dependents != null) {
      return dependents;
    }
    final Set<RefClass> newDependents = new HashSet<>();
    tabulateInitializationTransitiveDependentClasses(refClass, newDependents);
    refClass.putUserData(TRANSITIVE_INITIALIZATION_DEPENDENT_CLASSES_KEY,
                         newDependents);
    return newDependents;
  }

  private static void tabulateInitializationTransitiveDependentClasses(
    RefClass refClass, Set<? super RefClass> newDependents) {
    final LinkedList<RefClass> pendingClasses = new LinkedList<>();
    final Set<RefClass> processedClasses = new HashSet<>();
    pendingClasses.addLast(refClass);
    while (!pendingClasses.isEmpty()) {
      final RefClass classToProcess = pendingClasses.removeFirst();
      newDependents.add(classToProcess);
      processedClasses.add(classToProcess);
      final Set<RefClass> dependents =
        calculateInitializationDependentsForClass(classToProcess);
      for (RefClass dependent : dependents) {
        if (!pendingClasses.contains(dependent) &&
            !processedClasses.contains(dependent)) {
          pendingClasses.addLast(dependent);
        }
      }
    }
    newDependents.remove(refClass);
  }
}
