/*
 * Copyright (C) 2015 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 static org.jboss.errai.ioc.util.GeneratedNamesUtil.qualifiedClassNameToIdentifier;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.enterprise.inject.Any;
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Stereotype;
import javax.inject.Named;

import org.jboss.errai.codegen.meta.HasAnnotations;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaClassMember;
import org.jboss.errai.codegen.meta.MetaParameter;
import org.jboss.errai.codegen.util.CDIAnnotationUtils;
import org.jboss.errai.ioc.rebind.ioc.graph.api.Qualifier;
import org.jboss.errai.ioc.rebind.ioc.graph.api.QualifierFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;

/**
 * Default implementation of {@link QualifierFactory}. Adheres to the CDI spec
 * regarding what qualifiers injectables and injection points have.
 *
 * @author Max Barkley <mbarkley@redhat.com>
 */
public class DefaultQualifierFactory implements QualifierFactory {

  private static final Any ANY = new Any() {
    @Override
    public Class<? extends Annotation> annotationType() {
      return Any.class;
    }
    @Override
    public String toString() {
      return "@Any";
    }
    @Override
    public boolean equals(final Object obj) {
      return obj instanceof Any;
    }
  };

  private static final AnnotationWrapper ANY_WRAPPER = new AnnotationWrapper(ANY);

  private static final Default DEFAULT = new Default() {
    @Override
    public Class<? extends Annotation> annotationType() {
      return Default.class;
    }
    @Override
    public String toString() {
      return "@Default";
    }
    @Override
    public boolean equals(final Object obj) {
      return obj instanceof Default;
    }
  };

  private static final AnnotationWrapper DEFAULT_WRAPPER = new AnnotationWrapper(DEFAULT);

  private static final Qualifier UNIVERSAL = new Universal();

  private final Map<SortedSet<AnnotationWrapper>, NormalQualifier> qualifiers = new HashMap<>();

  @Override
  public Qualifier forSource(final HasAnnotations annotated) {
    final SortedSet<AnnotationWrapper> annos = getRawQualifiers(annotated);
    maybeAddDefaultForSource(annos);
    annos.add(ANY_WRAPPER);

    return getOrCreateQualifier(annos);
  }

  @Override
  public Qualifier forSink(final HasAnnotations annotated) {
    final SortedSet<AnnotationWrapper> annos = getRawQualifiers(annotated);
    maybeAddDefaultForSink(annos);

    return getOrCreateQualifier(annos);
  }

  private NormalQualifier getOrCreateQualifier(final SortedSet<AnnotationWrapper> annos) {
    NormalQualifier qualifier = qualifiers.get(annos);
    if (qualifier == null) {
      qualifier = new NormalQualifier(annos);
      qualifiers.put(annos, qualifier);
    }

    return qualifier;
  }

  private SortedSet<AnnotationWrapper> getRawQualifiers(final HasAnnotations annotated) {
    final SortedSet<AnnotationWrapper> annos = new TreeSet<>();
    for (final Annotation anno : annotated.getAnnotations()) {
      if (anno.annotationType().isAnnotationPresent(javax.inject.Qualifier.class)) {
        if (anno.annotationType().equals(Named.class) && ((Named) anno).value().equals("")) {
          annos.add(createNamed(annotated));
        } else {
          annos.add(new AnnotationWrapper(anno));
        }
      } else if (anno.annotationType().isAnnotationPresent(Stereotype.class)) {
        annos.addAll(getRawQualifiers(MetaClassFactory.get(anno.annotationType())));
      }
    }

    return annos;
  }

  private AnnotationWrapper createNamed(final HasAnnotations annotated) {
    final String rawName;
    if (annotated instanceof MetaClassMember) {
      rawName = ((MetaClassMember) annotated).getName();
    } else if (annotated instanceof MetaClass) {
      rawName = ((MetaClass) annotated).getName();
    } else if (annotated instanceof MetaParameter) {
      rawName = ((MetaParameter) annotated).getName();
    } else {
      throw new RuntimeException("Unrecognized annotated type " + annotated.getClass().getName());
    }

    return createNamed(CDIAnnotationUtils.formatDefaultElName(rawName));
  }

  private AnnotationWrapper createNamed(final String defaultName) {
    return new AnnotationWrapper(new Named() {
      @Override
      public Class<? extends Annotation> annotationType() {
        return Named.class;
      }

      @Override
      public String value() {
        return defaultName;
      }

      @Override
      public String toString() {
        return "@javax.inject.Named(value=" + defaultName + ")";
      }
    });
  }

  private void maybeAddDefaultForSource(final Set<AnnotationWrapper> annos) {
    if (annos.isEmpty() || onlyContainsNamed(annos)) {
      annos.add(DEFAULT_WRAPPER);
    }
  }

  private void maybeAddDefaultForSink(final Set<AnnotationWrapper> annos) {
    if (annos.isEmpty()) {
      annos.add(DEFAULT_WRAPPER);
    }
  }

  private boolean onlyContainsNamed(final Set<AnnotationWrapper> annos) {
    return annos.size() == 1 && annos.iterator().next().anno.annotationType().equals(Named.class);
  }

  @Override
  public Qualifier forDefault() {
    return getOrCreateQualifier(new TreeSet<>(Arrays.asList(DEFAULT_WRAPPER, ANY_WRAPPER)));
  }

  @Override
  public Qualifier forUniversallyQualified() {
    return UNIVERSAL;
  }

