/**
 * Copyright (C) 2016 Red Hat, Inc. and/or its affiliates.
 *
 * 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.jboss.errai.ioc.rebind.ioc.graph.impl;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

import org.apache.commons.lang3.Validate;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.meta.MetaParameterizedType;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.Dependency;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.DependencyType;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.InjectableType;
import org.jboss.errai.ioc.rebind.ioc.graph.api.HasInjectableHandle;
import org.jboss.errai.ioc.rebind.ioc.graph.api.Injectable;

/**
 *
 * @author Max Barkley <mbarkley@redhat.com>
 */
final class GraphUtil {

  private GraphUtil() {}

  static void throwDuplicateConcreteInjectableException(final String name, final Injectable first,
          final Injectable second) {
    final String message = "Two concrete injectables exist with the same name (" + name + "):\n"
                            + "\t" + first + "\n"
                            + "\t" + second;

    throw new RuntimeException(message);
  }

  static ProducerInstanceDependencyImpl findProducerInstanceDep(final InjectableImpl injectable) {
    for (final BaseDependency dep : injectable.dependencies) {
      if (dep.dependencyType.equals(DependencyType.ProducerMember)) {
        return (ProducerInstanceDependencyImpl) dep;
      }
    }
    throw new RuntimeException("Could not find producer member.");
  }

  static MetaMethod getOverridenMethod(final MetaMethod specializingMethod) {
    final MetaClass[] producerParams = GraphUtil.getParameterTypes(specializingMethod);
    MetaClass enclosingType = specializingMethod.getDeclaringClass();
    MetaMethod specializedMethod = null;
    while (specializedMethod == null && enclosingType.getSuperClass() != null) {
      enclosingType = enclosingType.getSuperClass();
      specializedMethod = enclosingType.getDeclaredMethod(specializingMethod.getName(), producerParams);
    }

    return specializedMethod;
  }

  static MetaClass[] getParameterTypes(final MetaMethod producerMethod) {
    final MetaClass[] paramTypes = new MetaClass[producerMethod.getParameters().length];
    for (int i = 0; i < paramTypes.length; i++) {
      paramTypes[i] = producerMethod.getParameters()[i].getType();
    }

    return paramTypes;
  }

  /**
   * Required so that subtypes get all the qualifiers of supertypes when there
   * are multiple @Specializes in the hierarchy.
   */
  static void sortSuperTypesBeforeSubtypes(final List<InjectableImpl> specializations) {
    Collections.sort(specializations, new Comparator<InjectableImpl>() {
      @Override
      public int compare(final InjectableImpl c1, final InjectableImpl c2) {
        return getScore(c1) - getScore(c2);
      }

      private int getScore(final InjectableImpl c) {
        if (c.injectableType.equals(InjectableType.Producer)) {
          return getDistanceFromObject(findProducerInstanceDep(c).producingMember.getDeclaringClass());
        } else {
          return getDistanceFromObject(c.type);
        }
      }

      private int getDistanceFromObject(MetaClass type) {
        int distance = 0;
        for (; type.getSuperClass() != null; type = type.getSuperClass()) {
          distance++;
        }

        return distance;
      }
    });
  }

  static String combineProblemMessages(final Collection<String> problems) {
    final StringBuilder builder = new StringBuilder("The following problems were found:\n\n");
    for (final String problem : problems) {
      builder.append(problem)
             .append("\n");
    }

    return builder.toString();
  }

  static Injectable getResolvedDependency(final Dependency dep, final Injectable depOwner) {
    return Validate.notNull(dep.getInjectable(), "The dependency %s in %s should have already been resolved.", dep, depOwner);
  }

  static String buildMessageFromProblems(final List<String> dependencyProblems) {
    final StringBuilder builder = new StringBuilder();
    builder.append("The following dependency problems were found:\n");
    for (final String problem : dependencyProblems) {
      builder.append('\t')
             .append(problem)
             .append('\n');
    }

    return builder.toString();
  }

  static InjectableReference copyInjectableReference(final InjectableReference injectable) {
    final InjectableReference copy = new InjectableReference(injectable.type, injectable.qualifier);
    copy.linked.addAll(injectable.linked);

    return copy;
  }

