/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 2013-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */
package com.github.javaparser.resolution.types;

import com.github.javaparser.ast.AccessSpecifier;
import com.github.javaparser.resolution.MethodUsage;
import com.github.javaparser.resolution.declarations.ResolvedFieldDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration.Bound;
import com.github.javaparser.resolution.model.typesystem.LazyType;
import com.github.javaparser.resolution.types.parametrization.ResolvedTypeParameterValueProvider;
import com.github.javaparser.resolution.types.parametrization.ResolvedTypeParametersMap;
import com.github.javaparser.resolution.types.parametrization.ResolvedTypeParametrized;
import com.github.javaparser.utils.Pair;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * A ReferenceType like a class, an interface or an enum. Note that this type can contain also the values
 * specified for the type parameters.
 *
 * @author Federico Tomassetti
 */
public abstract class ResolvedReferenceType
        implements ResolvedType, ResolvedTypeParametrized, ResolvedTypeParameterValueProvider {

    protected static String JAVA_LANG_ENUM = java.lang.Enum.class.getCanonicalName();

    protected static String JAVA_LANG_OBJECT = java.lang.Object.class.getCanonicalName();

    // Can't use java.lang.Record.class.getCanonicalName() since records were only added in Java 14.
    protected static String JAVA_LANG_RECORD = "java.lang.Record";

    //
    // Fields
    //
    protected ResolvedReferenceTypeDeclaration typeDeclaration;

    protected ResolvedTypeParametersMap typeParametersMap;

    //
    // Constructors
    //
    public ResolvedReferenceType(ResolvedReferenceTypeDeclaration typeDeclaration) {
        this(typeDeclaration, deriveParams(typeDeclaration));
    }

    public ResolvedReferenceType(ResolvedReferenceTypeDeclaration typeDeclaration, List<ResolvedType> typeArguments) {
        if (typeDeclaration == null) {
            throw new IllegalArgumentException("TypeDeclaration is not expected to be null");
        }
        if (typeDeclaration.isTypeParameter()) {
            throw new IllegalArgumentException("You should use only Classes, Interfaces and enums");
        }
        if (typeArguments.size() > 0
                && typeArguments.size() != typeDeclaration.getTypeParameters().size()) {
            throw new IllegalArgumentException(String.format(
                    "expected either zero type arguments or has many as defined in the declaration (%d). Found %d",
                    typeDeclaration.getTypeParameters().size(), typeArguments.size()));
        }
        ResolvedTypeParametersMap.Builder typeParametersMapBuilder = new ResolvedTypeParametersMap.Builder();
        for (int i = 0; i < typeArguments.size(); i++) {
            typeParametersMapBuilder.setValue(
                    typeDeclaration.getTypeParameters().get(i), typeArguments.get(i));
        }
        this.typeParametersMap = typeParametersMapBuilder.build();
        this.typeDeclaration = typeDeclaration;
    }

    //
    // Public Object methods
    //
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (o instanceof LazyType) {
            final LazyType lazyType = (LazyType) o;
            if (!lazyType.isReferenceType()) return false;
            return this.equals(lazyType.asReferenceType());
        }
        if (getClass() != o.getClass()) return false;
        ResolvedReferenceType that = (ResolvedReferenceType) o;
        if (!typeDeclaration.equals(that.typeDeclaration)) return false;
        if (!typeParametersMap.equals(that.typeParametersMap)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int result = typeDeclaration.hashCode();
        result = 31 * result + typeParametersMap.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "ReferenceType{" + getQualifiedName() + ", typeParametersMap=" + typeParametersMap + '}';
    }

    // /
    // / Relation with other types
    // /
    @Override
    public final boolean isReferenceType() {
        return true;
    }

    // /
    // / Downcasting
    // /
    @Override
    public ResolvedReferenceType asReferenceType() {
        return this;
    }

    // /
    // / Naming
    // /
    @Override
    public String describe() {
        StringBuilder sb = new StringBuilder();
        if (hasName()) {
            sb.append(typeDeclaration.getQualifiedName());
        } else {
            sb.append("<anonymous class>");
        }
        if (!typeParametersMap().isEmpty()) {
            sb.append("<");
            sb.append(String.join(
                    ", ",
                    typeDeclaration.getTypeParameters().stream()
                            .map(tp -> typeParametersMap().getValue(tp).describe())
                            .collect(Collectors.toList())));
            sb.append(">");
        }
        return sb.toString();
    }

    // /
    // / TypeParameters
    // /
    /**
     * Execute a transformation on all the type parameters of this element.
     */
    public abstract ResolvedType transformTypeParameters(ResolvedTypeTransformer transformer);

    @Override
    public ResolvedType replaceTypeVariables(
            ResolvedTypeParameterDeclaration tpToReplace,
            ResolvedType replaced,
            Map<ResolvedTypeParameterDeclaration, ResolvedType> inferredTypes) {
        if (replaced == null) {
            throw new IllegalArgumentException();
        }
        ResolvedReferenceType result = this;
        int i = 0;
        for (ResolvedType tp : this.typeParametersValues()) {
            ResolvedType transformedTp = tp.replaceTypeVariables(tpToReplace, replaced, inferredTypes);
            // Identity comparison on purpose
            if (tp.isTypeVariable() && tp.asTypeVariable().describe().equals(tpToReplace.getName())) {
                inferredTypes.put(tp.asTypeParameter(), replaced);
            }
            // FIXME
            if (true) {
                List<ResolvedType> typeParametersCorrected =
                        result.asReferenceType().typeParametersValues();
                typeParametersCorrected.set(i, transformedTp);
                result = create(typeDeclaration, typeParametersCorrected);
            }
            i++;
        }
        List<ResolvedType> values = result.typeParametersValues();
        // FIXME
        if (values.contains(tpToReplace)) {
            int index = values.indexOf(tpToReplace);
            values.set(index, replaced);
            if (result.getTypeDeclaration().isPresent()) {
                return create(result.getTypeDeclaration().get(), values);
            }
        }
        return result;
    }

    // /
    // / Assignability
    // /
    /**
     * This method checks if ThisType t = new OtherType() would compile.
     */
    @Override
    public abstract boolean isAssignableBy(ResolvedType other);

    // /
    // / Ancestors
    // /
    /**
     * Return all ancestors, that means all superclasses and interfaces.
     * This list should always include Object (unless this is a reference to Object).
     * The type typeParametersValues should be expressed in terms of this type typeParametersValues.
     * The default order of presenting ancestors corresponds to a search in depth.
     * <p>
     * For example, given:
     * <p>
     * class Foo&lt;A, B&gt; {}
     * class Bar&lt;C&gt; extends Foo&lt;C, String&gt; {}
     * <p>
     * a call to getAllAncestors on a reference to Bar having type parameter Boolean should include
     * Foo&lt;Boolean, String&gt;.
     */
    public abstract List<ResolvedReferenceType> getAllAncestors();

    /**
     * Return all ancestors, that means all superclasses and interfaces.
     * This list should always include Object (unless this is a reference to Object).
     * The type typeParametersValues should be expressed in terms of this type typeParametersValues.
     */
    public abstract List<ResolvedReferenceType> getAllAncestors(
            Function<ResolvedReferenceTypeDeclaration, List<ResolvedReferenceType>> traverser);

    /**
     * Return direct ancestors, that means the superclasses and interfaces implemented directly.
     * This list should include Object if the class has no other superclass or the interface is not extending another interface.
     * There is an exception for Object itself.
     */
    public abstract List<ResolvedReferenceType> getDirectAncestors();

    public final List<ResolvedReferenceType> getAllInterfacesAncestors() {
        return getAllAncestors().stream()
                .filter(it -> it.getTypeDeclaration().isPresent())
                .filter(it -> it.getTypeDeclaration().get().isInterface())
                .collect(Collectors.toList());
    }

    public final List<ResolvedReferenceType> getAllClassesAncestors() {
        return getAllAncestors().stream()
                .filter(it -> it.getTypeDeclaration().isPresent())
                .filter(it -> it.getTypeDeclaration().get().isClass())
                .collect(Collectors.toList());
    }

    // /
    // / Type parameters
    // /
    /**
     * Get the type associated with the type parameter with the given name.
     * It returns Optional.empty unless the type declaration declares a type parameter with the given name.
     */
    @Override
    public Optional<ResolvedType> getGenericParameterByName(String name) {
        for (ResolvedTypeParameterDeclaration tp : typeDeclaration.getTypeParameters()) {
            if (tp.getName().equals(name)) {
                return Optional.of(this.typeParametersMap().getValue(tp));
            }
        }
        return Optional.empty();
    }

    /**
     * Get the values for all type parameters declared on this type.
     * The list can be empty for raw types.
     */
    public List<ResolvedType> typeParametersValues() {
        return this.typeParametersMap.isEmpty()
                ? Collections.emptyList()
                : typeDeclaration.getTypeParameters().stream()
                        .map(tp -> typeParametersMap.getValue(tp))
                        .collect(Collectors.toList());
    }

    /**
     * Get the values for all type parameters declared on this type.
     * In case of raw types the values correspond to TypeVariables.
     */
    public List<Pair<ResolvedTypeParameterDeclaration, ResolvedType>> getTypeParametersMap() {
        List<Pair<ResolvedTypeParameterDeclaration, ResolvedType>> typeParametersMap = new ArrayList<>();
        if (!isRawType()) {
            for (int i = 0; i < typeDeclaration.getTypeParameters().size(); i++) {
                typeParametersMap.add(new Pair<>(
                        typeDeclaration.getTypeParameters().get(i),
                        typeParametersValues().get(i)));
            }
        }
        return typeParametersMap;
    }

    @Override
    public ResolvedTypeParametersMap typeParametersMap() {
        return typeParametersMap;
    }

    // /
    // / Other methods introduced by ReferenceType
    // /
    /**
     * Corresponding TypeDeclaration
     */
    public final Optional<ResolvedReferenceTypeDeclaration> getTypeDeclaration() {
        return Optional.of(typeDeclaration);
    }

    /**
     * The type of the field could be different from the one in the corresponding FieldDeclaration because
     * type variables would be solved.
     */
    public Optional<ResolvedType> getFieldType(String name) {
        if (!typeDeclaration.hasField(name)) {
            return Optional.empty();
        }
        ResolvedType type = typeDeclaration.getField(name).getType();
        type = useThisTypeParametersOnTheGivenType(type);
        return Optional.of(type);
    }

    /**
     * Has the TypeDeclaration a name? Anonymous classes do not have one.
     */
    public boolean hasName() {
        return typeDeclaration.hasName();
    }

    /**
     * Qualified name of the declaration.
     */
    public String getQualifiedName() {
        return typeDeclaration.getQualifiedName();
    }

    /**
     * Id of the declaration. It corresponds to the qualified name, unless for local classes.
     */
    public String getId() {
        return typeDeclaration.getId();
    }

    /**
     * Methods declared on this type.
     */
    public abstract Set<MethodUsage> getDeclaredMethods();

    /**
     * Fields declared on this type.
     */
    public abstract Set<ResolvedFieldDeclaration> getDeclaredFields();

    /*
     * A class or interface whose declaration has one or more type parameters is a
     * generic class or interface [JLS, 8.1.2, 9.1.2]. For example, the List
     * interface has a single type parameter, E, representing its element type.
     * A raw type, is the name of the generic type used without any accompanying type
     * parameters [JLS, 4.8]. For example, the raw type corresponding to List<E> is
     * List.
     */
    public boolean isRawType() {
        if (!typeDeclaration.getTypeParameters().isEmpty()) {
            if (typeParametersMap().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Optional<ResolvedType> typeParamValue(ResolvedTypeParameterDeclaration typeParameterDeclaration) {
        if (typeParameterDeclaration.declaredOnMethod()) {
            throw new IllegalArgumentException();
        }
        if (!this.getTypeDeclaration().isPresent()) {
            // TODO: Consider IllegalStateException or similar
            return Optional.empty();
        }
        String typeId = this.getTypeDeclaration().get().getId();
        if (typeId.equals(typeParameterDeclaration.getContainerId())) {
            return Optional.of(this.typeParametersMap().getValue(typeParameterDeclaration));
        }
        for (ResolvedReferenceType ancestor : this.getAllAncestors()) {
            if (ancestor.getId().equals(typeParameterDeclaration.getContainerId())) {
                return Optional.of(ancestor.typeParametersMap().getValue(typeParameterDeclaration));
            }
        }
        return Optional.empty();
    }

    /**
     * @return A copy of the current reference type, without type parameters.
     */
    public abstract ResolvedType toRawType();

    /**
     * Get a list of all the methods available on this type. This list includes methods declared in this type and
     * methods inherited. This list includes methods of all sort of visibility. However it does not include methods
     * that have been overwritten.
     */
    public List<ResolvedMethodDeclaration> getAllMethods() {
        if (!this.getTypeDeclaration().isPresent()) {
            // empty list -- consider IllegalStateException or similar
            return new ArrayList<>();
        }
        // Get the methods declared directly on this.
        List<ResolvedMethodDeclaration> allMethods =
                new LinkedList<>(this.getTypeDeclaration().get().getDeclaredMethods());
        // Also get methods inherited from ancestors.
        getDirectAncestors().forEach(a -> allMethods.addAll(a.getAllMethods()));
        return allMethods;
    }

    /**
     * Fields which are visible to inheritors. They include all inherited fields which are visible to this
     * type plus all declared fields which are not private.
     */
    public List<ResolvedFieldDeclaration> getAllFieldsVisibleToInheritors() {
        List<ResolvedFieldDeclaration> res = new LinkedList<>(this.getDeclaredFields().stream()
                .filter(f -> f.accessSpecifier() != AccessSpecifier.PRIVATE)
                .collect(Collectors.toList()));
        getDirectAncestors().forEach(a -> res.addAll(a.getAllFieldsVisibleToInheritors()));
        return res;
    }

    public List<ResolvedMethodDeclaration> getAllMethodsVisibleToInheritors() {
        return this.getAllMethods().stream()
                .filter(m -> m.accessSpecifier() != AccessSpecifier.PRIVATE)
                .collect(Collectors.toList());
    }

    //
    // Protected methods
    //
    protected abstract ResolvedReferenceType create(
            ResolvedReferenceTypeDeclaration typeDeclaration, List<ResolvedType> typeParameters);

    protected ResolvedReferenceType create(
            ResolvedReferenceTypeDeclaration typeDeclaration, ResolvedTypeParametersMap typeParametersMap) {
        return create(
                typeDeclaration,
                typeDeclaration.getTypeParameters().stream()
                        .map(typeParametersMap::getValue)
                        .collect(Collectors.toList()));
    }

    protected abstract ResolvedReferenceType create(ResolvedReferenceTypeDeclaration typeDeclaration);

    /*
     * Verify if the resolved type is a boxing type of a primitive
     */
    protected boolean isCorrespondingBoxingType(String typeName) {
        ResolvedPrimitiveType resolvedPrimitiveType = (ResolvedPrimitiveType) ResolvedPrimitiveType.byName(typeName);
        return getQualifiedName().equals(resolvedPrimitiveType.getBoxTypeQName());
    }

    protected boolean compareConsideringTypeParameters(ResolvedReferenceType other) {
        if (other.equals(this)) {
            return true;
        }
        if (this.getQualifiedName().equals(other.getQualifiedName())) {
            if (this.isRawType() || other.isRawType()) {
                return true;
            }
            List<ResolvedType> typeParametersValues = typeParametersValues();
            if (typeParametersValues.size() != other.typeParametersValues().size()) {
                throw new IllegalStateException();
            }
            for (int i = 0; i < typeParametersValues.size(); i++) {
                ResolvedType thisParam = typeParametersValues.get(i);
                ResolvedType otherParam = other.typeParametersValues().get(i);
                if (!thisParam.equals(otherParam)) {
                    if (thisParam instanceof ResolvedWildcard) {
                        ResolvedWildcard thisParamAsWildcard = (ResolvedWildcard) thisParam;
                        if (thisParamAsWildcard.isSuper()
                                && otherParam.isAssignableBy(thisParamAsWildcard.getBoundedType())) {
                            // ok
                        } else if (thisParamAsWildcard.isExtends()
                                && thisParamAsWildcard.getBoundedType().isAssignableBy(otherParam)) {
                            // ok
                        } else if (!thisParamAsWildcard.isBounded()) {
                            // ok
                        } else {
                            return false;
                        }
                    } else {
                        if (thisParam instanceof ResolvedTypeVariable && otherParam instanceof ResolvedTypeVariable) {
                            // Here we want to compare something like @{code C extends Comparable<C>} with @{code K
                            // extends Comparable<K>}
                            // we have to compare the type of the erased bound (in this example the type @{code
                            // Comparable}).
                            List<ResolvedType> thisBounds =
                                    thisParam.asTypeVariable().asTypeParameter().getBounds().stream()
                                            .map(ResolvedTypeParameterDeclaration.Bound::getType)
                                            .map(type -> type.erasure())
                                            .collect(Collectors.toList());
                            List<ResolvedType> otherBounds =
                                    otherParam.asTypeVariable().asTypeParameter().getBounds().stream()
                                            .map(ResolvedTypeParameterDeclaration.Bound::getType)
                                            .map(type -> type.erasure())
                                            .collect(Collectors.toList());
                            return thisBounds.size() == otherBounds.size() && otherBounds.containsAll(thisBounds);
                        }
                        if (!(thisParam instanceof ResolvedTypeVariable)
                                && otherParam instanceof ResolvedTypeVariable) {
                            return compareConsideringVariableTypeParameters(
                                    thisParam, (ResolvedTypeVariable) otherParam);
                        }
                        if (thisParam instanceof ResolvedTypeVariable
                                && !(otherParam instanceof ResolvedTypeVariable)) {
                            return compareConsideringVariableTypeParameters(
                                    otherParam, (ResolvedTypeVariable) thisParam);
                        }
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    //
    // Private methods
    //
    private boolean compareConsideringVariableTypeParameters(
            ResolvedType referenceType, ResolvedTypeVariable typeVariable) {
        // verify if the ResolvedTypeVariable has only one type variable and the bound is
        // not a reference type with a bound parameter
        // for example EnumSet<E> noneOf(Class<E> elementType)
        List<Bound> bounds = typeVariable.asTypeVariable().asTypeParameter().getBounds();
        if (bounds.size() == 1) {
            ResolvedType boundType = bounds.get(0).getType();
            boolean hasTypeParameter = boundType.isReferenceType()
                    && !boundType.asReferenceType().typeParametersMap.isEmpty();
            return hasTypeParameter
                    ? compareConsideringTypeParameters(boundType.asReferenceType())
                    : boundType.isAssignableBy(referenceType);
        }
        return false;
    }

    private static List<ResolvedType> deriveParams(ResolvedReferenceTypeDeclaration typeDeclaration) {
        if (typeDeclaration == null) {
            throw new IllegalArgumentException("TypeDeclaration is not expected to be null");
        }
        List<ResolvedTypeParameterDeclaration> typeParameters = typeDeclaration.getTypeParameters();
        if (typeParameters == null) {
            throw new RuntimeException("Type parameters are not expected to be null");
        }
        return typeParameters.stream().map(ResolvedTypeVariable::new).collect(Collectors.toList());
    }

    public abstract ResolvedReferenceType deriveTypeParameters(ResolvedTypeParametersMap typeParametersMap);

    /**
     * We don't make this _ex_plicit in the data representation because that would affect codegen
     * and make everything generate like {@code <T extends Object>} instead of {@code <T>}
     *
     * @return true, if this represents {@code java.lang.Object}
     * @see ResolvedReferenceTypeDeclaration#isJavaLangObject()
     * @see <a href="https://github.com/javaparser/javaparser/issues/2044">https://github.com/javaparser/javaparser/issues/2044</a>
     */
    public boolean isJavaLangObject() {
        return this.isReferenceType()
                && // Consider anonymous classes
                hasName()
                && getQualifiedName().equals(JAVA_LANG_OBJECT);
    }

    /**
     * @return true, if this represents {@code java.lang.Enum}
     * @see ResolvedReferenceTypeDeclaration#isJavaLangEnum()
     */
    public boolean isJavaLangEnum() {
        return this.isReferenceType()
                && // Consider anonymous classes
                hasName()
                && getQualifiedName().equals(JAVA_LANG_ENUM);
    }

    /**
     * @return true, if this represents {@code java.lang.Record}
     * @see ResolvedReferenceTypeDeclaration#isJavaLangRecord()
     */
    public boolean isJavaLangRecord() {
        return this.isReferenceType()
                && // Consider anonymous classes
                hasName()
                && getQualifiedName().equals(JAVA_LANG_RECORD);
    }

    // /
    // / boxing/unboxing capability
    // /
    /*
     * Returns true if the reference type can be unboxed to the primitive type
     * For example : Integer to int
     */
    public boolean isUnboxable() {
        return Arrays.stream(ResolvedPrimitiveType.values())
                .anyMatch(pt -> getQualifiedName().equals(pt.getBoxTypeQName()));
    }

    /*
     * Returns true if the reference type can be unboxed to the specified primitive type
     * For example : Integer to int
     */
    public boolean isUnboxableTo(ResolvedPrimitiveType primitiveType) {
        return primitiveType.getBoxTypeQName().equals(this.asReferenceType().describe());
    }

    /*
     * Returns the optional corresponding primitive type
     */
    public Optional<ResolvedPrimitiveType> toUnboxedType() {
        return Arrays.stream(ResolvedPrimitiveType.values())
                .filter(pt -> this.asReferenceType().getQualifiedName().equals(pt.getBoxTypeQName()))
                .findFirst();
    }

    // /
    // / Erasure
    // /
    // The erasure of a parameterized type (§4.5) G<T1,...,Tn> is |G|.
    @Override
    public ResolvedType erasure() {
        if (!typeDeclaration.isGeneric()) return this;
        return create(typeDeclaration, erasureOfParamaters(typeParametersMap));
    }

    private List<ResolvedType> erasureOfParamaters(ResolvedTypeParametersMap typeParametersMap) {
        return new ArrayList<ResolvedType>();
    }

    private boolean isJavaObject(ResolvedType rt) {
        return rt != null && rt.isReferenceType() && rt.asReferenceType().isJavaLangObject();
    }

    @Override
    public String toDescriptor() {
        return String.format("L%s;", getQualifiedName().replace(".", "/"));
    }
}
