package de.dhbwstuttgart.syntaxtree;

import de.dhbwstuttgart.parser.NullToken;
import de.dhbwstuttgart.parser.scope.JavaClassName;
import de.dhbwstuttgart.syntaxtree.type.GenericRefType;
import de.dhbwstuttgart.syntaxtree.type.RefType;
import de.dhbwstuttgart.syntaxtree.type.TypePlaceholder;
import de.dhbwstuttgart.syntaxtree.type.RefTypeOrTPHOrWildcardOrGeneric;
import de.dhbwstuttgart.target.tree.TargetGeneric;
import org.antlr.v4.runtime.Token;

import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Stellt jede Art von Klasse dar. Auch abstrakte Klassen und Interfaces
 */
public class ClassOrInterface extends SyntaxTreeNode implements TypeScope {
  private Boolean methodAdded = false; // wird benoetigt bei in JavaTXCompiler.getConstraints()
  protected int modifiers;
  protected JavaClassName name;
  private final String fileName;

  private List<Field> fields = new ArrayList<>();
  private Optional<Constructor> fieldInitializations; // PL 2018-11-24: Noetig, um Bytecode fuer initializators nur einmal zu erzeugen
  private Optional<Method> staticInitializer;
  private List<Method> methods = new ArrayList<>();
  private GenericDeclarationList genericClassParameters;
  private RefType superClass;
  protected boolean isInterface;
  protected boolean isFunctionalInterface;
  private List<RefType> implementedInterfaces;
  private List<RefType> permittedSubtypes;
  private List<Constructor> constructors;
  private Set<GenericTypeVar> userDefinedGenerics;

  public ClassOrInterface(int modifiers, JavaClassName name, List<Field> fielddecl, Optional<Constructor> fieldInitializations, Optional<Method> staticInitializer, List<Method> methods, List<Constructor> constructors, GenericDeclarationList genericClassParameters, RefType superClass, Boolean isInterface, Boolean isFunctionalInterface, List<RefType> implementedInterfaces, List<RefType> permittedSubtypes, Token offset, String fileName) {
    super(offset);
    if (isInterface) {
      modifiers |= Modifier.INTERFACE | Modifier.ABSTRACT;
    }
    this.modifiers = modifiers;
    this.name = name;
    this.fields = fielddecl;
    this.fieldInitializations = fieldInitializations;
    this.staticInitializer = staticInitializer;
    this.genericClassParameters = genericClassParameters;
    this.superClass = superClass;
    this.isInterface = isInterface;
    this.isFunctionalInterface= isFunctionalInterface;
    this.implementedInterfaces = implementedInterfaces;
    this.permittedSubtypes = permittedSubtypes;
    this.methods = methods;
    this.constructors = constructors;
    this.fileName = fileName;
  }

  /*
   * erzeugt fuer Fields, Konstruktoren und Methoden neue ArrayList-Objekte alle anderen Datenobjekte werden nur kopiert.
   */
  public ClassOrInterface(ClassOrInterface cl) {
    super(cl.getOffset());
    this.modifiers = cl.modifiers;
    this.name = cl.name;
    this.fields = new ArrayList<>(cl.fields);
    this.fieldInitializations = cl.fieldInitializations;
    this.staticInitializer = cl.staticInitializer;
    this.genericClassParameters = cl.genericClassParameters;
    this.superClass = cl.superClass;
    this.isInterface = cl.isInterface;
    this.isFunctionalInterface= cl.isFunctionalInterface;
    this.implementedInterfaces = cl.implementedInterfaces;
    this.methods = new ArrayList<>(cl.methods);
    this.constructors = new ArrayList<>(cl.constructors);
    this.fileName = cl.fileName;
  }

  public String getFileName() {
    return fileName;
  }

  public Optional<Field> getField(String name) {
    // TODO This should be a map
    return fields.stream().filter(field -> field.getName().equals(name)).findFirst();
  }

