/*
 * Copyright 2006 Ralf Joachim
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.exolab.javasource;

import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Vector;

import org.exolab.castor.builder.SourceGenerator;

/**
 * A abstract base class for representations of the Java Source code for a Java Class.
 *
 * @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
 * @version $Revision: 6668 $ $Date: 2005-05-08 12:32:06 -0600 (Sun, 08 May 2005) $
 * @since 1.1
 */
public abstract class AbstractJClass extends JStructure {

    /** The source code for static initialization. */
    private JSourceCode _staticInitializer;

    /** The list of member variables (fields) of this JClass. */
    private Map<String, JField> _fields = new LinkedHashMap<String, JField>();

    /** The list of member constants of this {@link JClass}. */
    private Map<String, JConstant> _constants = new LinkedHashMap<String, JConstant>();

    /** The list of constructors for this JClass. */
    private Vector<JConstructor> _constructors;

    /** The list of methods of this JClass. */
    private Vector<JMethod> _methods;

    /** A collection of inner classes for this JClass. */
    private Vector<JClass> _innerClasses;

    private Vector<String> _sourceCodeEntries = new Vector<String>();

    /**
     * Returns a collection of (complete) source code fragments.
     * @return A collection of (complete) source code fragments.
     */
    public String[] getSourceCodeEntries() {
        return _sourceCodeEntries.toArray(new String[_sourceCodeEntries.size()]);
    }

    /**
     * Creates a new AbstractJClass with the given name.
     *
     * @param name The name of the AbstractJClass to create.
     */
    protected AbstractJClass(final String name) {
        super(name);
        
        _staticInitializer = new JSourceCode();
        _constructors = new Vector<JConstructor>();
        _methods = new Vector<JMethod>();
        _innerClasses = null;
        
        //-- initialize default Java doc
        getJDocComment().appendComment("Class " + getLocalName() + ".");
    }

    //--------------------------------------------------------------------------

    /**
     * Returns the JSourceCode for the static initializer of this JClass.
     *
     * @return The JSourceCode for the static initializer of this JClass.
     */
    public final JSourceCode getStaticInitializationCode() {
        return _staticInitializer;
    }

    /**
     * {@inheritDoc}
     */
    public final JField getField(final String name) {
        return _fields.get(name);
    }

    /**
     * {@inheritDoc}
     */
    public final JConstant getConstant(final String name) {
        return _constants.get(name);
    }

    /**
     * {@inheritDoc}
     */
    public final JField[] getFields() {
        return _fields.values().toArray(new JField[_fields.size()]);
    }

    /**
     * {@inheritDoc}
     */
    public final JConstant[] getConstants() {
        return _constants.values().toArray(new JConstant[_constants.size()]);
    }

    /**
     * Returns the amount of fields.
     * @return The amount of fields.
     */
    public final int getFieldCount() {
        return _fields.size();
    }

    /**
     * Returns the amount of constants.
     * @return The amount of constants.
     */
    public final int getConstantCount() {
        return _constants.size();
    }

    /**
     * {@inheritDoc}
     */
    public final void addField(final JField jField) {
        if (jField == null) {
            throw new IllegalArgumentException("Class members cannot be null");
        }

        String name = jField.getName();

        if (_fields.get(name) != null) {
            String nameToCompare = (name.startsWith("_")) ? name.substring(1) : name;
            nameToCompare = nameToCompare.substring(0, 1).toUpperCase()
                            + nameToCompare.substring(1);
            if (JNaming.isReservedByCastor(nameToCompare)) {
                String warn = "'" + nameToCompare + "' might conflict with a field name used"
                        + " by Castor.  If you get a complaint\nabout a duplicate name, you will"
                        + " need to use a mapping file or change the name\nof the conflicting"
                        + " schema element.";
                System.out.println(warn);
            }

            String err = "Duplicate name found as a class member: " + name;
            throw new IllegalArgumentException(err);
        }
        _fields.put(name, jField);
    }

