/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2001-2002 (C) Intalio, Inc. All Rights Reserved.
 */
package org.exolab.javasource;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;

/**
 * This class represents the basic Java "structure" for a Java source file. This
 * is the base class for JClass and JInterface. <br/> This is a useful utility
 * when creating in memory source code. The code in this package was modelled
 * after the Java Reflection API as much as possible to reduce the learning
 * curve.
 * 
 * @author <a href="mailto:skopp AT riege DOT de">Martin Skopp</a>
 * @author <a href="mailto:keith AT kvisco DOT com">Keith Visco</a>
 * @version $Revision: 8382 $ $Date: 2005-12-13 14:58:48 -0700 (Tue, 13 Dec
 *          2005) $
 */
public abstract class JStructure extends JType implements JAnnotatedElement {

    /**
     * The Id for Source control systems. <br/> Note: I needed to break the
     * String into parts to prevent CVS from expanding it here!
     */
    private static final String DEFAULT_HEADER = "$" + "Id$";

    /**
     * The source control version for listed in the JavaDoc <br/> Note: I needed
     * to break the String into parts to prevent CVS from expanding it here!
     */
    private static final String DEFAULT_VERSION = "$" + "Revision$ $" + "Date$";

    /** A standard complaint for a bad parameter. */
    private static final String JSW_SHOULD_NOT_BE_NULL = "argument 'jsw' should not be null.";

    /** The source header. */
    private JComment _header;

    /** The package to which this JStructure belongs. */
    private String _packageName;

    /** List of imported classes and packages. */
    private Vector<String> _imports;

    /** The Javadoc for this JStructure. */
    private JDocComment _jdc;

    /** Implementation of JAnnoatedElement to delagate to. */
    private JAnnotatedElementHelper _annotatedElement;

    /**
     * The JModifiers for this JStructure, which allows us to change the
     * resulting qualifiers.
     */
    private JModifiers _modifiers;

    /** The set of interfaces implemented/extended by this JStructure. */
    private Vector<String> _interfaces;

    /**
     * Creates a new JStructure with the given name.
     * 
     * @param name
     *            The name of the JStructure.
     */
    protected JStructure(final String name) {
        super(name);

        // -- verify name is a valid java class name
        if (!isValidClassName(name)) {
            String lname = getLocalName();
            String err = "'" + lname + "' is ";
            if (JNaming.isKeyword(lname)) {
                err += "a reserved word and may not be used as  a class name.";
            } else {
                err += "not a valid Java identifier.";
            }
            throw new IllegalArgumentException(err);
        }

        _header = null;
        _packageName = JNaming.getPackageFromClassName(name);
        _imports = new Vector<String>();
        _jdc = new JDocComment(JDocDescriptor
                .createVersionDesc(DEFAULT_VERSION));
        _annotatedElement = new JAnnotatedElementHelper();
        _modifiers = new JModifiers();
        _interfaces = new Vector<String>();
    }

    /**
     * Test the provided name and return true if it is a valid class name.
     * 
     * @param classname
     *            A class name to test.
     * @return True if the provided class name is a valid class name.
     */
    private boolean isValidClassName(final String classname) {
        if (classname == null) {
            return false;
        }

        String name = classname;
        int beforeTypeName = name.indexOf("<");
        if (beforeTypeName > 0) {
            name = name.substring(0, beforeTypeName);
        }

        // -- ignore package information, for now
        name = JNaming.getLocalNameFromClassName(name);

        return JNaming.isValidJavaIdentifier(name);
    }

    /**
     * Returns the JComment header to display at the top of the source file for
     * this JStructure, or null if no header was set.
     * 
     * @return The JComment header or null if none exists.
     */
    public final JComment getHeader() {
        return _header;
    }

    /**
     * Sets the header comment for this JStructure.
     * 
     * @param comment
     *            The comment to display at the top of the source file when
     *            printed.
     */
    public final void setHeader(final JComment comment) {
        _header = comment;
    }

