/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2001-2002 The Apache Software Foundation.  All rights 
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 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 end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:  
 *       "This product includes software developed by the 
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Ant", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written 
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``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 THE APACHE SOFTWARE FOUNDATION 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.tools.ant.taskdefs;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.FileSet;

import java.util.Vector;
import java.io.File;

/**
 * This task converts path and classpath information to a specific target OS format.
 * The resulting formatted path is placed into a specified property.
 * <p>
 * LIMITATION: Currently this implementation groups all machines into one of two
 * types: Unix or Windows.  Unix is defined as NOT windows.
 *
 * @author Larry Streepy <a href="mailto:streepy@healthlanguage.com">streepy@healthlanguage.com</a>
 *
 * @ant.task category="utility"
 */
public class PathConvert extends Task {

    /**
     * Helper class, holds the nested <map> values.  Elements will look like this:
     * &lt;map from="d:" to="/foo"/>
     * <p>
     * When running on windows, the prefix comparison will be case insensitive.
     */
    public class MapEntry {

        /**
         * Set the "from" attribute of the map entry
         */
        public void setFrom( String from ) {
            this.from = from;
        }

        /**
         * Set the "to" attribute of the map entry
         */
        public void setTo( String to ) {
            this.to = to;
        }

        /**
         * Apply this map entry to a given path element
         * @param elem Path element to process
         * @return String Updated path element after mapping
         */
        public String apply( String elem ) {
            if( from == null || to == null ) {
                throw new BuildException( "Both 'from' and 'to' must be set in a map entry" );
            }

            // If we're on windows, then do the comparison ignoring case
            String cmpElem = onWindows ? elem.toLowerCase() : elem;
            String cmpFrom = onWindows ? from.toLowerCase() : from;

            // If the element starts with the configured prefix, then convert the prefix
            // to the configured 'to' value.

            if( cmpElem.startsWith( cmpFrom ) ) {
                int len = from.length();

                if( len >= elem.length() ) {
                    elem = to;
                } else {
                    elem = to + elem.substring( len );
                }
            }

            return elem;
        }

        // Members
        private String from = null;
        private String to = null;
    }

    /**
     * Create a nested PATH element
     */
    public Path createPath() {

        if( isReference() ) {
            throw noChildrenAllowed();
        }

        if( path == null ) {
            path = new Path(getProject());
        }
        return path.createPath();
    }

    /**
     * Create a nested MAP element
     */
    public MapEntry createMap() {

        MapEntry entry = new MapEntry();
        prefixMap.addElement( entry );
        return entry;
    }

    /**
     * Set the value of the targetos attribute
     */
    public void setTargetos( String target ) {

        targetOS = target.toLowerCase();

        if( ! targetOS.equals( "windows" ) && ! target.equals( "unix" ) && 
            ! targetOS.equals( "netware" )) {
            throw new BuildException( "targetos must be one of 'unix', 'netware', or 'windows'" );
        }

        // Currently, we deal with only two path formats: Unix and Windows
        // And Unix is everything that is not Windows

        // for NetWare, piggy-back on Windows, since in the validateSetup code,
        // the same assumptions can be made as with windows -
        // that ; is the path separator

        targetWindows = (targetOS.equals("windows") || targetOS.equals("netware"));
    }

    /**
     * Set the value of the proprty attribute - this is the property into which our
     * converted path will be placed.
     */
    public void setProperty( String p ) {
        property = p;
    }

    /**
     * Adds a reference to a PATH or FILESET defined elsewhere.
     */
    public void setRefid(Reference r) {
        if( path != null ) {
            throw noChildrenAllowed();
        }

        refid = r;
    }

    /**
     * Override the default path separator string for the target os
     */
    public void setPathSep( String sep ) {
        pathSep = sep;
    }

    /**
     * Override the default directory separator string for the target os
     */
    public void setDirSep( String sep ) {
        dirSep = sep;
    }

    /**
     * Has the refid attribute of this element been set?
     */
    public boolean isReference() {
        return refid != null;
    }

