/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.codemodel;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Collection;
import java.util.Collections;


/**
 * A Java package.
 */
public final class JPackage implements JDeclaration, JGenerable, JClassContainer, JAnnotatable, Comparable<JPackage>, JDocCommentable {

    /**
     * Name of the package.
     * May be the empty string for the root package.
     */
    private String name;

    private final JCodeModel owner;

    /**
     * List of classes contained within this package keyed by their name.
     */
    private final Map<String,JDefinedClass> classes = new TreeMap<String,JDefinedClass>();

    /**
     * List of resources files inside this package.
     */
    private final Set<JResourceFile> resources = new HashSet<JResourceFile>();
    
    /**
     * All {@link JClass}s in this package keyed the upper case class name.
     * 
     * This field is non-null only on Windows, to detect
     * "Foo" and "foo" as a collision. 
     */
    private final Map<String,JDefinedClass> upperCaseClassMap;

    /**
     * Lazily created list of package annotations.
     */
    private List<JAnnotationUse> annotations = null;

    /**
     * package javadoc.
     */
    private JDocComment jdoc = null;

    /**
     * JPackage constructor
     *
     * @param name
     *        Name of package
     *
     * @param  cw  The code writer being used to create this package
     *
     * @throws IllegalArgumentException
     *         If each part of the package name is not a valid identifier
     */
    JPackage(String name, JCodeModel cw) {
        this.owner = cw;
        if (name.equals(".")) {
            String msg = "Package name . is not allowed";
            throw new IllegalArgumentException(msg);
        }
        
        if(JCodeModel.isCaseSensitiveFileSystem)
            upperCaseClassMap = null;
        else
            upperCaseClassMap = new HashMap<String,JDefinedClass>();
        
        this.name = name;
    }


    public JClassContainer parentContainer() {
        return parent();
    }
    
    /**
     * Gets the parent package, or null if this class is the root package.
     */
    public JPackage parent() {
        if(name.length()==0)    return null;
        
        int idx = name.lastIndexOf('.');
        return owner._package(name.substring(0,idx));
    }

    public boolean isClass() { return false; }
    public boolean isPackage() { return true; }
    public JPackage getPackage() { return this; }
    
    /**
     * Add a class to this package.
     *
     * @param mods
     *        Modifiers for this class declaration
     *
     * @param name
     *        Name of class to be added to this package
     *
     * @return Newly generated class
     * 
     * @exception JClassAlreadyExistsException
     *      When the specified class/interface was already created.
     */
    public JDefinedClass _class(int mods, String name) throws JClassAlreadyExistsException {
        return _class(mods,name,ClassType.CLASS);
    }

    /**
     * {@inheritDoc}
     * @deprecated
     */
    public JDefinedClass _class( int mods, String name, boolean isInterface ) throws JClassAlreadyExistsException {
    	return _class(mods,name, isInterface?ClassType.INTERFACE:ClassType.CLASS );
    }
    
    public JDefinedClass _class( int mods, String name, ClassType classTypeVal ) throws JClassAlreadyExistsException {
        if(classes.containsKey(name))
            throw new JClassAlreadyExistsException(classes.get(name));
        else {
            // XXX problems caught in the NC constructor
            JDefinedClass c = new JDefinedClass(this, mods, name, classTypeVal);
            
            if( upperCaseClassMap!=null ) {
                JDefinedClass dc = upperCaseClassMap.get(name.toUpperCase());
                if(dc!=null)
                    throw new JClassAlreadyExistsException(dc);
                upperCaseClassMap.put(name.toUpperCase(),c);
            }            
            classes.put(name,c);
            return c;
        }
    }

	/**
	 * Adds a public class to this package.
	 */
    public JDefinedClass _class(String name) throws JClassAlreadyExistsException {
		return _class( JMod.PUBLIC, name );
	}

    /**
     * Gets a reference to the already created {@link JDefinedClass}.
     * 
     * @return null
     *      If the class is not yet created.
     */
    public JDefinedClass _getClass(String name) {
        if(classes.containsKey(name))
            return classes.get(name);
        else
            return null;
    }

