/*
 * Copyright (c) 1997, 2023 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.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.sun.codemodel.writer.FileCodeWriter;
import com.sun.codemodel.writer.ProgressCodeWriter;

/**
 * Root of the code DOM.
 *
 * <p>
 * Here's your typical CodeModel application.
 *
 * <pre>{@code
 * JCodeModel cm = new JCodeModel();
 *
 * // generate source code by populating the 'cm' tree.
 * cm._class(...);
 * ...
 *
 * // write them out
 * cm.build(new File("."));
 * }</pre>
 *
 * <p>
 * Every CodeModel node is always owned by one {@link JCodeModel} object
 * at any given time (which can be often accesesd by the {@code owner()} method.)
 *
 * As such, when you generate Java code, most of the operation works
 * in a top-down fashion. For example, you create a class from {@link JCodeModel},
 * which gives you a {@link JDefinedClass}. Then you invoke a method on it
 * to generate a new method, which gives you {@link JMethod}, and so on.
 *
 * There are a few exceptions to this, most notably building {@link JExpression}s,
 * but generally you work with CodeModel in a top-down fashion.
 *
 * Because of this design, most of the CodeModel classes aren't directly instanciable.
 *
 *
 * <h2>Where to go from here?</h2>
 * <p>
 * Most of the time you'd want to populate new type definitions in a {@link JCodeModel}.
 * See {@link #_class(String, ClassType)}.
 */
public final class JCodeModel {
    
    /** The packages that this JCodeWriter contains. */
    private final HashMap<String,JPackage> packages = new HashMap<>();

    /** Java module in {@code module-info.java} file. */
    private JModule module;

    /** All JReferencedClasses are pooled here. */
    private final HashMap<Class<?>,JReferencedClass> refClasses = new HashMap<>();

    private final Map<String, String> classNameReplacer = new HashMap<>();

    /** Obtains a reference to the special "null" type. */
    public final JNullType NULL = new JNullType(this);
    // primitive types 
    public final JPrimitiveType VOID    = new JPrimitiveType(this,"void",   Void.class);
    public final JPrimitiveType BOOLEAN = new JPrimitiveType(this,"boolean",Boolean.class);
    public final JPrimitiveType BYTE    = new JPrimitiveType(this,"byte",   Byte.class);
    public final JPrimitiveType SHORT   = new JPrimitiveType(this,"short",  Short.class);
    public final JPrimitiveType CHAR    = new JPrimitiveType(this,"char",   Character.class);
    public final JPrimitiveType INT     = new JPrimitiveType(this,"int",    Integer.class);
    public final JPrimitiveType FLOAT   = new JPrimitiveType(this,"float",  Float.class);
    public final JPrimitiveType LONG    = new JPrimitiveType(this,"long",   Long.class);
    public final JPrimitiveType DOUBLE  = new JPrimitiveType(this,"double", Double.class);
    
    /**
     * If the flag is true, we will consider two classes "Foo" and "foo"
     * as a collision.
     */
    /* package */ static final boolean isCaseSensitiveFileSystem = getFileSystemCaseSensitivity();

    private static boolean getFileSystemCaseSensitivity() {
        try {
            // let the system property override, in case the user really
            // wants to override.
            if( System.getProperty("com.sun.codemodel.FileSystemCaseSensitive")!=null )
                return true;
        } catch( Exception e ) {}
        
        // on Unix, it's case sensitive.
        return (File.separatorChar == '/');
    }


    public JCodeModel() {}
    
    /**
     * Add a package to the list of packages to be generated.
     *
     * @param name
     *        Name of the package. Use "" to indicate the root package.
     *
     * @return Newly generated package
     */
    public JPackage _package(String name) {
        JPackage p = packages.get(name);
        if (p == null) {
            p = new JPackage(name, this);
            packages.put(name, p);
        }
        return p;
    }

    /**
     * Creates and returns Java module to be generated.
     * @param name The Name of Java module.
     * @return New Java module.
     */
    public JModule _moduleInfo(final String name) {
        return module = new JModule(name);
    }

    /**
     * Returns existing Java module to be generated.
     * @return Java module or {@code null} if Java module was not created yet.
     */
    public JModule _getModuleInfo() {
        return module;
    }

    /**
     * Creates Java module instance and adds existing packages with classes to the Java module info.
     * Used to initialize and build Java module instance with existing packages content.
     * @param name The Name of Java module.
     * @param requires Requires directives to add.
     * @throws IllegalStateException when Java module instance was not initialized.
     */
    public void _prepareModuleInfo(final String name, final String ...requires) {
        _moduleInfo(name);
        _updateModuleInfo(requires);
    }