    public final void addConstant(final JConstant jConstant) {
        if (jConstant == null) {
            throw new IllegalArgumentException("Class constants cannot be null");
        }

        String name = jConstant.getName();

        if (_constants.get(name) != null) {
            String nameToCompare = (name.startsWith("_")) ? name.substring(1) : name;
            nameToCompare = nameToCompare.substring(0, 1).toUpperCase()
                            + nameToCompare.substring(1);
            if (JNaming.isReservedByCastor(nameToCompare)) {
                String warn = "'" + nameToCompare + "' might conflict with a constant name used"
                        + " by Castor.  If you get a complaint\nabout a duplicate name, you will"
                        + " need to use a mapping file or change the name\nof the conflicting"
                        + " schema element.";
                System.out.println(warn);
            }

            String err = "Duplicate name found as a class member: " + name;
            throw new IllegalArgumentException(err);
        }
        _constants.put(name, jConstant);
    }

    /**
     * Removes the field with the given name from this JClass.
     *
     * @param name The name of the field to remove.
     * @return The JField if it was found and removed.
     */
    public final JField removeField(final String name) {
        if (name == null) { return null; }

        JField field = _fields.remove(name);

        //-- clean up imports
        //-- NOT YET IMPLEMENTED
        return field;
    }

    /**
     * Removes the constant with the given name from this {@link JClass}.
     *
     * @param name The name of the constant to remove.
     * @return The JConstant if it was found and removed.
     */
    public final JConstant removeConstant(final String name) {
        if (name == null) { return null; }

        JConstant constant = _constants.remove(name);

        //-- clean up imports
        //-- NOT YET IMPLEMENTED
        return constant;
    }

    /**
     * Removes the given JField from this JClass.
     *
     * @param jField The JField to remove.
     * @return true if the field was found and removed.
     */
    public final boolean removeField(final JField jField) {
        if (jField == null) { return false; }

        Object field = _fields.get(jField.getName());
        if (field == jField) {
            _fields.remove(jField.getName());
            return true;
        }
        //-- clean up imports
        //-- NOT YET IMPLEMENTED
        return false;
    }

    /**
     * Removes the given {@link JConstant} from this {@link JClass}.
     *
     * @param jConstant The {@link JConstant} to remove.
     * @return true if the constant was found and removed.
     */
    public final boolean removeConstant(final JConstant jConstant) {
        if (jConstant == null) { return false; }

        Object constant = _constants.get(jConstant.getName());
        if (constant == jConstant) {
            _constants.remove(jConstant.getName());
            return true;
        }
        //-- clean up imports
        //-- NOT YET IMPLEMENTED
        return false;
    }

    /**
     * Creates a new JConstructor and adds it to this JClass.
     *
     * @return The newly created constructor.
     */
    public final JConstructor createConstructor() {
        return createConstructor(null);
    }

    /**
     * Creates a new JConstructor and adds it to this JClass.
     *
     * @param params A list of parameters for this constructor.
     * @return The newly created constructor.
     */
    public final JConstructor createConstructor(final JParameter[] params) {
        JConstructor cons = new JConstructor(this);
        if (params != null) {
            for (int i = 0; i < params.length; i++) {
                cons.addParameter(params[i]);
            }
        }
        addConstructor(cons);
        return cons;
    }

    /**
     * Returns the constructor at the specified index.
     *
     * @param index The index of the constructor to return.
     * @return The JConstructor at the specified index.
     */
    public final JConstructor getConstructor(final int index) {
        return _constructors.elementAt(index);
    }

    /**
     * Returns the an array of the JConstructors contained within this JClass.
     *
     * @return An array of JConstructor.
     */
    public final JConstructor[] getConstructors() {
        int size = _constructors.size();
        JConstructor[] jcArray = new JConstructor[size];

        for (int i = 0; i < _constructors.size(); i++) {
            jcArray[i] = _constructors.elementAt(i);
        }
        return jcArray;
    }
    
    public final int getContructorsCount() {
    	return _constructors.size();
    }
    
    /**
     * Adds the given Constructor to this classes list of constructors. The
     * constructor must have been created with this JClass' createConstructor.
     *
     * @param constructor The constructor to add.
     */
    public void addConstructor(final JConstructor constructor) {
        if (constructor == null) {
            throw new IllegalArgumentException("Constructors cannot be null");
        }

        if (constructor.getDeclaringClass() == this) {

            /** check signatures (add later) **/
            if (!_constructors.contains(constructor)) {
                _constructors.addElement(constructor);
            }
        } else {
            String err = "The given JConstructor was not created by this JClass";
            throw new IllegalArgumentException(err);
        }
    }