  public Optional<Method> getStaticInitializer() {
    return staticInitializer;
  }

  public boolean isSealed() { return permittedSubtypes != null; }
  public List<RefType> getPermittedSubtypes() { return permittedSubtypes != null ? permittedSubtypes : List.of(); }

  public boolean isInterface() {
      return (Modifier.INTERFACE & this.getModifiers()) != 0;
  }

  public boolean isFunctionalInterface() {
		return this.isFunctionalInterface;
  }
  
  // Gets if it is added
  public Boolean areMethodsAdded() {
    return methodAdded;
  }

  // Sets that it is added
  public void setMethodsAdded() {
    methodAdded = true;
  }

  // Gets class name
  public JavaClassName getClassName() {
    return this.name;
  }

  // Get modifiers
  public int getModifiers() {
    return this.modifiers;
  }

  public List<Field> getFieldDecl() {
    return this.fields;
  }

  public Optional<Constructor> getfieldInitializations() {
    return this.fieldInitializations;
  }

  public List<Method> getMethods() {
    return this.methods;
  }
  /*
   * public RefType getType() { return generateTypeOfClass(this.getClassName(), this.getGenerics(), this.getOffset()); }
   */
  // TODO: Das hier ist ein Problem. Je nach Kontext wird hier ein anderer Typ benötigt
  public static RefType generateTypeOfClass(JavaClassName name, GenericDeclarationList genericsOfClass, Token offset) {
    // Hier wird immer ein generischer Typ generiert, also mit Type placeholdern
    List<RefTypeOrTPHOrWildcardOrGeneric> params = new ArrayList<>();
    for (GenericTypeVar genericTypeVar : genericsOfClass) {
      // params.add(genericTypeVar.getTypePlaceholder());
      params.add(TypePlaceholder.fresh(offset));
    }
    return new RefType(name, params, offset);
  }

  /**
   * 
   * @return die aktuelle Klasse als RefType
   */
  public RefType generateTypeOfThisClass() {
    List<RefTypeOrTPHOrWildcardOrGeneric> params = new ArrayList<>();
    for (GenericTypeVar genericTypeVar : this.getGenerics()) {
      // params.add(genericTypeVar.getTypePlaceholder());
      params.add(new GenericRefType(genericTypeVar.getName(), new NullToken()));
    }
    return new RefType(name, params, new NullToken());
  }

  /**
   * Die Superklasse im Kontext dieser ClassOrInterface Das bedeutet, dass generische Variablen als GenericRefTypes dargestellt sind
   */
  public RefType getSuperClass() {
    return superClass;
  }

  public GenericDeclarationList getGenerics() {
    return this.genericClassParameters;
  }

  @Override
  public RefTypeOrTPHOrWildcardOrGeneric getReturnType() {
    return null;
  }

  public List<Constructor> getConstructors() {
    return constructors;
  }

  @Override
  public void accept(ASTVisitor visitor) {
    visitor.visit(this);
  }

  public Collection<RefType> getSuperInterfaces() {
    return implementedInterfaces;
  }

  public String toString() {
    return this.name.toString() + this.genericClassParameters.toString();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof ClassOrInterface other)) return false;
    return Objects.equals(name, other.name);
  }

  @Override
  public int hashCode() {
    return Objects.hash(name);
  }

  public Set<GenericTypeVar> getUserDefinedGenerics() {
    if (this.userDefinedGenerics != null) return this.userDefinedGenerics;

    var genericsIter = getGenerics().iterator();
    if (genericsIter.hasNext()) {
      // Add empty set of generics to cache so that it doesn't try to calculate it later
      this.userDefinedGenerics = new HashSet<>();
      while (genericsIter.hasNext()) {
        var next = genericsIter.next();
        userDefinedGenerics.add(next);
      }
    } else {
      this.userDefinedGenerics = new HashSet<>();
    }

    return this.userDefinedGenerics;
  }
}
