/*
 * Copyright (c) EMC Corporation. All rights reserved.
 */
package radl.java.extraction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;

import radl.common.StringUtil;
import radl.core.Log;


/**
 * Base class for processing REST-specific annotations.
 */
public abstract class AbstractRestAnnotationProcessor extends AbstractRestProcessor {

  protected abstract Collection<String> getUri(Element element, Collection<TypeElement> annotations);

  protected abstract String getMethod(Element element, TypeElement annotation);

  protected abstract Collection<String> getConsumes(Element element, TypeElement annotation);

  protected abstract Collection<String> getProduces(Element element, TypeElement annotation);

  protected abstract Parameter getParameter(Element element, TypeElement annotation);

  private final Set<String> supportedAnnotations = new LinkedHashSet<>();
  private String[] loggableClasses = new String[0];

  public AbstractRestAnnotationProcessor(String packageName, String[] annotationNames) {
    for (String annotation : annotationNames) {
      supportedAnnotations.add(packageName + annotation);
    }
  }

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    return supportedAnnotations;
  }

  protected void setLoggableClasses(String classNames) {
    loggableClasses = classNames == null ? new String[0] : classNames.split(",");
  }

  @Override
  protected void doProcess(Set<? extends TypeElement> annotations, RoundEnvironment environment) {
    Set<? extends Element> allElements = environment.getRootElements();
    for (Entry<Element, Collection<TypeElement>> entry : annotationsByElement(annotations, environment).entrySet()) {
      processAnnotations(allElements, entry.getKey(), entry.getValue());
    }
  }

  private Map<Element, Collection<TypeElement>> annotationsByElement(Set<? extends TypeElement> annotations,
      RoundEnvironment environment) {
    Map<Element, Collection<TypeElement>> result = new HashMap<>();
    for (TypeElement annotation : annotations) {
      for (Element element : environment.getElementsAnnotatedWith(annotation)) {
        if (shouldIgnore(element.getKind())) {
          continue;
        }
        Collection<TypeElement> elementAnnotations = result.get(element);
        if (elementAnnotations == null) {
          elementAnnotations = new ArrayList<>();
          result.put(element, elementAnnotations);
        }
        elementAnnotations.add(annotation);
      }
    }
    return result;
  }

  private boolean shouldIgnore(ElementKind kind) {
    return kind != ElementKind.CLASS && kind != ElementKind.METHOD && kind != ElementKind.PARAMETER;
  }

  private void processAnnotations(Set<? extends Element> allElements, Element element,
      Collection<TypeElement> annotations) {
    Collection<String> uri = null;
    String method = null;
    Collection<String> consumes = null;
    Collection<String> produces = null;
    Collection<Parameter> parameters = new ArrayList<>();
    uri = getUri(element, annotations);
    for (TypeElement annotation : annotations) {
      method = update(getMethod(element, annotation), method);
      consumes = update(getConsumes(element, annotation), consumes);
      produces = update(getProduces(element, annotation), produces);
      addParameters(getParameter(element, annotation), parameters);
    }
    processElement(allElements, element, uri, method, consumes, produces, parameters);
  }

  private void addParameters(Parameter parameter, Collection<Parameter> parameters) {
    if (parameter != null) {
      parameters.add(parameter);
    }
  }

  private String update(String newValue, String oldValue) {
    return newValue == null ? oldValue : newValue;
  }

  Collection<String> update(Collection<String> newValue, Collection<String> oldValue) {
    return newValue == null ? oldValue : newValue;
  }

  private void processElement(Set<? extends Element> allElements, Element element, Collection<String> uris,
      String method, Collection<String> consumes, Collection<String> produces, Collection<Parameter> parameters) {
    Element classElement = element;
    while (classElement.getKind() != ElementKind.CLASS) {
      classElement = classElement.getEnclosingElement();
    }
    Collection<Element> conreteSubClasses = getConcreteSubClassesOf(allElements, classElement);
    for (Element concreteSubClassElement : conreteSubClasses) {
      processElement(allElements, element, concreteSubClassElement, uris, method, consumes, produces, parameters);
    }
  }

  private Collection<Element> getConcreteSubClassesOf(Set<? extends Element> allTypes, Element classElement) {
    Collection<Element> result = new HashSet<>();
    for (Element subClassElement : getTypesExtendingOrImplementing(allTypes, classElement)) {
      if (!subClassElement.getModifiers().contains(Modifier.ABSTRACT)) {
        result.add(subClassElement);
      }
    }
    return result;
  }

  private void processElement(Set<? extends Element> allTypes, Element element, Element classElement,
      Collection<String> uris, String method, Collection<String> consumes, Collection<String> produces,
      Collection<Parameter> parameters) {
    String resourceName = qualifiedNameOf(classElement);
    addResource(resourceName, getDocumentationFor(classElement));

    if (parameters.isEmpty()) {
      if (uris == null) {
        addMethod(resourceName, method, consumes, produces, getDocumentationFor(element));
      } else {
        if (element.getKind() == ElementKind.METHOD) {
          for (String childResourceName : getChildResourceNames(allTypes, resourceName, element)) {
            addResource(childResourceName, null);
            getResourceModel().addParentResource(childResourceName, resourceName);
            getResourceModel().addLocations(childResourceName, uris);
            addMethod(childResourceName, method, consumes, produces, getDocumentationFor(element));
          }
        } else {
          getResourceModel().addLocations(resourceName, uris);
          addMethod(resourceName, method, consumes, produces, getDocumentationFor(element));
        }
      }
    } else {
      for (Parameter parameter : parameters) {
        addParameter(resourceName, parameter);
      }
    }
  }

  private void addParameter(String className, Parameter parameter) {
    logClass("Added parameter " + parameter.getName(), className);
    getResourceModel().addLocationVar(className, parameter.getName(), parameter.getDocumentation());
  }

  private void logClass(String message, String className) {
    for (String loggableClass : loggableClasses) {
      if (className.contains(loggableClass)) {
        Log.info(className + " - " + message);
        return;
      }
    }
  }

  private void addResource(String className, String documentation) {
    logClass("Added", className);
    getResourceModel().addResource(className, documentation);
  }

  private void addMethod(String resource, String method, Collection<String> consumes, Collection<String> produces,
      String documentation) {
    if (method != null) {
      getResourceModel().addMethod(resource, method, collectionToString(consumes), collectionToString(produces),
          documentation);
    }
  }

  protected String collectionToString(Collection<String> values) {
    if (values == null) {
      return null;
    }
    StringBuilder result = new StringBuilder();
    String prefix = "";
    for (String value : values) {
      result.append(prefix).append(value);
      prefix = ",";
    }
    return result.toString();
  }

  private Iterable<String> getChildResourceNames(Set<? extends Element> allTypes, String classResourceName,
      Element methodElement) {
    Element returnType = getReturnType(methodElement);
    if (allTypes.contains(returnType)) {
      Collection<String> result = new HashSet<>();
      for (Element type : getTypesExtendingOrImplementing(allTypes, returnType)) {
        result.add(qualifiedNameOf(type));
      }
      return result;
    }
    return Collections.singleton(classResourceName + '.' + nameOf(methodElement));
  }

  private Collection<Element> getTypesExtendingOrImplementing(Set<? extends Element> allTypes, Element baseType) {
    Collection<Element> result = new LinkedHashSet<>();
    result.add(baseType);
    addTypesExtendingOrImplementing(allTypes, baseType.asType(), result);
    logClass("Sub types: " + result, qualifiedNameOf(baseType));
    return result;
  }

  private void addTypesExtendingOrImplementing(Set<? extends Element> allElements, TypeMirror superType,
      Collection<Element> subElements) {
    Types typeUtils = processingEnv.getTypeUtils();
    for (Element element : allElements) {
      if (subElements.contains(element)) {
        continue;
      }
      TypeMirror type = element.asType();
      if (typeUtils.directSupertypes(type).contains(superType)) {
        subElements.add(element);
        addTypesExtendingOrImplementing(allElements, type, subElements);
      }
    }
  }

  private Element getReturnType(Element methodElement) {
    Element result = null;
    if (methodElement instanceof ExecutableElement) {
      TypeMirror returnType = ((ExecutableElement)methodElement).getReturnType();
      if (returnType instanceof DeclaredType) {
        result = ((DeclaredType)returnType).asElement();
      }
    }
    return result;
  }

  protected Collection<String> valueOf(TypeElement annotation, Element element) {
    return valueOf(annotation, element, "value");
  }

  protected Collection<String> valueOf(TypeElement annotation, Element element, String property) {
    if (annotation == null) {
      return null;
    }
    Name annotationClassName = annotation.getQualifiedName();
    for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
      TypeElement annotationElement = (TypeElement)annotationMirror.getAnnotationType().asElement();
      if (annotationElement.getQualifiedName().contentEquals(annotationClassName)) {
        return valueOf(annotationMirror, property);
      }
    }
    return null;
  }

  private Collection<String> valueOf(AnnotationMirror annotation, String property) {
    for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotation.getElementValues().entrySet()) {
      if (entry.getKey().getSimpleName().contentEquals(property)) {
        Object values = entry.getValue().getValue();
        if (values instanceof Iterable) {
          Collection<String> result = new ArrayList<>();
          for (Object value : (Iterable<?>)values) {
            result.add(StringUtil.stripQuotes(value.toString()));
          }
          return result;
        }
        if (values instanceof String) {
          return Arrays.asList(StringUtil.stripQuotes(values.toString()));
        }
        throw new IllegalStateException("Unhandled annotation value type: " + values.getClass().getName());
      }
    }
    return null;
  }

  protected String singleValueOf(TypeElement annotation, Element element) {
    Collection<String> result = valueOf(annotation, element);
    return result == null ? null : result.iterator().next();
  }

}