    /**
     * Removes the given constructor from this JClass.
     *
     * @param constructor The JConstructor to remove.
     * @return true if the constructor was removed, otherwise false.
     */
    public final boolean removeConstructor(final JConstructor constructor) {
        return _constructors.removeElement(constructor);
    }

    /**
     * Returns an array of all the JMethods of this JClass.
     *
     * @return An array of all the JMethods of this JClass.
     */
    public final JMethod[] getMethods() {
        int size = _methods.size();
        JMethod[] marray = new JMethod[size];

        for (int i = 0; i < _methods.size(); i++) {
            marray[i] = _methods.elementAt(i);
        }
        return marray;
    }

    /**
     * Returns the first occurance of the method with the given name, starting
     * from the specified index.
     *
     * @param name The name of the method to look for.
     * @param startIndex The starting index to begin the search.
     * @return The method if found, otherwise null.
     */
    public final JMethod getMethod(final String name, final int startIndex) {
        for (int i = startIndex; i < _methods.size(); i++) {
            JMethod jMethod = _methods.elementAt(i);
            if (jMethod.getName().equals(name)) { return jMethod; }
        }
        return null;
    }

    /**
     * Returns the JMethod located at the specified index.
     *
     * @param index The index of the JMethod to return.
     * @return The JMethod.
     */
    public final JMethod getMethod(final int index) {
        return _methods.elementAt(index);
    }
    
    public final int getMethodCount() {
    	return _methods.size(); 
    }

    /**
     * Adds the given JMethod to this JClass.
     *
     * @param jMethod The JMethod to add.
     * @param importReturnType true if we add the importReturnType to the class
     *        import lists. It could be useful to set it to false when all
     *        types are fully qualified.
     */
    public final void addMethod(final JMethod jMethod, final boolean importReturnType) {
        if (jMethod == null) {
            throw new IllegalArgumentException("Class methods cannot be null");
        }

        //-- check method name and signatures *add later*

        //-- keep method list sorted for esthetics when printing
        //-- START SORT :-)
        boolean added = false;
        JModifiers modifiers = jMethod.getModifiers();

        if (modifiers.isAbstract()) {
            getModifiers().setAbstract(true);
        }

        for (int i = 0; i < _methods.size(); i++) {
            JMethod tmp = _methods.elementAt(i);
            //-- first compare modifiers
            if (tmp.getModifiers().isPrivate()) {
                if (!modifiers.isPrivate()) {
                    _methods.insertElementAt(jMethod, i);
                    added = true;
                    break;
                }
            }
            //-- compare names
            if (jMethod.getName().compareTo(tmp.getName()) < 0) {
                    _methods.insertElementAt(jMethod, i);
                    added = true;
                    break;
            }
        }
        //-- END SORT
        if (!added) { _methods.addElement(jMethod); }

    }

    /**
     * Adds the given JMethod to this JClass.
     *
     * @param jMethod The JMethod to add.
     */
    public final void addMethod(final JMethod jMethod) {
         addMethod(jMethod, true);
    }

    /**
     * Adds the given array of JMethods to this JClass.
     *
     * @param jMethods The JMethod[] to add.
     */
    public final void addMethods(final JMethod[] jMethods) {
        for (int i = 0; i < jMethods.length; i++) { addMethod(jMethods[i]); }
    }

    /**
     * Removes the given method from this JClass.
     *
     * @param method The JMethod to remove.
     * @return true if the method was removed, otherwise false.
     */
    public final boolean removeMethod(final JMethod method) {
        return _methods.removeElement(method);
    }

    /**
     * Creates and returns an inner-class for this JClass.
     *
     * @param localname The name of the class (no package name).
     * @return the new JClass.
     */
    public final JClass createInnerClass(final String localname) {
        if (localname == null) {
            String err = "argument 'localname' must not be null.";
            throw new IllegalArgumentException(err);
        }
        if (localname.indexOf('.') >= 0) {
            String err = "The name of an inner-class must not contain a package name.";
            throw new IllegalArgumentException(err);
        }
        String classname = getPackageName();
        if (classname != null) {
            classname = classname + "." + localname;
        } else {
            classname = localname;
        }

        JClass innerClass = new JInnerClass(classname);
        if (_innerClasses == null) {
            _innerClasses = new Vector<JClass>();
        }
        _innerClasses.addElement(innerClass);
        return innerClass;

    }