    /**
     * Order is based on the lexicological order of the package name.
     */
    public int compareTo(JPackage that) {
        return this.name.compareTo(that.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
     */
    public JDefinedClass _interface(int mods, String name) throws JClassAlreadyExistsException {
        return _class(mods,name,ClassType.INTERFACE);
    }

    /**
     * Adds a public interface to this package.
     */
    public JDefinedClass _interface(String name) throws JClassAlreadyExistsException {
        return _interface(JMod.PUBLIC, name);
    }
    
    /**
     * 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.
     
     */
    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.
     
     */
    public JDefinedClass _enum (String name) throws JClassAlreadyExistsException {
    	return _class (JMod.PUBLIC,name,ClassType.ENUM);
    }
    /**
     * Adds a new resource file to this package.
     */
    public JResourceFile addResourceFile(JResourceFile rsrc) {
        resources.add(rsrc);
        return rsrc;
    }
    
    /**
     * Checks if a resource of the given name exists.
     */
    public boolean hasResourceFile(String name) {
        for (JResourceFile r : resources)
            if (r.name().equals(name))
                return true;
        return false;
    }
    
    /**
     * Iterates all resource files in this package.
     */
    public Iterator<JResourceFile> propertyFiles() {
        return resources.iterator();
    }

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

    /**
     * Removes a class from this package.
     */
    public void remove(JClass c) {
        if (c._package() != this)
            throw new IllegalArgumentException(
                "the specified class is not a member of this package," + " or it is a referenced class");

        // note that c may not be a member of classes.
        // this happens when someone is trying to remove a non generated class
        classes.remove(c.name());
        if (upperCaseClassMap != null)
            upperCaseClassMap.remove(c.name().toUpperCase());
    }
	
    /**
     * Reference a class within this package.
     */
    public JClass ref(String name) throws ClassNotFoundException {
        if (name.indexOf('.') >= 0)
            throw new IllegalArgumentException("JClass name contains '.': " + name);

        String n = "";
        if (!isUnnamed())
            n = this.name + '.';
        n += name;

        return owner.ref(Class.forName(n));
    }
    
    /**
     * Gets a reference to a sub package of this package.
     */
    public JPackage subPackage( String pkg ) {
        if(isUnnamed())     return owner()._package(pkg);
        else                return owner()._package(name+'.'+pkg);
    }

    /**
     * Returns an iterator that walks the top-level classes defined in this
     * package.
     */
    public Iterator<JDefinedClass> classes() {
        return classes.values().iterator();
    }
    
    /**
     * Checks if this package contains any classes.
     * @return {@code true} if this package contains any classes
     *         or {@code false} otherwise.
     */
    public boolean hasClasses() {
        return !classes.isEmpty();
    }

    /**
     * Checks if a given name is already defined as a class/interface
     */
    public boolean isDefined(String classLocalName) {
        Iterator<JDefinedClass> itr = classes();
        while (itr.hasNext()) {
            if ((itr.next()).name().equals(classLocalName))
                return true;
        }

        return false;
    }

    /**
     * Checks if this package is the root, unnamed package.
     */
    public final boolean isUnnamed() { return name.length() == 0; }

    /**
     * Get the name of this package
     *
     * @return
     *		The name of this package, or the empty string if this is the
     *		null package. For example, this method returns strings like
     *		<code>"java.lang"</code>
     */
    public String name() {
        return name;
    }

    /**
     * Return the code model root object being used to create this package.
     */
    public final JCodeModel owner() { return owner; }


    public JAnnotationUse annotate(JClass clazz) {
        if(isUnnamed())
            throw new IllegalArgumentException("the root package cannot be annotated");
        if(annotations==null)
           annotations = new ArrayList<JAnnotationUse>();
        JAnnotationUse a = new JAnnotationUse(clazz);
        annotations.add(a);
        return a;
    }

    public JAnnotationUse annotate(Class<? extends Annotation> clazz) {
        return annotate(owner.ref(clazz));
    }

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

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

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

    /**
     * Convert the package name to directory path equivalent
     */
    File toPath(File dir) {
        if (name == null) return dir;
        return new File(dir, name.replace('.', File.separatorChar));
    }

    public void declare(JFormatter f ) {
        if (name.length() != 0)
            f.p("package").p(name).p(';').nl();
    }

    public void generate(JFormatter f) {
        f.p(name);
    }


    void build( CodeWriter src, CodeWriter res ) throws IOException {

        // write classes
        for (JDefinedClass c : classes.values()) {
            if (c.isHidden())
                continue;   // don't generate this file

            JFormatter f = createJavaSourceFileWriter(src, c.name());
            f.write(c);
            f.close();
        }

        // write package annotations
        if(annotations!=null || jdoc!=null) {
            JFormatter f = createJavaSourceFileWriter(src,"package-info");

            if (jdoc != null)
                f.g(jdoc);

            // TODO: think about importing
            if (annotations != null){
                for (JAnnotationUse a : annotations)
                    f.g(a).nl();
            }
            f.d(this);

            f.close();
        }

        // write resources
        for (JResourceFile rsrc : resources) {
            CodeWriter cw = rsrc.isResource() ? res : src;
            OutputStream os = new BufferedOutputStream(cw.openBinary(this, rsrc.name()));
            rsrc.build(os);
            os.close();
        }
    }

    /*package*/ int countArtifacts() {
        int r = 0;
        for (JDefinedClass c : classes.values()) {
            if (c.isHidden())
                continue;   // don't generate this file
            r++;
        }

        if(annotations!=null || jdoc!=null) {
            r++;
        }

        r+= resources.size();

        return r;
    }

    private JFormatter createJavaSourceFileWriter(CodeWriter src, String className) throws IOException {
        Writer bw = new BufferedWriter(src.openSource(this,className+".java"));
        return new JFormatter(new PrintWriter(bw));
    }
}
