/**
 * 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@codehaus.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.codehaus.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.
 *
 * Contributors:
 * --------------
 * Gary Shea (shea@gtsdesign.com)   - Original Author
 * Keith Visco (kvisco@intalio.com) - Changed JCompElement references to
 *                                    JStructure, some additional tweaking
 *                                    to get it working with the current
 *                                    Javasource package.
 *
 * $Id$
 */

package org.codehaus.modello.plugin.java.javasource;

/*
 * Copyright (c) 2004, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import java.io.File;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

import org.codehaus.plexus.util.WriterFactory;

/**
 * A representation of the Java Source code for a Java compilation
 * unit. This is
 * a useful utility when creating in memory source code.
 * This package was modelled after the Java Reflection API
 * as much as possible to reduce the learning curve.
 * @author <a href="mailto:shea@gtsdesign.com">Gary Shea</a>
 * @version $Revision$ $Date$
 **/
public class JCompUnit
{

    /**
     * The Id for Source control systems
     * I needed to separate this line to prevent CVS from
     * expanding it here! ;-)
     **/
    private static final String DEFAULT_HEADER
        = "$" + "Id$";

    private JComment header = null;

    /**
     * The package for this JCompUnit
     **/
    private String packageName = null;

    /**
     * The file to which this JCompUnit will be saved
     **/
    private String fileName = null;

    /**
     * The set of top-level classes that live in this compilation unit.
     **/
    //private TypeList classes = null;
    private List<JClass> classes = null;

    /**
     * The set of top-level interfaces that live in this compilation unit.
     **/
    //private TypeList interfaces = null;
    private List<JInterface> interfaces = null;

    /**
     * Creates a new JCompUnit
     * @param packageName the name of the package for this JCompUnit.
     * If packageName is null or empty, no 'package' line will be generated.
     * @param fileName the name of the file in which this JCompUnit
     * will be stored
     **/
    public JCompUnit( String packageName, String fileName )
    {
        this.packageName = packageName;
        this.fileName = fileName;
        init();
    } //-- JCompUnit

    /**
     * Creates a new JCompUnit with the given JClass (which must have
     * been created with either a full class name or package/local
     * name) as the public class.  Package and file name are taken
     * from jClass.
     * @param jClass the public class for this JCompUnit.
     **/
    public JCompUnit( JClass jClass )
    {
        this.packageName = jClass.getPackageName();

        // The outer name is the package plus the simple name of the
        // outermost enclosing class.  The file name is just the
        // simple name of the outermost enclosing class, so the
        // package name part must be stripped off.

        /*
          Commented out until inner-class support has been added.
          kvisco - 20021211

        String outer = jClass.getOuterName();
        int lastDot = outer.lastIndexOf(".");
        String filePrefix;
        if (lastDot != -1) {
            filePrefix = outer.substring (lastDot + 1);
        } else {
            filePrefix = outer;
        }
        */
        String filePrefix = jClass.getLocalName();

        this.fileName = filePrefix + ".java";
        init();
        classes.add( jClass );

    } //-- JCompUnit

    /**
     * Creates a new JCompUnit with the given JInterface as public interface
     * Package and file name are taken from jInterface.
     * @param jInterface the public interface for this JCompUnit.
     **/
    public JCompUnit( JInterface jInterface )
    {
        this.packageName = jInterface.getPackageName();
        this.fileName = jInterface.getLocalName() + ".java";
        init();
        interfaces.add( jInterface );
    } //-- JCompUnit

    private void init()
    {
        classes = new ArrayList<JClass>();
        interfaces = new ArrayList<JInterface>();
    }