  @Override
  public Qualifier combine(final Qualifier q1, final Qualifier q2) {
    if (q1 instanceof Universal || q2 instanceof Universal) {
      return UNIVERSAL;
    } else if (q1 instanceof NormalQualifier && q2 instanceof NormalQualifier) {
      return combineNormal((NormalQualifier) q1, (NormalQualifier) q2);
    } else {
      throw new RuntimeException("At least one unrecognized qualifier implementation: " + q1.getClass().getName()
              + " and " + q2.getClass().getName());
    }
  }

  private Qualifier combineNormal(final NormalQualifier q1, final NormalQualifier q2) {
    final Multimap<Class<? extends Annotation>, AnnotationWrapper> allAnnosByType = HashMultimap.create();
    for (final AnnotationWrapper wrapper : q1.annotations) {
      allAnnosByType.put(wrapper.anno.annotationType(), wrapper);
    }
    for (final AnnotationWrapper wrapper : q2.annotations) {
      allAnnosByType.put(wrapper.anno.annotationType(), wrapper);
    }

    for (final Class<? extends Annotation> annoType : allAnnosByType.keySet()) {
      if (allAnnosByType.get(annoType).size() == 2) {
        final Iterator<AnnotationWrapper> iter = allAnnosByType.get(annoType).iterator();
        throw new RuntimeException("Found two annotations of same type but with different values:\n\t"
                + iter.next() + "\n\t" + iter.next());
      }
    }

    return getOrCreateQualifier(new TreeSet<>(allAnnosByType.values()));
  }

  private static final class NormalQualifier implements Qualifier {

    private final Set<AnnotationWrapper> annotations;
    private String identifier;

    private NormalQualifier(final Set<AnnotationWrapper> set) {
      this.annotations = set;
    }

    @Override
    public boolean isDefaultQualifier() {
      return annotations.size() == 2 && annotations.contains(ANY_WRAPPER) && annotations.contains(DEFAULT_WRAPPER);
    }

    @Override
    public boolean isSatisfiedBy(final Qualifier other) {
      if (other instanceof Universal) {
        return true;
      } else if (other instanceof NormalQualifier) {
        return ((NormalQualifier) other).annotations.containsAll(annotations);
      } else {
        throw new RuntimeException("Unrecognized qualifier type: " + other.getClass().getName());
      }
    }

    @Override
    public String getIdentifierSafeString() {
      if (identifier == null) {
        final StringBuilder builder = new StringBuilder();
        for (final AnnotationWrapper wrapper : annotations) {
          builder.append(qualifiedClassNameToIdentifier(wrapper.anno.annotationType()))
                 .append("__");
        }
        // Remove last delimeter
        if (annotations.size() > 0) {
          builder.delete(builder.length()-2, builder.length());
        }

        identifier = builder.toString();
      }

      return identifier;
    }

    @Override
    public String getName() {
      for (final AnnotationWrapper wrapper : annotations) {
        if (wrapper.anno.annotationType().equals(Named.class)) {
          return ((Named) wrapper.anno).value();
        }
      }

      return null;
    }

    @Override
    public String toString() {
      return annotations.stream().map(AnnotationWrapper::toString).reduce((s1, s2) -> s1 + " " + s2).orElse("");
    }

    @Override
    public int hashCode() {
      return annotations.hashCode();
    }

    @Override
    public boolean equals(final Object obj) {
      if (!(obj instanceof NormalQualifier)) {
        return false;
      }
      final NormalQualifier other = (NormalQualifier) obj;

      return annotations.equals(other.annotations);
    }

    @Override
    public Iterator<Annotation> iterator() {
      final Iterator<AnnotationWrapper> iter = annotations.iterator();
      return new Iterator<Annotation>() {

        @Override
        public boolean hasNext() {
          return iter.hasNext();
        }

        @Override
        public Annotation next() {
          return iter.next().anno;
        }

        @Override
        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }
  }

  private static final class Universal implements Qualifier {
    @Override
    public boolean isSatisfiedBy(final Qualifier other) {
      return other == this;
    }

    @Override
    public boolean isDefaultQualifier() {
      return false;
    }

    @Override
    public String getIdentifierSafeString() {
      return "Universal";
    }

    @Override
    public String toString() {
      return "@" + getIdentifierSafeString();
    }

    @Override
    public boolean equals(final Object obj) {
      return obj instanceof Universal;
    }

    @Override
    public String getName() {
      return null;
    }

    @Override
    public Iterator<Annotation> iterator() {
      return Collections.<Annotation>emptyList().iterator();
    }
  }

  private static final class AnnotationWrapper implements Comparable<AnnotationWrapper> {
    private final Annotation anno;

    private AnnotationWrapper(final Annotation anno) {
      this.anno = anno;
    }

    @Override
    public int hashCode() {
      return anno.annotationType().hashCode();
    }

    @Override
    public boolean equals(final Object obj) {
      if (!(obj instanceof AnnotationWrapper)) {
        return false;
      }
      final AnnotationWrapper other = (AnnotationWrapper) obj;

      return CDIAnnotationUtils.equals(anno, other.anno);
    }

    @Override
    public String toString() {
      return anno.toString();
    }

    @Override
    public int compareTo(final AnnotationWrapper o) {
      final int compareTo = anno.annotationType().getName().compareTo(o.anno.annotationType().getName());
      if (compareTo != 0) {
        return compareTo;
      } else if (equals(o)) {
        return 0;
      } else {
        // Arbitrary stable ordering for annotations of same type with different values.
        return anno.toString().compareTo(o.anno.toString());
      }
    }
  }

}
