/*******************************************************************************
 * Copyright (c) 2015 Red Hat. All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v1.0 which accompanies this
 * distribution, and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors: Red Hat - Initial Contribution
 *******************************************************************************/

package org.lambdamatic.mongodb.apt.template;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;

import org.apache.commons.lang3.ClassUtils;
import org.lambdamatic.mongodb.annotations.EmbeddedDocument;
import org.lambdamatic.mongodb.annotations.TransientField;
import org.lambdamatic.mongodb.apt.BaseAnnotationProcessor;
import org.lambdamatic.mongodb.metadata.ProjectionMetadata;
import org.lambdamatic.mongodb.metadata.QueryMetadata;
import org.lambdamatic.mongodb.metadata.UpdateMetadata;

/**
 * Template Context builder for {@link QueryMetadata}, {@link ProjectionMetadata} and
 * {@link UpdateMetadata} generated classes.
 */
public class MetadataTemplateContext extends BaseTemplateContext {

  /**
   * {@link Function} to generate the simple class name for the {@link QueryMetadata} implementation
   * of a given {@link TypeElement}.
   */
  public static final Function<DeclaredType, String> elementToQuerySimpleClassNameFunction =
      t -> "Q" + ClassUtils.getShortClassName(t.toString());

  /**
   * {@link Function} to generate the simple class name for the {@link QueryMetadata} implementation
   * of a given {@link TypeElement}.
   */
  public static final Function<DeclaredType, String> elementToProjectionSimpleClassName =
      t -> "P" + ClassUtils.getShortClassName(t.toString());

  /**
   * {@link Function} to generate the simple class name for the {@link QueryMetadata} implementation
   * of a given {@link TypeElement}.
   */
  public static final Function<DeclaredType, String> elementToUpdateSimpleClassNameFunction =
      t -> "U" + ClassUtils.getShortClassName(t.toString());

  /**
   * {@link MetadataTemplateContext} constructor for a {@link QueryMetadata} implementation.
   * 
   * @param domainType the type of the annotated document
   * @param annotationProcessor the annotation processor used to generate the {@link QueryMetadata}
   *        implementation
   * @return a {@link MetadataTemplateContext} with all the data to generate the source code
   */
  public static MetadataTemplateContext createQueryMetadataTemplateContext(
      final TypeElement domainType, final BaseAnnotationProcessor annotationProcessor) {
    return new MetadataTemplateContext(domainType, annotationProcessor,
        TemplateField.createQueryFieldFunction, elementToQuerySimpleClassNameFunction,
        "query_metadata_template.mustache");
  }

  /**
   * {@link MetadataTemplateContext} constructor for a {@link ProjectionMetadata} implementation.
   * 
   * @param domainType the type of the annotated document
   * @param annotationProcessor the annotation processor used to generate the
   *        {@link ProjectionMetadata} implementation
   * @return a {@link MetadataTemplateContext} with all the data to generate the source code
   */
  public static MetadataTemplateContext createProjectionMetadataTemplateContext(
      final TypeElement domainType, final BaseAnnotationProcessor annotationProcessor) {
    return new MetadataTemplateContext(domainType, annotationProcessor,
        TemplateField.createProjectionFieldFunction, elementToProjectionSimpleClassName,
        "projection_metadata_template.mustache");
  }

  /**
   * {@link MetadataTemplateContext} constructor for a {@link UpdateMetadata} implementation.
   * 
   * @param domainType the type of the annotated document
   * @param annotationProcessor the annotation processor used to generate the {@link UpdateMetadata}
   *        implementation
   * @return a {@link MetadataTemplateContext} with all the data to generate the source code
   */
  public static MetadataTemplateContext createUpdateMetadataTemplateContext(
      final TypeElement domainType, final BaseAnnotationProcessor annotationProcessor) {
    return new MetadataTemplateContext(domainType, annotationProcessor,
        TemplateField.createUpdateFieldFunction, elementToUpdateSimpleClassNameFunction,
        "update_metadata_template.mustache");
  }

  /** The list of relevant fields to process. */
  private final List<VariableElement> domainTypeFields;

  /**
   * the list of {@link TemplateField} that will be output in the generated source code.
   */
  private final List<TemplateField> templateFields;

  /** the simple name of the Java class to generate. */
  private final String simpleClassName;

  /** the fully qualified name of the Java class to generate. */
  private final String fullyQualifiedClassName;

  /** the name of the template to use to generate the source code. */
  private final String templateFileName;

  /** The annotations to include on the generated type. */
  private List<TemplateAnnotation> annotations;

  /**
   * Full constructor
   * 
   * @param domainElement the {@link TypeElement} to work on.
   * @param templateFieldBuildFunction the {@link Function} used to generate a single
   *        {@link TemplateField} from a given relevant {@link VariableElement} in the given
   *        {@link TypeElement}.
   * @param templateMethodsBuildFunction the {@link Function} used to generate zero or more
   *        {@link TemplateMethods} from a given relevant {@link VariableElement} in the given
   *        {@link TypeElement}.
   * 
   */
  private MetadataTemplateContext(final TypeElement domainElement,
      final BaseAnnotationProcessor annotationProcessor,
      final BiFunction<VariableElement, ProcessingEnvironment, TemplateField> templateFieldBuildFunction,
      final Function<DeclaredType, String> simpleClassNameBuilder, final String templateFileName) {
    super((DeclaredType) domainElement.asType(), annotationProcessor);
    this.domainTypeFields =
        domainElement.getEnclosedElements().stream().filter(e -> e.getKind() == ElementKind.FIELD)
            .filter(e -> e.getAnnotation(TransientField.class) == null)
            .map(e -> (VariableElement) e).collect(Collectors.toList());
    this.templateFields = this.domainTypeFields.stream().map(f -> {
      return templateFieldBuildFunction.apply(f, annotationProcessor.getProcessingEnvironment());
    }).collect(Collectors.toList());
    this.simpleClassName = simpleClassNameBuilder.apply(this.domainType);
    this.fullyQualifiedClassName = getPackageName() + '.' + this.simpleClassName;
    this.templateFileName = templateFileName;
    this.annotations = Stream.of(domainElement.getAnnotationsByType(EmbeddedDocument.class))
        .map(a -> TemplateAnnotation.Builder.type(EmbeddedDocument.class).build())
        .collect(Collectors.toList());
  }

  @Override
  public String getFullyQualifiedClassName() {
    return this.fullyQualifiedClassName;
  }

  @Override
  public String getSimpleClassName() {
    return this.simpleClassName;
  }

  @Override
  public String getTemplateFileName() {
    return this.templateFileName;
  }

  /**
   * @return the {@link List} of {@link TemplateField} for the associated domain type.
   */
  public List<TemplateField> getFields() {
    return this.templateFields;
  }

  /**
   * Finds all required import statements that need to be included in the target template.
   * 
   * @return the {@link List} of class imports (excluding those in the same package)
   */
  public List<String> getRequiredImports() {
    // combining required types on annotations and fields
    final List<String> requiredImports =
        Stream
            .concat(this.templateFields.stream().flatMap(f -> f.getRequiredJavaTypes().stream()),
                this.annotations.stream().flatMap(a -> a.getRequiredJavaTypes().stream()))
            .filter(i -> {
              final String packageCanonicalName = ClassUtils.getPackageCanonicalName(i);
              return !packageCanonicalName.equals(getPackageName())
                  && !packageCanonicalName.equals("java.lang");
            }).distinct().sorted().collect(Collectors.toList());
    return requiredImports;
  }

}