    /**
     * Adds the given JStructure (either a JInterface or
     * a JClass) to this JCompUnit.
     *
     * @param jStructure the JStructure to add
     * @exception java.lang.IllegalArgumentException when the given
     * JStructure has the same name of an existing JStructure
     * or if the class of jStructure is unknown.
     */
    public void addStructure( JStructure jStructure )
        throws IllegalArgumentException
    {
        if ( jStructure instanceof JInterface )
            addInterface( (JInterface) jStructure );
        else if ( jStructure instanceof JClass )
            addClass( (JClass) jStructure );
        else
        {
            String err = "Unknown JStructure subclass '"
                + jStructure.getClass().getName() + "'.";
            throw new IllegalArgumentException( err );
        }

    } //-- addStructure

    /**
     * Adds a JClass which should be printed in this file.
     * 
     * @param jClass the JClass to add
     **/
    public void addClass( JClass jClass )
    {
        classes.add( jClass );
    } //-- addClass

    /**
     * Adds a JInterface which should be printed in this file.
     * 
     * @param jInterface the JInterface to add
     **/
    public void addInterface( JInterface jInterface )
    {
        interfaces.add( jInterface );
    } //-- addInterface

    /**
     * returns a array of String containing all import classes/packages,
     * also imports within the same package of this object.
     * @return a array of String containing all import classes/packages,
     * also imports within the same package of this object.
     */
    public SortedSet<String> getImports()
    {

        SortedSet<String> allImports = new TreeSet<String>();

        // add imports from classes
        for ( JClass jClass : classes )
        {
            Enumeration<String> e = jClass.getImports();
            while ( e.hasMoreElements() )
            {
                allImports.add( e.nextElement() );
            }
        }

        for ( JInterface jInterface : interfaces )
        {
            Enumeration<String> e = jInterface.getImports();
            while ( e.hasMoreElements() )
            {
                allImports.add( e.nextElement() );
            }
        }

        return allImports;
    }

    /**
     * Returns the name of the file that this JCompUnit would be
     * printed as, given a call to #print.
     *
     * @param destDir the destination directory. This may be null.
     * @return the name of the file that this JCompUnit would be
     * printed as, given a call to #print.
     **/
    public String getFilename( String destDir )
    {

        String filename = new String( fileName );

        //-- 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;
    } //-- getFilename

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

    protected static String getPackageFromClassName( String className )
    {
        int idx = -1;
        if ( ( idx = className.lastIndexOf( '.' ) ) > 0 )
        {
            return className.substring( 0, idx );
        }
        return null;
    } //-- getPackageFromClassName

    /**
     * Prints the source code for this JClass in the current directory
     * with the default line seperator of the the runtime platform.
     * @see #print(java.lang.String, java.lang.String)
     **/
    public void print()
    {
        print( null, null );
    } //-- print

    /**
     * Prints the source code for this JClass
     * with the default line seperator of the the runtime platform.
     * @param destDir the destination directory to generate the file.
     * @see #print(java.lang.String, java.lang.String)
     **/
    public void print( String destDir )
    {
        print( destDir, null );
    } //-- print

