/*
 * Copyright (c) 2018 - 2020 - Frank Hossfeld
 *
 *  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.github.nalukit.nalu.processor;

import com.github.nalukit.nalu.processor.model.intern.ClassNameModel;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class ProcessorUtils {

  private ProcessingEnvironment processingEnvironment;

  private Messager messager;

  private Elements elements;

  @SuppressWarnings("unused")
  private ProcessorUtils(Builder builder) {
    super();

    this.processingEnvironment = builder.processingEnvironment;

    this.messager = this.processingEnvironment.getMessager();
    this.elements = this.processingEnvironment.getElementUtils();
  }

  public static Builder builder() {
    return new Builder();
  }

  //  public void store(IsMetaModel metaModel,
  //                    String fileName)
  //    throws ProcessorException {
  //    try {
  //      FileObject fileObject = processingEnvironment.getFiler()
  //                                                   .createResource(StandardLocation.CLASS_OUTPUT,
  //                                                                   "",
  //                                                                   fileName);
  //      PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(fileObject.openOutputStream()));
  //      metaModel.createPropertes()
  //               .store(printWriter,
  //                      "");
  //      printWriter.close();
  //    } catch (IOException ex) {
  //      throw new ProcessorException("NaluProcessor: Unable to write file: >>" + fileName + "<< -> exception: " + ex.getMessage());
  //    }
  //  }

  //  public boolean implementsInterface(ProcessingEnvironment processingEnvironment,
  //                                     TypeElement typeElement,
  //                                     TypeMirror implementedInterface) {
  //    return processingEnvironment.getTypeUtils()
  //                                .isAssignable(typeElement.asType(),
  //                                              implementedInterface);
  //  }

  //  public String getCanonicalClassName(Element element) {
  //    return this.getPackageAsString(element) +
  //           "." + element.getSimpleName()
  //                        .toString();
  //  }

  public String getPackageAsString(Element type) {
    return this.getPackage(type)
               .getQualifiedName()
               .toString();
  }

  public PackageElement getPackage(Element type) {
    while (type.getKind() != ElementKind.PACKAGE) {
      type = type.getEnclosingElement();
    }
    return (PackageElement) type;
  }

  public Elements getElements() {
    return this.elements;
  }

  /**
   * checks if a class or interface is implemented.
   *
   * @param types       types
   * @param typeMirror  of the class to check
   * @param toImplement the type mirror to implement
   * @return true - class is implemented
   */
  public boolean extendsClassOrInterface(Types types,
                                         TypeMirror typeMirror,
                                         TypeMirror toImplement) {
    String clearedToImplement = this.removeGenericsFromClassName(toImplement.toString());
    Set<TypeMirror> setOfSuperType = this.getFlattenedSupertypeHierarchy(types,
                                                                         typeMirror);
    for (TypeMirror mirror : setOfSuperType) {
      if (clearedToImplement.equals(this.removeGenericsFromClassName(mirror.toString()))) {
        return true;
      }
    }
    return false;
  }

  private String removeGenericsFromClassName(String className) {
    if (className.contains("<")) {
      className = className.substring(0,
                                      className.indexOf("<"));
    }
    return className;
  }

  /**
   * Returns all of the superclasses and superinterfaces for a given generator
   * including the generator itself. The returned set maintains an internal
   * breadth-first ordering of the generator, followed by its interfaces (and their
   * super-interfaces), then the supertype and its interfaces, and so on.
   *
   * @param types      types
   * @param typeMirror of the class to check
   * @return Set of implemented super types
   */
  public Set<TypeMirror> getFlattenedSupertypeHierarchy(Types types,
                                                        TypeMirror typeMirror) {
    List<TypeMirror> toAdd = new ArrayList<>();
    LinkedHashSet<TypeMirror> result = new LinkedHashSet<>();
    toAdd.add(typeMirror);
    for (int i = 0; i < toAdd.size(); i++) {
      TypeMirror type = toAdd.get(i);
      if (result.add(type)) {
        toAdd.addAll(types.directSupertypes(type));
      }
    }
    return result;
  }

  public boolean supertypeHasGeneric(Types types,
                                     TypeMirror typeMirror,
                                     TypeMirror implementsMirror) {
    TypeMirror superTypeMirror = this.getFlattenedSupertype(types,
                                                            typeMirror,
                                                            implementsMirror);
    if (superTypeMirror == null) {
      return false;
    }
    return superTypeMirror.toString()
                          .contains("<");
  }

  public TypeMirror getFlattenedSupertype(Types types,
                                          TypeMirror typeMirror,
                                          TypeMirror implementsMirror) {
    String implementsMirrorWihoutGeneric = this.removeGenericsFromClassName(implementsMirror.toString());
    Set<TypeMirror> implementedSuperTypes = this.getFlattenedSupertypeHierarchy(types,
                                                                                typeMirror);
    for (TypeMirror typeMirrorSuperType : implementedSuperTypes) {
      String tn1WithoutGenric = this.removeGenericsFromClassName(typeMirrorSuperType.toString());
      if (implementsMirrorWihoutGeneric.equals(tn1WithoutGenric)) {
        return typeMirrorSuperType;
      }
    }
    return null;
  }

  //  public String createNameWithleadingUpperCase(String name) {
  //    return name.substring(0,
  //                          1)
  //               .toUpperCase() + name.substring(1);
  //  }

  public void createErrorMessage(String errorMessage) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    pw.println(errorMessage);
    pw.close();
    messager.printMessage(Diagnostic.Kind.ERROR,
                          sw.toString());

  }

  public String createFullClassName(String packageName,
                                    String className) {
    return packageName.replace(".",
                               "_") + "_" + className;
  }

  public void createNoteMessage(String noteMessage) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    pw.println(noteMessage);
    pw.close();
    messager.printMessage(Diagnostic.Kind.NOTE,
                          sw.toString());
  }

  public void createWarningMessage(String warningMessage) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    pw.println(warningMessage);
    pw.close();
    messager.printMessage(Diagnostic.Kind.WARNING,
                          sw.toString());
  }

  //  public <T> boolean isSuperClass(Types typeUtils,
  //                                  TypeElement typeElement,
  //                                  Class<T> superClazz) {
  //    for (TypeMirror tm : typeUtils.directSupertypes(typeElement.asType())) {
  //      String canonicalNameTM = this.getCanonicalClassName((TypeElement) typeUtils.asElement(tm));
  //      if (superClazz.getCanonicalName()
  //                    .equals(canonicalNameTM)) {
  //        return true;
  //      } else {
  //        return this.isSuperClass(typeUtils,
  //                                 (TypeElement) typeUtils.asElement(tm),
  //                                 superClazz);
  //      }
  //    }
  //    return false;
  //  }
  //
  //  public List<TypeElement> getListOfSuperClasses(TypeElement typeElement,
  //                                                 Class<?> implementingSuperClass) {
  //    List<TypeElement> listOfTypeMirror = new ArrayList<>();
  //    TypeMirror implementingSuperClassTypeMirror = this.getTypeMirror(implementingSuperClass.getCanonicalName());
  //    Set<TypeMirror> list = this.getFlattenedSupertypeHierarchy(this.processingEnvironment.getTypeUtils(),
  //                                                               typeElement.asType());
  //    list.stream()
  //        .filter(mirror -> !implementingSuperClassTypeMirror.toString()
  //                                                           .equals(mirror.toString()))
  //        .filter(mirror -> !typeElement.asType()
  //                                      .toString()
  //                                      .equals(mirror.toString()))
  //        .filter(mirror -> this.processingEnvironment.getTypeUtils()
  //                                                    .isAssignable(mirror,
  //                                                                  implementingSuperClassTypeMirror))
  //        .forEachOrdered(mirror -> {
  //          listOfTypeMirror.add(this.getTypeElement(mirror));
  //        });
  //    return listOfTypeMirror;
  //  }
  //
  //  public TypeMirror getTypeMirror(String className) {
  //    return this.getTypeElement(className)
  //               .asType();
  //  }
  //
  //  public TypeElement getTypeElement(TypeMirror mirror) {
  //    return (TypeElement) this.processingEnvironment.getTypeUtils()
  //                                                   .asElement(mirror);
  //  }
  //
  //  public TypeElement getTypeElement(String className) {
  //    return this.processingEnvironment.getElementUtils()
  //                                     .getTypeElement(className);
  //  }
  //
  //  public String getEventBusResourcePath() {
  //    return StandardLocation.CLASS_OUTPUT + "/" + "META-INF/" + ProcessorConstants.NALU_REACT_FOLDER_NAME + "/" + ProcessorConstants.EVENT_BUS_FOLDER_NAME + "/EventBus";
  //  }

  public <A extends Annotation> List<Element> getMethodFromTypeElementAnnotatedWith(ProcessingEnvironment processingEnvironment,
                                                                                    TypeElement element,
                                                                                    Class<A> annotation) {
    List<Element> annotatedMethods = processingEnvironment.getElementUtils()
                                                          .getAllMembers(element)
                                                          .stream()
                                                          .filter(methodElement -> methodElement.getAnnotation(annotation) != null)
                                                          .collect(Collectors.toList());
    return annotatedMethods;
  }

  public String createInternalEventName(ExecutableElement executableElement) {
    String internalEventname = executableElement.getSimpleName()
                                                .toString();
    for (VariableElement variableElement : executableElement.getParameters()) {
      internalEventname += ProcessorConstants.PARAMETER_DELIMITER;
      internalEventname += variableElement.asType()
                                          .toString()
                                          .replace(".",
                                                   "_");
    }
    return internalEventname;
  }

  public boolean doesExist(ClassNameModel typeElementClassName) {
    if (Objects.isNull(typeElementClassName)) {
      return false;
    }
    return this.processingEnvironment.getElementUtils()
                                     .getTypeElement(typeElementClassName.getClassName()) != null;
  }

  public String createHistoryMetaDataClassName(String historyConverterClassName) {
    return this.setFirstCharacterToUpperCase(this.createHistoryMetaDataVariableName(historyConverterClassName)) + "_" + ProcessorConstants.META_DATA;
  }

  public String setFirstCharacterToLowerCase(String className) {
    return className.substring(0,
                               1)
                    .toLowerCase() + className.substring(1);
  }

  public String setFirstCharacterToUpperCase(String className) {
    return className.substring(0,
                               1)
                    .toUpperCase() + className.substring(1);
  }

  public String createHistoryMetaDataVariableName(String historyConverterClassName) {
    return this.createFullClassName(historyConverterClassName);
  }

  public String createFullClassName(String className) {
    return className.replace(".",
                             "_");
  }

  public String createSetMethodName(String value) {
    return "set" +
           value.substring(0,
                           1)
                .toUpperCase() +
           value.substring(1);
  }

  public String createEventNameFromHandlingMethod(String event) {
    return event.substring(2,
                           3)
                .toLowerCase() + event.substring(3);
  }

  public static class Builder {

    ProcessingEnvironment processingEnvironment;

    public Builder processingEnvironment(ProcessingEnvironment processingEnvironment) {
      this.processingEnvironment = processingEnvironment;
      return this;
    }

    public ProcessorUtils build() {
      return new ProcessorUtils(this);
    }

  }

}