    /**
     * Adds existing packages with classes to the Java module info.
     * Java module instance must exist before calling this method.
     * Used to update Java module instance with existing packages content after it was prepared on client side.
     * @param requires Requires directives to add.
     * @throws IllegalStateException when Java module instance was not initialized.
     */
    public void _updateModuleInfo(final String ...requires) {
        if (module == null) {
            throw new IllegalStateException("Java module instance was not initialized yet.");
        }
        module._exports(packages.values(), false);
        module._requires(requires);
    }

    public JPackage rootPackage() {
        return _package("");
    }

    /**
     * Returns an iterator that walks the packages defined using this code
     * writer.
     */
    public Iterator<JPackage> packages() {
        return packages.values().iterator();
    }
    
    /**
     * Creates a new generated class.
     * 
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     */
    public JDefinedClass _class(String fullyqualifiedName) throws JClassAlreadyExistsException {
        return _class(fullyqualifiedName,ClassType.CLASS);
    }

    /**
     * Creates a dummy, unknown {@link JClass} that represents a given name.
     *
     * <p>
     * This method is useful when the code generation needs to include the user-specified
     * class that may or may not exist, and only thing known about it is a class name.
     */
    public JClass directClass(String name) {
        return new JDirectClass(this,name);
    }

    /**
     * Creates a new generated class.
     *
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     */
    public JDefinedClass _class(int mods, String fullyqualifiedName,ClassType t) throws JClassAlreadyExistsException {
        int idx = fullyqualifiedName.lastIndexOf('.');
        if( idx<0 )     return rootPackage()._class(fullyqualifiedName);
        else
            return _package(fullyqualifiedName.substring(0,idx))
                ._class(mods, fullyqualifiedName.substring(idx+1), t );
    }

    /**
     * Creates a new generated class.
     *
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     */
    public JDefinedClass _class(String fullyqualifiedName,ClassType t) throws JClassAlreadyExistsException {
        return _class( JMod.PUBLIC, fullyqualifiedName, t );
    }

    /**
     * Gets a reference to the already created generated class.
     * 
     * @return null
     *      If the class is not yet created.
     * @see JPackage#_getClass(String)
     */
    public JDefinedClass _getClass(String fullyQualifiedName) {
        int idx = fullyQualifiedName.lastIndexOf('.');
        if( idx<0 )     return rootPackage()._getClass(fullyQualifiedName);
        else
            return _package(fullyQualifiedName.substring(0,idx))
                ._getClass( fullyQualifiedName.substring(idx+1) );
    }

    /**
     * Creates a new anonymous class.
     * 
     * @deprecated
     *      The naming convention doesn't match the rest of the CodeModel.
     *      Use {@link #anonymousClass(JClass)} instead.
     */
    @Deprecated
    public JDefinedClass newAnonymousClass(JClass baseType) {
        return new JAnonymousClass(baseType);
    }

    /**
     * Creates a new anonymous class.
     */
    public JDefinedClass anonymousClass(JClass baseType) {
        return new JAnonymousClass(baseType);
    }

    public JDefinedClass anonymousClass(Class<?> baseType) {
        return anonymousClass(ref(baseType));
    }
    
    /**
     * Generates Java source code.
     * A convenience method for <code>build(destDir,destDir,System.out)</code>.
     * 
     * @param	destDir
     *		source files are generated into this directory.
     * @param   status
     *      if non-null, progress indication will be sent to this stream.
     */
    public void build( File destDir, PrintStream status ) throws IOException {
        build(destDir,destDir,status);
    }

    /**
     * Generates Java source code.
     * A convenience method that calls {@link #build(CodeWriter,CodeWriter)}.
     *
     * @param	srcDir
     *		Java source files are generated into this directory.
     * @param	resourceDir
     *		Other resource files are generated into this directory.
     * @param   status
     *      if non-null, progress indication will be sent to this stream.
     */
    public void build( File srcDir, File resourceDir, PrintStream status ) throws IOException {
        CodeWriter src = new FileCodeWriter(srcDir);
        CodeWriter res = new FileCodeWriter(resourceDir);
        if(status!=null) {
            src = new ProgressCodeWriter(src, status );
            res = new ProgressCodeWriter(res, status );
        }
        build(src,res);
    }

