/*
 * Copyright (c) 1997, 2022 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package com.sun.codemodel;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * A generated Java class/interface/enum/....
 *
 * <p>
 * This class models a declaration, and since a declaration can be always
 * used as a reference, it inherits {@link JClass}.
 *
 * <h2>Where to go from here?</h2>
 * <p>
 * You'd want to generate fields and methods on a class.
 * See {@link #method(int, JType, String)} and {@link #field(int, JType, String)}.
 */
public class JDefinedClass
    extends JClass
    implements JDeclaration, JClassContainer, JGenerifiable, JAnnotatable, JDocCommentable {

    /** Name of this class. Null if anonymous. */
    private String name;
    
    /** Modifiers for the class declaration */
    private JMods mods;

    /** Name of the super class of this class. */
    private JClass superClass;

    /** List of interfaces that this class implements */
    private final Set<JClass> interfaces = new TreeSet<>();

    /** Fields keyed by their names. */
    /*package*/ final Map<String,JFieldVar> fields = new LinkedHashMap<>();

    /** Static initializer, if this class has one */
    private JBlock init = null;
    
    /** Instance initializer, if this class has one */
    private JBlock instanceInit = null;

    /** class javadoc */
    private JDocComment jdoc = null;

    /** Set of constructors for this class, if any */
    private final List<JMethod> constructors = new ArrayList<>();

    /** Set of methods that are members of this class */
    private final List<JMethod> methods = new ArrayList<>();

    /**
     * Nested classes as a map from name to JDefinedClass.
     * The name is all capitalized in a case sensitive file system
     * ({@link JCodeModel#isCaseSensitiveFileSystem}) to avoid conflicts.
     *
     * Lazily created to save footprint.
     *
     * @see #getClasses()
     */
    private Map<String,JDefinedClass> classes;


    /**
     * Flag that controls whether this class should be really generated or not.
     * 
     * Sometimes it is useful to generate code that refers to class X,
     * without actually generating the code of X.
     * This flag is used to surpress X.java file in the output.
     */
    private boolean hideFile = false;

    /**
     * Client-app spcific metadata associated with this user-created class.
     */
    public Object metadata;

    /**
     * String that will be put directly inside the generated code.
     * Can be null.
     */
    private String directBlock;

    /**
     * If this is a package-member class, this is {@link JPackage}.
     * If this is a nested class, this is {@link JDefinedClass}.
     * If this is an anonymous class, this constructor shouldn't be used.
     */
    private JClassContainer outer;

    
    /** Default value is class or interface
     *  or annotationTypeDeclaration
     *  or enum
     * 
     */
    private final ClassType classType;
    
    /* List containing the enum value declarations
     *  
     */
//    private List enumValues = new ArrayList();
    
    /**
     * Set of enum constants that are keyed by names.
     * In Java, enum constant order is actually significant,
     * because of order ID they get. So let's preserve the order.
     */
    private final Map<String,JEnumConstant> enumConstantsByName = new LinkedHashMap<>();

    /**
     * Annotations on this variable. Lazily created.
     */
    private List<JAnnotationUse> annotations = null;


    /**
     * Helper class to implement {@link JGenerifiable}.
     */
    private final JGenerifiableImpl generifiable = new JGenerifiableImpl() {
        @Override
        protected JCodeModel owner() {
            return JDefinedClass.this.owner();
        }
    };

    JDefinedClass(JClassContainer parent, int mods, String name, ClassType classTypeval) {
        this(mods, name, parent, parent.owner(), classTypeval);
    }

    /**
     * Constructor for creating anonymous inner class.
     */
    JDefinedClass(
        JCodeModel owner,
        int mods,
        String name) {
        this(mods, name, null, owner);
    }
    
    private JDefinedClass(
            int mods,
            String name,
            JClassContainer parent,
            JCodeModel owner) {
    	this (mods,name,parent,owner,ClassType.CLASS);
    }

    /**
     * JClass constructor
     *
     * @param mods
     *        Modifiers for this class declaration
     *
     * @param name
     *        Name of this class
     */
    private JDefinedClass(
        int mods,
        String name,
        JClassContainer parent,
        JCodeModel owner,
		ClassType classTypeVal) {
        super(owner);

        if(name!=null) {
            if (name.trim().length() == 0)
                throw new IllegalArgumentException("JClass name empty");
    
            if (!Character.isJavaIdentifierStart(name.charAt(0))) {
                String msg =
                    "JClass name "
                        + name
                        + " contains illegal character"
                        + " for beginning of identifier: "
                        + name.charAt(0);
                throw new IllegalArgumentException(msg);
            }
            for (int i = 1; i < name.length(); i++) {
                if (!Character.isJavaIdentifierPart(name.charAt(i))) {
                    String msg =
                        "JClass name "
                            + name
                            + " contains illegal character "
                            + name.charAt(i);
                    throw new IllegalArgumentException(msg);
                }
            }
        }

        this.classType = classTypeVal;
        if (isInterface())
            this.mods = JMods.forInterface(mods);
        else
            this.mods = JMods.forClass(mods);

        this.name = name;

        this.outer = parent;
    }

    /**
     * Returns true if this is an anonymous class.
     */
    public final boolean isAnonymous() {
        return name == null;
    }

    /**
     * This class extends the specifed class.
     *
     * @param superClass
     *        Superclass for this class
     *
     * @return This class
     */
    public JDefinedClass _extends(JClass superClass) {
        if (this.classType==ClassType.INTERFACE)
        	if(superClass.isInterface()){
        		return this._implements(superClass);
        	} else throw new IllegalArgumentException("unable to set the super class for an interface");
        if (superClass == null)
            throw new NullPointerException();
        
        for( JClass o=superClass.outer(); o!=null; o=o.outer() ){
            if(this==o){
                throw new IllegalArgumentException("Illegal class inheritance loop." +
                "  Outer class " + this.name + " may not subclass from inner class: " + o.name());
            }
        }
        
        this.superClass = superClass;
        return this;
    }

    public JDefinedClass _extends(Class<?> superClass) {
        return _extends(owner().ref(superClass));
    }

    /**
     * Returns the class extended by this class.
     */
    @Override
    public JClass _extends() {
        if(superClass==null)
            superClass = owner().ref(Object.class);
        return superClass;
    }

    /**
     * This class implements the specifed interface.
     *
     * @param iface
     *        Interface that this class implements
     *
     * @return This class
     */
    public JDefinedClass _implements(JClass iface) {
        interfaces.add(iface);
        return this;
    }

    public JDefinedClass _implements(Class<?> iface) {
        return _implements(owner().ref(iface));
    }

    /**
     * Returns an iterator that walks the nested classes defined in this
     * class.
     */
    @Override
    public Iterator<JClass> _implements() {
        return interfaces.iterator();
    }

    /**
     * JClass name accessor.
     * 
     * <p>
     * For example, for <code>java.util.List</code>, this method
     * returns <code>"List"</code>"
     *
     * @return Name of this class
     */
    @Override
    public String name() {
        return name;
    }
    
    /**
     * If the named enum already exists, the reference to it is returned.
     * Otherwise this method generates a new enum reference with the given
     * name and returns it.
     *
     * @param name
     *  	The name of the constant.
     * @return
     *      The generated type-safe enum constant.
     */
    public JEnumConstant enumConstant(String name){
        JEnumConstant ec = enumConstantsByName.get(name);
        if (null == ec) {
            ec = new JEnumConstant(this, name);
            enumConstantsByName.put(name, ec);
        }
        return ec;
    }

    /**
     * Returns set of enum constants keyed by names.
     *
     * @return Set of enum constants keyed by names
     */
    public Map<String, JEnumConstant> enumConstants() {
        return Collections.unmodifiableMap(enumConstantsByName);
    }

    /**
     * Gets the fully qualified name of this class.
     */
    @Override
    public String fullName() {
        if (outer instanceof JDefinedClass)
            return ((JDefinedClass) outer).fullName() + '.' + name();

        JPackage p = _package();
        if (p.isUnnamed())
            return name();
        else
            return p.name() + '.' + name();
    }

    @Override
    public String binaryName() {
        if (outer instanceof JDefinedClass)
            return ((JDefinedClass) outer).binaryName() + '$' + name();
        else
            return fullName();
    }

    @Override
    public boolean isInterface() {
        return this.classType==ClassType.INTERFACE;
    }

    @Override
    public boolean isAbstract() {
        return mods.isAbstract();
    }

    /**
     * Adds a field to the list of field members of this JDefinedClass.
     *
     * @param mods
     *        Modifiers for this field
     *
     * @param type
     *        JType of this field
     *
     * @param name
     *        Name of this field
     *
     * @return Newly generated field
     */
    public JFieldVar field(int mods, JType type, String name) {
        return field(mods, type, name, null);
    }

    public JFieldVar field(int mods, Class<?> type, String name) {
        return field(mods, owner()._ref(type), name);
    }

    /**
     * Adds a field to the list of field members of this JDefinedClass.
     *
     * @param mods
     *        Modifiers for this field.
     * @param type
     *        JType of this field.
     * @param name
     *        Name of this field.
     * @param init
     *        Initial value of this field.
     *
     * @return Newly generated field
     */
    public JFieldVar field(
        int mods,
        JType type,
        String name,
        JExpression init) {
        JFieldVar f = new JFieldVar(this,JMods.forField(mods), type, name, init);

        if (fields.containsKey(name)) {
            throw new IllegalArgumentException("trying to create the same field twice: "+name);
        }
        
        fields.put(name, f);
        return f;
    }

    /**  This method indicates if the interface
     *   is an annotationTypeDeclaration
     *
     */
    public boolean isAnnotationTypeDeclaration() {
        return this.classType==ClassType.ANNOTATION_TYPE_DECL;
        

    }

    /**
     * Add an annotationType Declaration to this package
     * @param name
     *      Name of the annotation Type declaration to be added to this package
     * @return
     *      newly created Annotation Type Declaration
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     
     */
    @Override
    public JDefinedClass _annotationTypeDeclaration(String name) throws JClassAlreadyExistsException {
    	return _class (JMod.PUBLIC,name,ClassType.ANNOTATION_TYPE_DECL);
    }
   
    /**
     * Add a public enum to this package
     * @param name
     *      Name of the enum to be added to this package
     * @return
     *      newly created Enum
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     
     */
    @Override
    public JDefinedClass _enum (String name) throws JClassAlreadyExistsException {
    	return _class (JMod.PUBLIC,name,ClassType.ENUM);
    }
    
    /**
     * Add a public enum to this package
     * @param name
     *      Name of the enum to be added to this package
     * @param mods
     * 		Modifiers for this enum declaration
     * @return
     *      newly created Enum
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     
     */
    public JDefinedClass _enum (int mods,String name) throws JClassAlreadyExistsException {
    	return _class (mods,name,ClassType.ENUM);
    }
    
    

    

    public ClassType getClassType(){
        return this.classType;
    }
    
    public JFieldVar field(
        int mods,
        Class<?> type,
        String name,
        JExpression init) {
        return field(mods, owner()._ref(type), name, init);
    }

    /**
     * Returns all the fields declred in this class.
     * The returned {@link Map} is a read-only live view.
     *
     * @return always non-null.
     */
    public Map<String,JFieldVar> fields() {
        return Collections.unmodifiableMap(fields);
    }

    /**
     * Removes a {@link JFieldVar} from this class.
     *
     * @throws IllegalArgumentException
     *      if the given field is not a field on this class. 
     */
    public void removeField(JFieldVar field) {
        if(fields.remove(field.name())!=field)
            throw new IllegalArgumentException();
    }

    /**
     * Creates, if necessary, and returns the static initializer
     * for this class.
     *
     * @return JBlock containing initialization statements for this class
     */
    public JBlock init() {
        if (init == null)
            init = new JBlock();
        return init;
    }

    /**
     * Creates, if necessary, and returns the instance initializer
     * for this class.
     *
     * @return JBlock containing initialization statements for this class
     */
    public JBlock instanceInit() {
        if (instanceInit == null)
            instanceInit = new JBlock();
        return instanceInit;
    }
    
    /**
     * Adds a constructor to this class.
     *
     * @param mods
     *        Modifiers for this constructor
     */
    public JMethod constructor(int mods) {
        JMethod c = new JMethod(mods, this);
        constructors.add(c);
        return c;
    }

    /**
     * Returns an iterator that walks the constructors defined in this class.
     */
    public Iterator<JMethod> constructors() {
        return constructors.iterator();
    }

    /**
     * Looks for a method that has the specified method signature
     * and return it.
     * 
     * @return
     *      null if not found.
     */
    public JMethod getConstructor(JType[] argTypes) {
        for (JMethod m : constructors) {
            if (m.hasSignature(argTypes))
                return m;
        }
        return null;
    }

    /**
     * Add a method to the list of method members of this JDefinedClass instance.
     *
     * @param mods
     *        Modifiers for this method
     *
     * @param type
     *        Return type for this method
     *
     * @param name
     *        Name of the method
     *
     * @return Newly generated JMethod
     */
    public JMethod method(int mods, JType type, String name) {
        // XXX problems caught in M constructor
        JMethod m = new JMethod(this, mods, type, name);
        methods.add(m);
        return m;
    }

    public JMethod method(int mods, Class<?> type, String name) {
        return method(mods, owner()._ref(type), name);
    }

    /**
     * Returns the set of methods defined in this class.
     */
    public Collection<JMethod> methods() {
        return methods;
    }

    /**
     * Looks for a method that has the specified method signature
     * and return it.
     * 
     * @return
     *      null if not found.
     */
    public JMethod getMethod(String name, JType[] argTypes) {
        for (JMethod m : methods) {
            if (!m.name().equals(name))
                continue;

            if (m.hasSignature(argTypes))
                return m;
        }
        return null;
    }

    @Override
    public boolean isClass() {
        return true;
    }
    @Override
    public boolean isPackage() {
        return false;
    }
    @Override
    public JPackage getPackage() { return parentContainer().getPackage(); }

    /**
     * Add a new nested class to this class.
     *
     * @param mods
     *        Modifiers for this class declaration
     *
     * @param name
     *        Name of class to be added to this package
     *
     * @return Newly generated class
     */
    @Override
    public JDefinedClass _class(int mods, String name)
        throws JClassAlreadyExistsException {
        return _class(mods, name, ClassType.CLASS);
    }

    /**
     *
     *
     * @deprecated
     */
    @Deprecated
    @Override
    public JDefinedClass _class(int mods, String name, boolean isInterface) throws JClassAlreadyExistsException {
    	return _class(mods,name,isInterface?ClassType.INTERFACE:ClassType.CLASS);
    }

    @Override
    public JDefinedClass _class(int mods, String name, ClassType classTypeVal)
        throws JClassAlreadyExistsException {

        String NAME;
        if (JCodeModel.isCaseSensitiveFileSystem)
            NAME = name.toUpperCase();
        else
            NAME = name;

        if (getClasses().containsKey(NAME))
            throw new JClassAlreadyExistsException(getClasses().get(NAME));
        else {
            // XXX problems caught in the NC constructor
            JDefinedClass c = new JDefinedClass(this, mods, name, classTypeVal);
            getClasses().put(NAME,c);
            return c;
        }
    }

    /**
     * Add a new public nested class to this class.
     */
    @Override
    public JDefinedClass _class(String name)
        throws JClassAlreadyExistsException {
        return _class(JMod.PUBLIC, name);
    }

    /**
     * Add an interface to this package.
     *
     * @param mods
     *        Modifiers for this interface declaration
     *
     * @param name
     *        Name of interface to be added to this package
     *
     * @return Newly generated interface
     */
    @Override
    public JDefinedClass _interface(int mods, String name)
        throws JClassAlreadyExistsException {
        return _class(mods, name, ClassType.INTERFACE);
    }

    /**
     * Adds a public interface to this package.
     */
    @Override
    public JDefinedClass _interface(String name)
        throws JClassAlreadyExistsException {
        return _interface(JMod.PUBLIC, name);
    }

    /**
     * Creates, if necessary, and returns the class javadoc for this
     * JDefinedClass
     *
     * @return JDocComment containing javadocs for this class
     */
    @Override
    public JDocComment javadoc() {
        if (jdoc == null)
            jdoc = new JDocComment(owner());
        return jdoc;
    }

    /**
     * Mark this file as hidden, so that this file won't be
     * generated.
     * 
     * <p>
     * This feature could be used to generate code that refers
     * to class X, without actually generating X.java.
     */
    public void hide() {
        hideFile = true;
    }

    public boolean isHidden() {
        return hideFile;
    }

    /**
     * Returns an iterator that walks the nested classes defined in this
     * class.
     */
    @Override
    public final Iterator<JDefinedClass> classes() {
        if(classes==null)
            return Collections.emptyIterator();
        else
            return classes.values().iterator();
    }

    private Map<String,JDefinedClass> getClasses() {
        if(classes==null)
            classes = new TreeMap<>();
        return classes;
    }


    /**
     * Returns all the nested classes defined in this class.
     */
    public final JClass[] listClasses() {
        if(classes==null)
            return new JClass[0];
        else
            return classes.values().toArray(new JClass[0]);
    }

    @Override
    public JClass outer() {
        if (outer.isClass())
            return (JClass) outer;
        else
            return null;
    }

    @Override
    public void declare(JFormatter f) {
        if (jdoc != null)
            f.nl().g(jdoc);

        if (annotations != null){
            for (JAnnotationUse annotation : annotations)
                f.g(annotation).nl();
        }

        f.g(mods).p(classType.declarationToken).id(name).d(generifiable);

        if (superClass != null && superClass != owner().ref(Object.class))
            f.nl().i().p("extends").g(superClass).nl().o();

        if (!interfaces.isEmpty()) {
            if (superClass == null)
                f.nl();
            f.i().p(classType==ClassType.INTERFACE ? "extends" : "implements");
            f.g(interfaces);
            f.nl().o();
        }
        declareBody(f);
    }

    /**
     * prints the body of a class.
     */
    protected void declareBody(JFormatter f) {
        f.p('{').nl().nl().i();
        boolean first = true;

        if (!enumConstantsByName.isEmpty()) {
            for (JEnumConstant c : enumConstantsByName.values()) {
                if (!first) f.p(',').nl();
                f.d(c);
                first = false;
            }
        	f.p(';').nl();
        }

        for( JFieldVar field : fields.values() )
            f.d(field);
        if (init != null)
            f.nl().p("static").s(init);
        if (instanceInit != null)
            f.nl().s(instanceInit);
        for (JMethod m : constructors) {
            f.nl().d(m);
        }
        for (JMethod m : methods) {
            f.nl().d(m);
        }
        if(classes!=null)
            for (JDefinedClass dc : classes.values())
                f.nl().d(dc);

        
        if (directBlock != null)
            f.p(directBlock);
        f.nl().o().p('}').nl();
    }

    /**
     * Places the given string directly inside the generated class.
     * 
     * This method can be used to add methods/fields that are not
     * generated by CodeModel.
     * This method should be used only as the last resort.
     */
    public void direct(String string) {
        if (directBlock == null)
            directBlock = string;
        else
            directBlock += string;
    }

    @Override
    public final JPackage _package() {
        JClassContainer p = outer;
        while (p != null && !(p instanceof JPackage))
            p = p.parentContainer();
        return (JPackage) p;
    }

    @Override
    public final JClassContainer parentContainer() {
        return outer;
    }

    @Override
    public JTypeVar generify(String name) {
        return generifiable.generify(name);
    }
    @Override
    public JTypeVar generify(String name, Class<?> bound) {
        return generifiable.generify(name, bound);
    }
    @Override
    public JTypeVar generify(String name, JClass bound) {
        return generifiable.generify(name, bound);
    }
    @Override
    public JTypeVar[] typeParams() {
        return generifiable.typeParams();
    }

    @Override
    protected JClass substituteParams(
        JTypeVar[] variables,
        List<JClass> bindings) {
        return this;
    }

    /** Adding ability to annotate a class
     * @param clazz
     *          The annotation class to annotate the class with
     */
    @Override
    public JAnnotationUse annotate(Class <? extends Annotation> clazz){
        return annotate(owner().ref(clazz));
    }

    /** Adding ability to annotate a class
      * @param clazz
      *          The annotation class to annotate the class with
      */
    @Override
     public JAnnotationUse annotate(JClass clazz){
        if(annotations==null)
           annotations = new ArrayList<>();
        JAnnotationUse a = new JAnnotationUse(clazz);
        annotations.add(a);
        return a;
    }

    @Override
    public <W extends JAnnotationWriter<? extends Annotation>> W annotate2(Class<W> clazz) {
        return TypedAnnotationWriter.create(clazz,this);
    }

    @Override
    public boolean removeAnnotation(JAnnotationUse annotation) {
        return this.annotations.remove(annotation);
    }

    @Override
    public Collection<JAnnotationUse> annotations() {
        if (annotations == null)
            annotations = new ArrayList<>();
        return Collections.unmodifiableCollection(annotations);
    }

    /**
     * @return
     *      the current modifiers of this class.
     *      Always return non-null valid object.
     */
    public JMods mods() {
        return mods;
    }

    public JClass superClass() {
        return superClass;
    }
}
