package org.codehaus.plexus.util;

/*
 * Copyright The Codehaus Foundation.
 *
 * 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.
 */

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

/**
 * DirectoryWalker
 * @version $Id$
 */
public class DirectoryWalker
{
    /**
     * DirStackEntry is an Item on the {@link DirectoryWalker#dirStack}
     */
    class DirStackEntry
    {
        /**
         * Count of files in the directory.
         */
        public int count;

        /**
         * Current Directory.
         */
        public File dir;

        /**
         * Index (or offset) within the directory count.
         */
        public int index;

        /**
         * Offset for percentage calculations. Based on parent DirStackEntry.
         */
        public double percentageOffset;

        /**
         * Size of percentage space to work with.
         */
        public double percentageSize;

        /**
         * Create a DirStackEntry.
         *
         * @param d the directory to track
         * @param length the length of entries in the directory.
         */
        public DirStackEntry( File d, int length )
        {
            dir = d;
            count = length;
        }

        /**
         * Calculate the next percentage offset.
         * Used by the next DirStackEntry.
         *
         * @return the value for the next percentage offset.
         */
        public double getNextPercentageOffset()
        {
            return percentageOffset + ( index * ( percentageSize / count ) );
        }

        /**
         * Calculate the next percentage size.
         * Used by the next DirStackEntry.
         *
         * @return the value for the next percentage size.
         */
        public double getNextPercentageSize()
        {
            return ( percentageSize / count );
        }

        /**
         * The percentage of the DirStackEntry right now.
         * Based on count, index, percentageOffset, and percentageSize.
         *
         * @return the percentage right now.
         */
        public int getPercentage()
        {
            double percentageWithinDir = (double) index / (double) count;
            return (int) Math.floor( percentageOffset + ( percentageWithinDir * percentageSize ) );
        }

        public String toString()
        {
            return "DirStackEntry[" + "dir=" + dir.getAbsolutePath() + ",count=" + count + ",index=" + index
                + ",percentageOffset=" + percentageOffset + ",percentageSize=" + percentageSize + ",percentage()="
                + getPercentage() + ",getNextPercentageOffset()=" + getNextPercentageOffset()
                + ",getNextPercentageSize()=" + getNextPercentageSize() + "]";
        }
    }

    private File baseDir;

    private int baseDirOffset;

    private Stack<DirectoryWalker.DirStackEntry> dirStack;

    private List<String> excludes;

    private List<String> includes;

    private boolean isCaseSensitive = true;

    private List<DirectoryWalkListener> listeners;

    private boolean debugEnabled = false;

    public DirectoryWalker()
    {
        includes = new ArrayList<String>();
        excludes = new ArrayList<String>();
        listeners = new ArrayList<DirectoryWalkListener>();
    }

    public void addDirectoryWalkListener( DirectoryWalkListener listener )
    {
        listeners.add( listener );
    }

    public void addExclude( String exclude )
    {
        excludes.add( fixPattern( exclude ) );
    }

    public void addInclude( String include )
    {
        includes.add( fixPattern( include ) );
    }

    /**
     * Add's to the Exclude List the default list of SCM excludes.
     */
    public void addSCMExcludes()
    {
        String scmexcludes[] = DirectoryScanner.DEFAULTEXCLUDES;
        for ( String scmexclude : scmexcludes )
        {
            addExclude( scmexclude );
        }
    }

    private void fireStep( File file )
    {
        DirStackEntry dsEntry = dirStack.peek();
        int percentage = dsEntry.getPercentage();
        for ( Object listener1 : listeners )
        {
            DirectoryWalkListener listener = (DirectoryWalkListener) listener1;
            listener.directoryWalkStep( percentage, file );
        }
    }

    private void fireWalkFinished()
    {
        for ( DirectoryWalkListener listener1 : listeners )
        {
            listener1.directoryWalkFinished();
        }
    }

    private void fireWalkStarting()
    {
        for ( DirectoryWalkListener listener1 : listeners )
        {
            listener1.directoryWalkStarting( baseDir );
        }
    }

    private void fireDebugMessage( String message )
    {
        for ( DirectoryWalkListener listener1 : listeners )
        {
            listener1.debug( message );
        }
    }

    private String fixPattern( String pattern )
    {
        String cleanPattern = pattern;

        if ( File.separatorChar != '/' )
        {
            cleanPattern = cleanPattern.replace( '/', File.separatorChar );
        }

        if ( File.separatorChar != '\\' )
        {
            cleanPattern = cleanPattern.replace( '\\', File.separatorChar );
        }

        return cleanPattern;
    }

    public void setDebugMode( boolean debugEnabled )
    {
        this.debugEnabled = debugEnabled;
    }