    /**
     * A convenience method for <code>build(destDir,System.out)</code>.
     */
    public void build( File destDir ) throws IOException {
        build(destDir,System.out);
    }

    /**
     * A convenience method for <code>build(srcDir,resourceDir,System.out)</code>.
     */
    public void build( File srcDir, File resourceDir ) throws IOException {
        build(srcDir,resourceDir,System.out);
    }

    /**
     * A convenience method for <code>build(out,out)</code>.
     */
    public void build( CodeWriter out ) throws IOException {
        build(out,out);
    }
    
    /**
     * Generates Java source code.
     */
    public void build( CodeWriter source, CodeWriter resource ) throws IOException {
        JPackage[] pkgs = packages.values().toArray(new JPackage[0]);
        // avoid concurrent modification exception
        for( JPackage pkg : pkgs ) {
            pkg.build(source,resource);
        }
        if (module != null) {
            module.build(source);
        }
        source.close();
        resource.close();
    }

    /**
     * Returns the number of files to be generated if
     * {@link #build} is invoked now.
     */
    public int countArtifacts() {
        int r = 0;
        JPackage[] pkgs = packages.values().toArray(new JPackage[0]);
        // avoid concurrent modification exception
        for( JPackage pkg : pkgs )
            r += pkg.countArtifacts();
        return r;
    }

    /**
     * Specify class names or packages to be replaced when the model is dumped into files.
     * @param c1 the regular expression to which class name or package will be replaced.
     * @param c2 the string to be substituted for the first match.
     */
    public void addClassNameReplacer(String c1, String c2) {
        classNameReplacer.put(c1, c2);
    }

    /**
     * Gives an unmodifiable copy of classNameReplacer
     * @return classNameReplacer
     */
    public Map<String, String> classNameReplacer() {
        return Collections.unmodifiableMap(classNameReplacer);
    }

    /**
     * Obtains a reference to an existing class from its Class object.
     *
     * <p>
     * The parameter may not be primitive.
     *
     * @see #_ref(Class) for the version that handles more cases.
     */
    public JClass ref(Class<?> clazz) {
        JReferencedClass jrc = refClasses.get(clazz);
        if (jrc == null) {
            if (clazz.isPrimitive())
                throw new IllegalArgumentException(clazz+" is a primitive");
            if (clazz.isArray()) {
                return new JArrayClass(this, _ref(clazz.getComponentType()));
            } else {
                jrc = new JReferencedClass(clazz);
                refClasses.put(clazz, jrc);
            }
        }
        return jrc;
    }

    public JType _ref(Class<?> c) {
        if(c.isPrimitive())
            return JType.parse(this,c.getName());
        else
            return ref(c);
    }

    /**
     * Obtains a reference to an existing class from its fully-qualified
     * class name.
     *
     * <p>
     * First, this method attempts to load the class of the given name.
     * If that fails, we assume that the class is derived straight from
     * {@link Object}, and return a {@link JClass}.
     */
    public JClass ref(String fullyQualifiedClassName) {
        try {
            // try the context class loader first
            return ref(SecureLoader.getContextClassLoader().loadClass(fullyQualifiedClassName));
        } catch (ClassNotFoundException e) {
            // fall through
        }
        // then the default mechanism.
        try {
            return ref(Class.forName(fullyQualifiedClassName));
        } catch (ClassNotFoundException e1) {
            // fall through
        }

        // assume it's not visible to us.
        return new JDirectClass(this,fullyQualifiedClassName);
    }

    /**
     * Cached for {@link #wildcard()}.
     */
    private JClass wildcard;

    /**
     * Gets a {@link JClass} representation for "?",
     * which is equivalent to "? extends Object".
     */
    public JClass wildcard() {
        if(wildcard==null)
            wildcard = ref(Object.class).wildcard();
        return wildcard;
    }

    /**
     * Obtains a type object from a type name.
     *
     * <p>
     * This method handles primitive types, arrays, and existing {@link Class}es.
     */
    public JType parseType(String name) {
        // array
        if(name.endsWith("[]"))
            return parseType(name.substring(0,name.length()-2)).array();

        // try primitive type
        try {
            return JType.parse(this,name);
        } catch (IllegalArgumentException e) {
        }

        // existing class
//        return new TypeNameParser(name).parseTypeName();
        return new TreeParser().parseTypeName(name);
    }

    private class TreeParser {