    /**
     * Do the execution.
     */
    public void execute() throws BuildException {

        // If we are a reference, the create a Path from the reference
        if( isReference() ) {
            path = new Path(getProject()).createPath();

            Object obj = refid.getReferencedObject(getProject());

            if( obj instanceof Path ) {
                path.setRefid(refid);
            } else if( obj instanceof FileSet ) {
                FileSet fs = (FileSet)obj;
                path.addFileset( fs );
            } else {
                throw new BuildException( "'refid' does not refer to a path or fileset" );
            }
        }

        validateSetup();                    // validate our setup

        // Currently, we deal with only two path formats: Unix and Windows
        // And Unix is everything that is not Windows
        // (with the exception for NetWare below)

        String osname = System.getProperty("os.name").toLowerCase();

        // for NetWare, piggy-back on Windows, since here and in the
        // apply code, the same assumptions can be made as with windows -
        // that \\ is an OK separator, and do comparisons case-insensitive.
        onWindows = ( (osname.indexOf("windows") >= 0) ||
                      (osname.indexOf("netware") >= 0)  );

        // Determine the from/to char mappings for dir sep
        char fromDirSep = onWindows ? '\\' : '/';
        char toDirSep   = dirSep.charAt(0);

        StringBuffer rslt = new StringBuffer( 100 );

        // Get the list of path components in canonical form
        String[] elems = path.list();

        for( int i=0; i < elems.length; i++ ) {
            String elem = elems[i];

            elem = mapElement( elem );      // Apply the path prefix map

            // Now convert the path and file separator characters from the
            // current os to the target os.

            elem = elem.replace( fromDirSep, toDirSep );

            if( i != 0 ) {
              rslt.append( pathSep );
            }
            rslt.append( elem );
        }

        // Place the result into the specified property
        String value = rslt.toString();

        log( "Set property " + property + " = " + value, Project.MSG_VERBOSE );

        getProject().setNewProperty( property, value );
    }

    /**
     * Apply the configured map to a path element.  The map is used to convert
     * between Windows drive letters and Unix paths.  If no map is configured,
     * then the input string is returned unchanged.
     *
     * @param elem The path element to apply the map to
     * @return String Updated element
     */
    private String mapElement( String elem ) {

        int size = prefixMap.size();

        if( size != 0 ) {

            // Iterate over the map entries and apply each one.  Stop when one of the
            // entries actually changes the element

            for( int i=0; i < size; i++ ) {
                MapEntry entry = (MapEntry)prefixMap.elementAt(i);
                String newElem = entry.apply( elem );

                // Note I'm using "!=" to see if we got a new object back from
                // the apply method.

                if( newElem != elem ) {
                    elem = newElem;
                    break;                  // We applied one, so we're done
                }
            }
        }

        return elem;
    }

    /**
     * Validate that all our parameters have been properly initialized.
     * @throws BuildException if something is not setup properly
     */
    private void validateSetup() throws BuildException {

        if( path == null ) {
            throw new BuildException( "You must specify a path to convert" );
        }

        if( property == null ) {
            throw new BuildException( "You must specify a property" );
        }

        // Must either have a target OS or both a dirSep and pathSep

        if( targetOS == null && pathSep == null && dirSep == null ) {
            throw new BuildException( "You must specify at least one of targetOS, dirSep, or pathSep" );
        }

        // Determine the separator strings.  The dirsep and pathsep attributes
        // override the targetOS settings.
        String dsep = File.separator;
        String psep = File.pathSeparator;

        if( targetOS != null ) {
            psep = targetWindows ? ";" : ":";
            dsep = targetWindows ? "\\" : "/";
        }

        if( pathSep != null ) {             // override with pathsep=
            psep = pathSep;
        }

        if( dirSep != null ) {              // override with dirsep=
            dsep = dirSep;
        }

        pathSep = psep;
        dirSep = dsep;
    }

    /**
     * Creates an exception that indicates that this XML element must
     * not have child elements if the refid attribute is set.  
     */
    private BuildException noChildrenAllowed() {
        return new BuildException("You must not specify nested PATH elements when using refid");
    }


    // Members
    private Path path = null;               // Path to be converted
    private Reference refid = null;         // Reference to path/fileset to convert
    private String targetOS = null;         // The target OS type
    private boolean targetWindows = false;  // Set when targetOS is set
    private boolean onWindows = false;      // Set if we're running on windows
    private String property = null;         // The property to receive the results
    private Vector prefixMap = new Vector();  // Path prefix map
    private String pathSep = null;          // User override on path sep char
    private String dirSep = null;           // User override on directory sep char
}