    /**
     * Returns an array of JClass (the inner classes) contained within this
     * JClass.
     *
     * @return An array of JClass contained within this JClass.
     */
    public final JClass[] getInnerClasses() {
        if (_innerClasses != null) {
            int size = _innerClasses.size();
            JClass[] carray = new JClass[size];
            _innerClasses.copyInto(carray);
            return carray;
        }
        return new JClass[0];
    }
    
    public final int getInnerClassCount() {
    	if (_innerClasses != null) {
    		return _innerClasses.size();
    	} 
    	return 0;
    	
    		
    }

    /**
     * Removes the given inner-class (JClass) from this JClass.
     *
     * @param jClass The JClass (inner-class) to remove.
     * @return true if the JClass was removed, otherwise false.
     */
    public final boolean removeInnerClass(final JClass jClass) {
        if (_innerClasses != null) {
            return _innerClasses.removeElement(jClass);
        }
        return false;
    }

    //--------------------------------------------------------------------------

    /**
     * {@inheritDoc}
     * @deprecated Please use the Velocity-template based approach instead.
     * @see SourceGenerator#setJClassPrinterType(String) 
     */
    public final void print(final JSourceWriter jsw) {
        print(jsw, false);
    }

    /**
     * Prints the source code for this JClass to the given JSourceWriter.
     *
     * @param classOnly If true, the file header, package declaration, and
     *        imports are not printed.
     * @param jsw The JSourceWriter to print to. Must not be null.
     * 
     * @deprecated Please use the Velocity-template based approach instead.
     * @see SourceGenerator#setJClassPrinterType(String) 
     */
    public abstract void print(final JSourceWriter jsw, final boolean classOnly);

    /**
     * Writes to the JSourceWriter the headers for this class file.  Headers
     * include the comment-header, the package declaration, and the imports.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printClassHeaders(final JSourceWriter jsw) {
        printHeader(jsw);
        printPackageDeclaration(jsw);

        //-- get imports from inner-classes
        Vector<String> removeImports = null;
        if ((_innerClasses != null) && (_innerClasses.size() > 0)) {
            removeImports = new Vector<String>();
            for (int i = 0; i < _innerClasses.size(); i++) {
                JClass iClass = _innerClasses.elementAt(i);
                Enumeration<String> enumeration = iClass.getImports();
                while (enumeration.hasMoreElements()) {
                    String classname = enumeration.nextElement();

                    int paramTypeIndex = classname.indexOf("<Object>");
                    if (paramTypeIndex != -1) {
                        classname = classname.substring(0, paramTypeIndex - 1);
                    }
                    if (!hasImport(classname)) {
                        addImport(classname);
                        removeImports.addElement(classname);
                    }
                }
            }
        }

        printImportDeclarations(jsw);

        //-- remove imports from inner-classes, if necessary
        if (removeImports != null) {
            for (int i = 0; i < removeImports.size(); i++) {
                removeImport(removeImports.elementAt(i));
            }
        }
    }

    /**
     * Writes to the {@link JSourceWriter} the constant definitions of this class.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printConstantDefinitions(final JSourceWriter jsw) {
        if (!_constants.isEmpty()) {
            jsw.writeln();
            jsw.writeln("  //--------------------------/");
            jsw.writeln(" //- Class/Member constants -/");
            jsw.writeln("//--------------------------/");
            jsw.writeln();
        }

        for (JConstant constant : _constants.values()) {
            printAbstractJField(jsw, constant);
        }
    }

    /**
     * Prints an {@link AbstractJField} instance to the given {@link JSourceWriter}.
     * 
     * @param jsw The {@link JSourceWriter} to print to.
     * @param field The field to print.
     */
    private void printAbstractJField(final JSourceWriter jsw, final AbstractJField field) {
        //-- print Java comment
        JDocComment comment = field.getComment();
        if (comment != null) { comment.print(jsw); }

        //-- print Annotations
        field.printAnnotations(jsw);

        // -- print member
        jsw.write(field.getModifiers().toString());
        jsw.write(' ');

        JType type = field.getType();
        String typeName = type.toString();
        //-- for esthetics use short name in some cases
        if (typeName.equals(toString())) {
            typeName = type.getLocalName();
        }
        jsw.write(typeName);
        jsw.write(' ');
        jsw.write(field.getName());

        String init = field.getInitString();
        if (init != null && !field.isDateTime()) {
            jsw.write(" = ");
            jsw.write(init);
        }

        jsw.writeln(';');
        jsw.writeln();
    }
    