        private Node buildTree(String str) {
            StringBuilder content = new StringBuilder();
            Node root = new Node(null);
            root.value = str;
            Node current = root;
            for (int i = 0; i < str.length(); i++) {
                char c = str.charAt(i);
                if (c == '<') {
                    Node child = new Node(current);
                    current.value = content.toString();
                    current.childs.add(child);
                    current = child;
                    content = new StringBuilder();
                } else if (c == '>') {
                    if (current.value == null) {
                        current.value = content.toString();
                    }
                    current = current.parent;
                    content = new StringBuilder();
                } else if (c == ',') {
                    if (current.value == null) {
                        current.value = content.toString();
                    }
                    Node brother = new Node(current.parent);
                    brother.parent.childs.add(brother);
                    current = brother;
                    content = new StringBuilder();
                } else {
                    content.append(c);
                }
            }
            return root;
        }

        private void postOrderCreateJClass(Node node) {
            if (node != null) {
                for (Node child : node.childs) {
                    postOrderCreateJClass(child);
                }
                node.jClass =  new TypeNameParser(node.value).parseTypeName();
                if (!node.childs.isEmpty()) {
                    JClass[] argsA = node.childs.stream().map(n -> n.jClass).toArray(JClass[]::new);
                    node.jClass = node.jClass.narrow(argsA);
                }
            }
        }

        private JClass parseTypeName(String str) {
            Node root = buildTree(str);
            postOrderCreateJClass(root);
            return root.jClass;
        }
    }

    private static class Node {
        private String value;
        private JClass jClass;
        private final Node parent;
        private final List<Node> childs = new LinkedList<>();