    /**
     * Returns the name of the package that this JStructure is a member of.
     * 
     * @return The name of the package that this JStructure is a member of, or
     *         null if there is no current package name defined.
     */
    public final String getPackageName() {
        return _packageName;
    }

    /**
     * Returns an Enumeration of imported package and class names for this
     * JStructure.
     * 
     * @return The Enumeration of imports. May be empty but will not be null.
     */
    public final Enumeration<String> getImports() {
        return _imports.elements();
    }

    /**
     * Returns the amount of imports.
     * 
     * @return The amount of imports.
     */
    public final int getImportCount() {
        return _imports.size();
    }

    /**
     * Returns true if the given classname exists in the imports of this
     * JStructure.
     * 
     * @param classname
     *            The class name to check for
     * @return True if the given classname exists in the imports list.
     */
    public final boolean hasImport(final String classname) {
        return _imports.contains(classname);
    }

    /**
     * Adds the given import to this JStructure. Note: You cannot import from
     * the "default package," so imports with no package are ignored.
     * 
     * @param className
     *            Name of the class to import.
     */
    public abstract void addImport(final String className);

    /**
     * Adds the given import to this JStructure. Given class name should not be
     * null or empty. <br/> Note: You cannot import from the "default package,"
     * so imports with no package are ignored.
     * 
     * @param className
     *            Name of the class to import.
     */
    protected final void addImportInternal(final String className) {
        // -- getPackageName
        String pkgName = JNaming.getPackageFromClassName(className);

        if (pkgName != null) {
            if (pkgName.equals(_packageName) || pkgName.equals("java.lang")) {
                return;
            }

            // -- for readabilty keep import list sorted, and make sure
            // -- we do not include more than one of the same import
            for (int i = 0; i < _imports.size(); i++) {
                String imp = _imports.elementAt(i);
                if (imp.equals(className)) {
                    return;
                }
                if (imp.compareTo(className) > 0) {
                    _imports.insertElementAt(className, i);
                    return;
                }
            }
            _imports.addElement(className);
        }
    }

    /**
     * Adds appropriate import for this JAnnotation.
     * 
     * @param annotation
     *            A JAnnotation for which we want to add an import to this
     *            JStructure.
     */
    protected final void addImport(final JAnnotation annotation) {
        addImport(annotation.getAnnotationType().getName());
    }

    /**
     * Adds appropriate imports for each JAnnotation in the given Array.
     * 
     * @param annotations
     *            An Array of JAnnotation; we want to add an import to this
     *            JStructure for each JAnnotation in the Array.
     */
    protected final void addImport(final JAnnotation[] annotations) {
        for (int i = 0; i < annotations.length; i++) {
            addImport(annotations[i].getAnnotationType().getName());
        }
    }

    /**
     * Remove the import of the given class name from this JStucture, returning
     * true if the import was found and removed.
     * 
     * @param className
     *            Name of the class to remove the import of.
     * @return If the import was previously part of this JStructure, false
     *         otherwise.
     */
    public final boolean removeImport(final String className) {
        boolean result = false;
        if (className == null) {
            return result;
        }
        if (className.length() == 0) {
            return result;
        }

        result = _imports.removeElement(className);
        return result;
    }

    /**
     * Returns the JavaDoc comment for this JStructure.
     * 
     * @return The JDocComment for this JStructure.
     */
    public final JDocComment getJDocComment() {
        return _jdc;
    }

    /**
     * Returns the object managing the annotations for this JStructure.
     * 
     * @return The object managing the annotations for this JStructure.
     */
    protected final JAnnotatedElementHelper getAnnotatedElementHelper() {
        return _annotatedElement;
    }

    /**
     * {@inheritDoc}
     */
    public final boolean hasAnnotations() {
        return _annotatedElement.hasAnnotations();
    }

    /**
     * {@inheritDoc}
     */
    public final JAnnotation[] getAnnotations() {
        return _annotatedElement.getAnnotations();
    }