  static String unsatisfiedDependencyMessage(final BaseDependency dep, final Injectable concrete,
          final Collection<Injectable> resolvedDisabledBeans) {
    final StringBuilder message = new StringBuilder()
            .append("Unsatisfied ")
            .append(dep.dependencyType.toString().toLowerCase())
            .append(" dependency ")
            .append(dep.injectable)
            .append(" for ")
            .append(concrete)
            .append('.');

    if (!resolvedDisabledBeans.isEmpty()) {
      message.append(" Some beans were found that satisfied this dependency, but must be enabled:\n");
      resolvedDisabledBeans.stream().forEach(inj -> message
              .append(inj.getInjectedType().getFullyQualifiedName()).append('\n'));
    }

    return message.toString();
  }

  static String ambiguousDependencyMessage(final BaseDependency dep, final Injectable concrete, final List<InjectableImpl> resolved) {
    final StringBuilder messageBuilder = new StringBuilder();
    messageBuilder.append("Ambiguous resolution for ")
                  .append(dep.dependencyType.toString().toLowerCase())
                  .append(" ")
                  .append(dep.injectable)
                  .append(" in ")
                  .append(concrete)
                  .append(".\n")
                  .append("Resolved types:\n")
                  .append(resolved.get(0));
    for (int i = 1; i < resolved.size(); i++) {
      messageBuilder.append(", ")
                    .append(resolved.get(i));
    }

    return messageBuilder.toString();
  }

  static boolean candidateSatisfiesInjectable(final InjectableReference injectableReference,
          final HasInjectableHandle candidate, final boolean considerTypeParameters) {
    return qualifiersMatch(injectableReference, candidate)
            && (!considerTypeParameters || typeParametersMatch(injectableReference, candidate))
            && notSameReference(injectableReference, candidate);
  }

  static boolean candidateSatisfiesInjectable(final InjectableReference injectableReference,
          final HasInjectableHandle candidate) {
    return candidateSatisfiesInjectable(injectableReference, candidate, true);
  }

  private static boolean notSameReference(final InjectableReference injectableReference, final HasInjectableHandle candidate) {
    return !candidate.equals(injectableReference);
  }

  private static boolean typeParametersMatch(final InjectableReference injectableReference, final HasInjectableHandle candidate) {
    return GraphUtil.hasAssignableTypeParameters(candidate.getInjectedType(), injectableReference.type);
  }

  private static boolean qualifiersMatch(final InjectableReference injectableReference, final HasInjectableHandle candidate) {
    return injectableReference.qualifier.isSatisfiedBy(candidate.getQualifier());
  }

  static boolean hasAssignableTypeParameters(final MetaClass fromType, final MetaClass toType) {
    final MetaParameterizedType toParamType = toType.getParameterizedType();
    final Optional<MetaParameterizedType> fromParamType = GraphUtil.getFromTypeParams(fromType, toType);

    return toParamType == null || fromParamType.map(type -> toParamType.isAssignableFrom(type)).orElse(true);
  }

  static Optional<MetaParameterizedType> getFromTypeParams(final MetaClass fromType, final MetaClass toType) {
    MetaClass parameterContainingType = null;
    if (toType.isInterface()) {
      if (fromType.getFullyQualifiedName().equals(toType.getFullyQualifiedName())) {
        parameterContainingType = fromType;
      }
      else for (final MetaClass type : fromType.getAllSuperTypesAndInterfaces()) {
        if (type.isInterface() && type.getFullyQualifiedName().equals(toType.getFullyQualifiedName())) {
          parameterContainingType = type;
          break;
        }
      }
    } else {
      MetaClass clazz = fromType;
      do {
        if (clazz.getFullyQualifiedName().equals(toType.getFullyQualifiedName())) {
          parameterContainingType = clazz;
          break;
        }
        clazz = clazz.getSuperClass();
      } while (!clazz.getFullyQualifiedName().equals("java.lang.Object"));
    }

    if (parameterContainingType == null) {
      final String classOrIface = (toType.isInterface() ? "interface" : "class");
      throw new RuntimeException("Could not find " + classOrIface + " " + toType.getFullyQualifiedName()
      + " through type " + fromType.getFullyQualifiedName());
    }
    else if (parameterContainingType.getParameterizedType() != null) {
      return Optional.of(parameterContainingType.getParameterizedType());
    }
    else {
      return Optional.empty();
    }
  }

}