    /**
     * @return Returns the baseDir.
     */
    public File getBaseDir()
    {
        return baseDir;
    }

    /**
     * @return Returns the excludes.
     */
    public List<String> getExcludes()
    {
        return excludes;
    }

    /**
     * @return Returns the includes.
     */
    public List<String> getIncludes()
    {
        return includes;
    }

    private boolean isExcluded( String name )
    {
        return isMatch( excludes, name );
    }

    private boolean isIncluded( String name )
    {
        return isMatch( includes, name );
    }

    private boolean isMatch( List<String> patterns, String name )
    {
        for ( String pattern1 : patterns )
        {
            if ( SelectorUtils.matchPath( pattern1, name, isCaseSensitive ) )
            {
                return true;
            }
        }

        return false;
    }

    private String relativeToBaseDir( File file )
    {
        return file.getAbsolutePath().substring( baseDirOffset + 1 );
    }

    /**
     * Removes a DirectoryWalkListener.
     *
     * @param listener the listener to remove.
     */
    public void removeDirectoryWalkListener( DirectoryWalkListener listener )
    {
        listeners.remove( listener );
    }

    /**
     * Performs a Scan against the provided {@link #setBaseDir(File)}
     */
    public void scan()
    {
        if ( baseDir == null )
        {
            throw new IllegalStateException( "Scan Failure.  BaseDir not specified." );
        }

        if ( !baseDir.exists() )
        {
            throw new IllegalStateException( "Scan Failure.  BaseDir does not exist." );
        }

        if ( !baseDir.isDirectory() )
        {
            throw new IllegalStateException( "Scan Failure.  BaseDir is not a directory." );
        }

        if ( includes.isEmpty() )
        {
            // default to include all.
            addInclude( "**" );
        }

        if ( debugEnabled )
        {
            Iterator<String> it;
            StringBuilder dbg = new StringBuilder();
            dbg.append( "DirectoryWalker Scan" );
            dbg.append( "\n  Base Dir: " ).append( baseDir.getAbsolutePath() );
            dbg.append( "\n  Includes: " );
            it = includes.iterator();
            while ( it.hasNext() )
            {
                String include = it.next();
                dbg.append( "\n    - \"" ).append( include ).append( "\"" );
            }
            dbg.append( "\n  Excludes: " );
            it = excludes.iterator();
            while ( it.hasNext() )
            {
                String exclude = it.next();
                dbg.append( "\n    - \"" ).append( exclude ).append( "\"" );
            }
            fireDebugMessage( dbg.toString() );
        }

        fireWalkStarting();
        dirStack = new Stack();
        scanDir( baseDir );
        fireWalkFinished();
    }

    private void scanDir( File dir )
    {
        File[] files = dir.listFiles();

        if ( files == null )
        {
            return;
        }

        DirectoryWalker.DirStackEntry curStackEntry = new DirectoryWalker.DirStackEntry( dir, files.length );
        if ( dirStack.isEmpty() )
        {
            curStackEntry.percentageOffset = 0;
            curStackEntry.percentageSize = 100;
        }
        else
        {
            DirectoryWalker.DirStackEntry previousStackEntry = (DirectoryWalker.DirStackEntry) dirStack.peek();
            curStackEntry.percentageOffset = previousStackEntry.getNextPercentageOffset();
            curStackEntry.percentageSize = previousStackEntry.getNextPercentageSize();
        }

        dirStack.push( curStackEntry );

        for ( int idx = 0; idx < files.length; idx++ )
        {
            curStackEntry.index = idx;
            String name = relativeToBaseDir( files[idx] );

            if ( isExcluded( name ) )
            {
                fireDebugMessage( name + " is excluded." );
                continue;
            }

            if ( files[idx].isDirectory() )
            {
                scanDir( files[idx] );
            }
            else
            {
                if ( isIncluded( name ) )
                {
                    fireStep( files[idx] );
                }
            }
        }

        dirStack.pop();
    }

    /**
     * @param baseDir The baseDir to set.
     */
    public void setBaseDir( File baseDir )
    {
        this.baseDir = baseDir;
        baseDirOffset = baseDir.getAbsolutePath().length();
    }

    /**
     * @param entries The excludes to set.
     */
    public void setExcludes( List<String> entries )
    {
        excludes.clear();
        if ( entries != null )
        {
            for ( String entry : entries )
            {
                excludes.add( fixPattern( entry ) );
            }
        }
    }

    /**
     * @param entries The includes to set.
     */
    public void setIncludes( List<String> entries )
    {
        includes.clear();
        if ( entries != null )
        {
            for ( String entry : entries )
            {
                includes.add( fixPattern( entry ) );
            }
        }
    }

}