    /**
     * {@inheritDoc}
     */
    public final JAnnotation getAnnotation(final JAnnotationType annotationType) {
        return _annotatedElement.getAnnotation(annotationType);
    }

    /**
     * {@inheritDoc}
     */
    public final boolean isAnnotationPresent(
            final JAnnotationType annotationType) {
        return _annotatedElement.isAnnotationPresent(annotationType);
    }

    /**
     * {@inheritDoc}
     */
    public final void addAnnotation(final JAnnotation annotation) {
        _annotatedElement.addAnnotation(annotation);
        addImport(annotation);
    }

    /**
     * {@inheritDoc}
     */
    public final JAnnotation removeAnnotation(
            final JAnnotationType annotationType) {
        return _annotatedElement.removeAnnotation(annotationType);
    }

    /**
     * Returns the JModifiers, which allows the qualifiers to be changed.
     * 
     * @return The JModifiers for this JStructure.
     */
    public final JModifiers getModifiers() {
        return _modifiers;
    }

    /**
     * Returns an Enumeration of interface names that this JStructure inherits
     * from.
     * 
     * @return The Enumeration of interface names for this JStructure. May be
     *         empty but will not be null.
     */
    public final Enumeration<String> getInterfaces() {
        return _interfaces.elements();
    }

    /**
     * Return the count of the number of Interfaces that have been added to this
     * JStructure.
     * 
     * @return The count of the number of Interfaces that have been added to
     *         this JStructure.
     */
    public final int getInterfaceCount() {
        return _interfaces.size();
    }

    /**
     * Adds the given interface to the list of interfaces this JStructure
     * inherits method declarations from, and either implements (JClass) or
     * extends (JInterface).
     * 
     * @param interfaceName
     *            The name of the interface to "inherit" method declarations
     *            from.
     */
    public final void addInterface(final String interfaceName) {
        if (!_interfaces.contains(interfaceName)) {
            _interfaces.addElement(interfaceName);
        }
    }

    /**
     * Removes the given interface from the list of interfaces this
     * {@link JStructure} has.
     * 
     * @param interfaceName
     *            The name of the interface to be removed.
     * @return true if {@link JStructure} implemented the interface and it was
     *         removed, false otherwise.
     */
    public final boolean removeInterface(final String interfaceName) {
        return _interfaces.remove(interfaceName);
    }

    /**
     * Returns the field with the given name, or null if no field was found with
     * that name.
     * 
     * @param name
     *            The name of the field to return.
     * @return The field with the given name, or null if no field was found with
     *         the given name.
     */
    public abstract JField getField(String name);

    /**
     * Returns an array of all the JFields of this JStructure.
     * 
     * @return An array of all the JFields of this JStructure.
     */
    public abstract JField[] getFields();

    /**
     * Adds the given JField to this JStructure. <br/> This method is
     * implemented by subclasses and should only accept the proper fields for
     * the subclass otherwise an IllegalArgumentException will be thrown. For
     * example a JInterface will only accept static fields.
     * 
     * @param jField
     *            The JField to add.
     */
    public abstract void addField(JField jField);

    /**
     * Adds the given JMember to this JStructure. <br/> This method is
     * implemented by subclasses and should only accept the proper types for the
     * subclass otherwise an IllegalArgumentException will be thrown.
     * 
     * @param jMember
     *            The JMember to add to this JStructure.
     */
    public abstract void addMember(JMember jMember);

    /**
     * Returns the name of the file that this JStructure would be printed to,
     * given a call to {@link #print(String, String)}.
     * 
     * @param destDir
     *            the destination directory. This may be null.
     * @return the name of the file that this JInterface would be printed to
     */
    public final String getFilename(final String destDir) {
        String filename = getLocalName() + ".java";

        // -- Convert Java package to path string
        String javaPackagePath = "";
        if ((_packageName != null) && (_packageName.length() > 0)) {
            javaPackagePath = _packageName.replace('.', File.separatorChar);
        }

        // -- Create fully qualified path (including 'destDir') to file
        File pathFile;
        if (destDir == null) {
            pathFile = new File(javaPackagePath);
        } else {
            pathFile = new File(destDir, javaPackagePath);
        }
        if (!pathFile.exists()) {
            pathFile.mkdirs();
        }

        // -- Prefix filename with path
        if (pathFile.toString().length() > 0) {
            filename = pathFile.toString() + File.separator + filename;
        }

        return filename;
    }