        public Node(Node parent) {
            this.parent = parent;
        }

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder(value);
            boolean hasChilds = !childs.isEmpty();
            if (hasChilds) {
                builder.append("<");
            }
            for (Node child : childs) {
                builder.append(child.toString()).append(",");
            }
            if (hasChilds) {
                // Remove last comma
                builder.deleteCharAt(builder.length() - 1);
                builder.append(">");
            }
            return builder.toString();
        }
    }

    private final class TypeNameParser {
        private final String s;
        private int idx;

        public TypeNameParser(String s) {
            this.s = s;
        }

        /**
         * Parses a type name token T (which can be potentially of the form {@code T<T1,T2,...>},
         * or {@code ? extends/super T}.)
         *
         * @return the index of the character next to T.
         */
        JClass parseTypeName() {
            int start = idx;

            if(s.charAt(idx)=='?') {
                // wildcard
                idx++;
                ws();
                String head = s.substring(idx);
                if(head.startsWith("extends")) {
                    idx+=7;
                    ws();
                    return parseTypeName().wildcard();
                } else
                if(head.startsWith("super")) {
                    throw new UnsupportedOperationException("? super T not implemented");
                } else {
                    // not supported
                    throw new IllegalArgumentException("only extends/super can follow ?, but found "+s.substring(idx));
                }
            }

            while(idx<s.length()) {
                char ch = s.charAt(idx);
                if(Character.isJavaIdentifierStart(ch)
                || Character.isJavaIdentifierPart(ch)
                || ch=='.')
                    idx++;
                else
                    break;
            }

            JClass clazz = ref(s.substring(start,idx));

            return parseSuffix(clazz);
        }

        /**
         * Parses additional left-associative suffixes, like type arguments
         * and array specifiers.
         */
        private JClass parseSuffix(JClass clazz) {
            if(idx==s.length())
                return clazz; // hit EOL

            char ch = s.charAt(idx);

            if(ch=='<')
                return parseSuffix(parseArguments(clazz));

            if(ch=='[') {
                if(s.charAt(idx+1)==']') {
                    idx+=2;
                    return parseSuffix(clazz.array());
                }
                throw new IllegalArgumentException("Expected ']' but found "+s.substring(idx+1));
            }

            return clazz;
        }

        /**
         * Skips whitespaces
         */
        private void ws() {
            while(Character.isWhitespace(s.charAt(idx)) && idx<s.length())
                idx++;
        }

        /**
         * Parses {@code '&lt;T1,T2,...,Tn>'}
         *
         * @return the index of the character next to '{@literal >}'
         */
        private JClass parseArguments(JClass rawType) {
            if(s.charAt(idx)!='<')
                throw new IllegalArgumentException();
            idx++;

            List<JClass> args = new ArrayList<>();

            while(true) {
                args.add(parseTypeName());
                if(idx==s.length())
                    throw new IllegalArgumentException("Missing '>' in "+s);
                char ch = s.charAt(idx);
                if(ch=='>')
                    return rawType.narrow(args.toArray(new JClass[0]));

                if(ch!=',')
                    throw new IllegalArgumentException(s);
                idx++;
            }

        }
    }

    /**
     * References to existing classes.
     * 
     * <p>
     * JReferencedClass is kept in a pool so that they are shared.
     * There is one pool for each JCodeModel object.
     * 
     * <p>
     * It is impossible to cache JReferencedClass globally only because
     * there is the _package() method, which obtains the owner JPackage
     * object, which is scoped to JCodeModel.
     */
    private class JReferencedClass extends JClass implements JDeclaration {
        private final Class<?> _class;

        JReferencedClass(Class<?> _clazz) {
            super(JCodeModel.this);
            this._class = _clazz;
            assert !_class.isArray();
        }

        @Override
        public String name() {
            return _class.getSimpleName().replace('$','.');
        }

        @Override
        public String fullName() {
            return _class.getName().replace('$','.');
        }

        @Override
        public String binaryName() {
            return _class.getName();
        }

        @Override
        public JClass outer() {
            Class<?> p = _class.getDeclaringClass();
            if(p==null)     return null;
            return ref(p);
        }

        @Override
        public JPackage _package() {
            String name = fullName();

            // this type is array
            if (name.indexOf('[') != -1)
                return JCodeModel.this._package("");

            // other normal case
            int idx = name.lastIndexOf('.');
            if (idx < 0)
                return JCodeModel.this._package("");
            else
                return JCodeModel.this._package(name.substring(0, idx));
        }

        @Override
        public JClass _extends() {
            Class<?> sp = _class.getSuperclass();
            if (sp == null) {
                if(isInterface())
                    return owner().ref(Object.class);
                return null;
            } else
                return ref(sp);
        }

        @Override
        public Iterator<JClass> _implements() {
            final Class<?>[] interfaces = _class.getInterfaces();
            return new Iterator<>() {
                private int idx = 0;

                @Override
                public boolean hasNext() {
                    return idx < interfaces.length;
                }

                @Override
                public JClass next() {
                    return JCodeModel.this.ref(interfaces[idx++]);
                }

                @Override
                public void remove() {
                    throw new UnsupportedOperationException();
                }
            };
        }

        @Override
        public boolean isInterface() {
            return _class.isInterface();
        }

        @Override
        public boolean isAbstract() {
            return Modifier.isAbstract(_class.getModifiers());
        }

        @Override
        public JPrimitiveType getPrimitiveType() {
            Class<?> v = boxToPrimitive.get(_class);
            if(v!=null)
                return JType.parse(JCodeModel.this,v.getName());
            else
                return null;
        }

        @Override
        public boolean isArray() {
            return false;
        }

        @Override
        public void declare(JFormatter f) {
        }

        @Override
        public JTypeVar[] typeParams() {
            // TODO: does JDK 1.5 reflection provides these information?
            return super.typeParams();
        }

        @Override
        protected JClass substituteParams(JTypeVar[] variables, List<JClass> bindings) {
            // TODO: does JDK 1.5 reflection provides these information?
            return this;
        }
    }

    /**
     * Conversion from primitive type {@link Class} (such as {@link Integer#TYPE}
     * to its boxed type (such as {@code Integer.class})
     */
    public static final Map<Class<?>,Class<?>> primitiveToBox;
    /**
     * The reverse look up for {@link #primitiveToBox}
     */
    public static final Map<Class<?>,Class<?>> boxToPrimitive;

    static {
        Map<Class<?>,Class<?>> m1 = new HashMap<>();
        Map<Class<?>,Class<?>> m2 = new HashMap<>();

        m1.put(Boolean.class,Boolean.TYPE);
        m1.put(Byte.class,Byte.TYPE);
        m1.put(Character.class,Character.TYPE);
        m1.put(Double.class,Double.TYPE);
        m1.put(Float.class,Float.TYPE);
        m1.put(Integer.class,Integer.TYPE);
        m1.put(Long.class,Long.TYPE);
        m1.put(Short.class,Short.TYPE);
        m1.put(Void.class,Void.TYPE);

        for (Map.Entry<Class<?>, Class<?>> e : m1.entrySet())
            m2.put(e.getValue(),e.getKey());

        boxToPrimitive = Collections.unmodifiableMap(m1);
        primitiveToBox = Collections.unmodifiableMap(m2);

    }
}