    /**
     * Prints the source code for this JCompUnit.
     * @param destDir the destination directory to generate the file.
     * @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 void print( String destDir, String lineSeparator )
    {

        //-- open output file
        String filename = getFilename( destDir );

        File file = new File( filename );
        JSourceWriter jsw = null;
        try
        {
            jsw = new JSourceWriter( WriterFactory.newPlatformWriter( file ) );
        }
        catch ( java.io.IOException ioe )
        {
            System.out.println( "unable to create compilation unit file: " + filename );
            return;
        }

        if ( lineSeparator == null )
        {
            lineSeparator = System.getProperty( "line.separator" );
        }
        jsw.setLineSeparator( lineSeparator );
        print( jsw );
        jsw.flush();
        jsw.close();
    } //-- print

    /**
     * Prints the source code for this JClass.
     * @param jsw the JSourceWriter to print to.
     **/
    public void print( JSourceWriter jsw )
    {

        // Traverse the nested class and interface heirarchy and
        // update the names to match the compilation unit.

        resolveNames();
        StringBuilder buffer = new StringBuilder();

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

        //-- print package name
        if ( ( packageName != null ) && ( packageName.length() > 0 ) )
        {

            buffer.setLength( 0 );
            buffer.append( "package " );
            buffer.append( packageName );
            buffer.append( ';' );
            jsw.writeln( buffer.toString() );
            jsw.writeln();
        }

        //-- print imports
        jsw.writeln( "  //---------------------------------------------/" );
        jsw.writeln( " //- Imported classes, interfaces and packages -/" );
        jsw.writeln( "//---------------------------------------------/" );
        jsw.writeln();
        SortedSet<String> allImports = getImports();
        String compUnitPackage = getPackageName();
        for ( String importName : allImports )
        {
            String importsPackage
                = JStructure.getPackageFromClassName( importName );
            if ( importsPackage != null &&
                !importsPackage.equals( compUnitPackage ) )
            {
                jsw.write( "import " );
                jsw.write( importName );
                jsw.writeln( ';' );
            }
        }
        jsw.writeln();

        // Print the public elements, interfaces first, then classes.
        // There should only be one public element, but if there are
        // more we let the compiler catch it.
        printStructures( jsw, true );

        // Print the remaining non-public elements, interfaces first.
        printStructures( jsw, false );

        jsw.flush();
    } //-- print

    /**
     * Print the source code for the contained JClass objects.
     * @param jsw the JSourceWriter to print to.
     * @param printPublic if true, print only public classes; if
     * false, print only non-public classes.
     **/
    final public void printStructures( JSourceWriter jsw, boolean printPublic )
    {

        //-- print class information
        //-- we need to add some JavaDoc API adding comments

        boolean isFirst = true;

        //SortedSet interfaceList = interfaces.sortedOnFullName();
        for ( JInterface jInterface : interfaces )
        {
            if ( jInterface.getModifiers().isPublic() == printPublic )
            {
                if ( isFirst )
                {
                    Header.print( jsw, printPublic );
                    isFirst = false;
                }
                jInterface.print( jsw, true );
                jsw.writeln();
            }
        }

        //SortedSet classList = classes.sortedOnFullName();
        for ( JClass jClass : classes )
        {
            if ( jClass.getModifiers().isPublic() == printPublic )
            {
                if ( isFirst )
                {
                    Header.print( jsw, printPublic );
                    isFirst = false;
                }
                jClass.print( jsw, true );
                jsw.writeln();
            }
        }
    } //-- printElements(JSourceWriter, int)

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

    //-------------------/
    //- Private Methods -/
    //-------------------/

    /**
     * Update the names of nested classes and interfaces.
     **/
    private void resolveNames()
    {

        /*
          Commented out until support for inner-classes is added
          kvisco - 20021211

        for (int i = 0; i < classes.size(); i++) {
            JClass jClass = (JClass) classes.get(i);
            jClass.resolveNames(packageName, null);
        }
        for (int i = 0; i < interfaces.size(); i++) {
            JInterface jInterface = (JInterface) interfaces.get(i);
            jInterface.resolveNames(packageName, null);
        }
        */
    } //-- resolveNames


} //-- JCompUnit

/**
 * Print the headers delineating the public and non-public elements of
 * the compilation unit.
 **/
class Header
{

    private static String[] publicHeader = {
        "  //-----------------------------/",
        " //-  Public Class / Interface -/",
        "//-----------------------------/",
    };
    private static String[] nonPublicHeader = {
        "  //-------------------------------------/",
        " //-  Non-Public Classes / Interfaces  -/",
        "//-------------------------------------/",
    };

    /**
     * Print the specified header to the given Writer.
     * @params JSourceWriter an open JSourceWriter
     * @params boolean if true print the public header, otherwise
     * print the non-public header.
     **/
    protected static void print( JSourceWriter jsw, boolean printPublic )
    {
        String[] header = printPublic ? publicHeader : nonPublicHeader;
        for ( String aHeader : header )
        {
            jsw.writeln( aHeader );
        }
        jsw.writeln();
    }
} //-- Header