    /**
     * Writes to the JSourceWriter the member variables of this class.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printMemberVariables(final JSourceWriter jsw) {
        if (!_fields.isEmpty()) {
            jsw.writeln();
            jsw.writeln("  //--------------------------/");
            jsw.writeln(" //- Class/Member Variables -/");
            jsw.writeln("//--------------------------/");
            jsw.writeln();
        }

        for (JField field : _fields.values()) {
            printAbstractJField(jsw, field);
        }
    }

    /**
     * Writes to the JSourceWriter any static initialization used by this class.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printStaticInitializers(final JSourceWriter jsw) {
        //----------------------/
        //- Static Initializer -/
        //----------------------/

        if (!_staticInitializer.isEmpty()) {
            jsw.writeln();
            jsw.writeln("static {");
            jsw.writeln(_staticInitializer.toString());
            jsw.writeln("};");
            jsw.writeln();
        }
    }

    /**
     * Writes to the JSourceWriter all constructors for this class.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printConstructors(final JSourceWriter jsw) {
        if (_constructors.size() > 0) {
            jsw.writeln();
            jsw.writeln("  //----------------/");
            jsw.writeln(" //- Constructors -/");
            jsw.writeln("//----------------/");
            jsw.writeln();
        }

        for (int i = 0; i < _constructors.size(); i++) {
            JConstructor jConstructor = _constructors.elementAt(i);
            jConstructor.print(jsw);
            jsw.writeln();
        }
    }

    /**
     * Writes to the JSourceWriter all methods belonging to this class.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printMethods(final JSourceWriter jsw) {
        if (_methods.size() > 0) {
            jsw.writeln();
            jsw.writeln("  //-----------/");
            jsw.writeln(" //- Methods -/");
            jsw.writeln("//-----------/");
            jsw.writeln();
        }

        for (int i = 0; i < _methods.size(); i++) {
            JMethod jMethod = _methods.elementAt(i);
            jMethod.print(jsw);
            jsw.writeln();
        }
    }
    
    protected final void printSourceCodeFragments(final JSourceWriter sourceWriter) {
        if (!_sourceCodeEntries.isEmpty()) {
            sourceWriter.writeln();
            sourceWriter.writeln("  //----------------------------------/");
            sourceWriter.writeln(" //- Injected source code fragments -/");
            sourceWriter.writeln("//----------------------------------/");
            sourceWriter.writeln();
        }
        for (String sourceCode : _sourceCodeEntries) {
            sourceWriter.writeln(sourceCode);
            sourceWriter.writeln();
        }
        
    }

    /**
     * Writes to the JSourceWriter all inner classes belonging to this class.
     * 
     * @param jsw The JSourceWriter to be used.
     */
    protected final void printInnerClasses(final JSourceWriter jsw) {
        if ((_innerClasses != null) && (_innerClasses.size() > 0)) {
            jsw.writeln();
            jsw.writeln("  //-----------------/");
            jsw.writeln(" //- Inner Classes -/");
            jsw.writeln("//-----------------/");
            jsw.writeln();

            for (int i = 0; i < _innerClasses.size(); i++) {
                JClass jClass = _innerClasses.elementAt(i);
                jClass.print(jsw, true);
                jsw.writeln();
            }
        }
    }

    /**
     * Adds a complete source code fragment (method) to this class.
     * @param sourceCode The complete source code fragment to be added.
     */
    public void addSourceCode(final String sourceCode) {
        _sourceCodeEntries.add(sourceCode);
    }

    public final int getSourceCodeEntryCount() {
        return _sourceCodeEntries.size(); 
    }
    
}