    /**
     * Prints the source code for this JStructure to the destination directory.
     * Subdirectories will be created if necessary for the package.
     * 
     * @param destDir
     *            Directory name to use as the root directory for all output.
     * @param lineSeparator
     *            The line separator to use at the end of each line. If null,
     *            then the default line separator for the runtime platform will
     *            be used.
     */
    public final void print(final String destDir, final String lineSeparator) {
        // -- open output file
        String filename = getFilename(destDir);

        File file = new File(filename);
        JSourceWriter jsw = null;
        try {
            jsw = new JSourceWriter(new FileWriter(file));
        } catch (IOException ioe) {
            System.out.println("unable to create class file: " + filename);
            return;
        }
        if (lineSeparator == null) {
            jsw.setLineSeparator(System.getProperty("line.separator"));
        } else {
            jsw.setLineSeparator(lineSeparator);
        }
        print(jsw);
        jsw.close();
    }

    /**
     * Prints the source code for this JStructure to the given JSourceWriter.
     * 
     * @param jsw
     *            The JSourceWriter to print to.
     * @deprecated Please use the Velocity-template based approach instead.
     */
    public abstract void print(JSourceWriter jsw);

    /**
     * A utility method that prints the header to the given JSourceWriter.
     * 
     * @param jsw
     *            The JSourceWriter to print to.
     */
    public final void printHeader(final JSourceWriter jsw) {
        if (jsw == null) {
            throw new IllegalArgumentException(JSW_SHOULD_NOT_BE_NULL);
        }

        // -- write class header
        if (_header != null) {
            _header.print(jsw);
        } else {
            jsw.writeln("/*");
            jsw.writeln(" * " + DEFAULT_HEADER);
            jsw.writeln(" */");
        }
        jsw.writeln();
        jsw.flush();
    }

    /**
     * A utility method that prints the packageDeclaration to the given
     * JSourceWriter.
     * 
     * @param jsw
     *            The JSourceWriter to print to.
     */
    public final void printPackageDeclaration(final JSourceWriter jsw) {
        if (jsw == null) {
            throw new IllegalArgumentException(JSW_SHOULD_NOT_BE_NULL);
        }

        // -- print package name
        if ((_packageName != null) && (_packageName.length() > 0)) {
            jsw.write("package ");
            jsw.write(_packageName);
            jsw.writeln(';');
            jsw.writeln();
        }
        jsw.flush();
    }

    /**
     * A utility method that prints the imports to the given JSourceWriter.
     * 
     * @param jsw
     *            The JSourceWriter to print to.
     */
    protected final void printImportDeclarations(final JSourceWriter jsw) {
        if (jsw == null) {
            throw new IllegalArgumentException(JSW_SHOULD_NOT_BE_NULL);
        }

        // -- print imports
        if (_imports.size() > 0) {
            jsw.writeln("  //---------------------------------/");
            jsw.writeln(" //- Imported classes and packages -/");
            jsw.writeln("//---------------------------------/");
            jsw.writeln();
            Enumeration<String> enumeration = _imports.elements();
            while (enumeration.hasMoreElements()) {
                jsw.write("import ");
                jsw.write(enumeration.nextElement());
                jsw.writeln(';');
            }
            jsw.writeln();
            jsw.flush();
        }
    }

    /**
     * {@inheritDoc} <br/> Returns the String representation of this JType.
     */
    public final String toString() {
        return getName();
    }

    //--------------------------------------------------------------------------
}
