/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.space;

import java.io.File;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;

/**
 * {@link Iterator} that iterates over named entries beneath a file-system directory.
 */
final class FileEntryIterator
    implements Iterator<String>
{
    // ----------------------------------------------------------------------
    // Implementation fields
    // ----------------------------------------------------------------------

    private final String rootPath;

    private final Deque<String> entryNames = new ArrayDeque<String>();

    private final boolean recurse;

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    /**
     * Creates an iterator that iterates over entries beneath the given file URL and sub-path.
     * 
     * @param url The root file URL
     * @param subPath The path below the root URL
     * @param recurse When {@code true} include sub-directories; otherwise don't
     */
    FileEntryIterator( final URL url, final String subPath, final boolean recurse )
    {
        rootPath = normalizePath( toFile( url ).getAbsoluteFile() );
        this.recurse = recurse;
        appendEntries( subPath );
    }

    // ----------------------------------------------------------------------
    // Public methods
    // ----------------------------------------------------------------------

    public boolean hasNext()
    {
        return entryNames.size() > 0;
    }

    public String next()
    {
        final String name = entryNames.removeFirst();
        if ( recurse && name.endsWith( "/" ) )
        {
            appendEntries( name );
        }
        return name;
    }

    public void remove()
    {
        throw new UnsupportedOperationException();
    }

    // ----------------------------------------------------------------------
    // Local methods
    // ----------------------------------------------------------------------

    /**
     * Converts a {@link URL} into a {@link File} converting slashes and encoded characters where appropriate.
     * 
     * @param url The file URL
     * @return File instance for the given URL
     */
    static File toFile( final URL url )
    {
        final StringBuilder buf = new StringBuilder();

        final String authority = url.getAuthority();
        if ( null != authority && authority.length() > 0 )
        {
            buf.append( File.separatorChar ).append( File.separatorChar ).append( authority );
        }

        final String path = url.getPath();
        int codePoint = 0, expectBytes = 0;
        for ( int i = 0, length = path.length(); i < length; i++ )
        {
            final char c = path.charAt( i );
            if ( '/' == c )
            {
                buf.append( File.separatorChar );
            }
            else if ( '%' == c && i < length - 2 )
            {
                final int hi = Character.digit( path.charAt( i + 1 ), 16 );
                final int lo = Character.digit( path.charAt( i + 2 ), 16 );
                if ( hi >= 0 && lo >= 0 )
                {
                    if ( hi < 8 )
                    {
                        buf.append( (char) ( hi << 4 | lo ) );
                    }
                    else if ( hi >= 12 )
                    {
                        // prepare multi-byte UTF-8 sequence
                        expectBytes = 12 == hi ? 1 : hi - 12;
                        codePoint = ( 13 == hi ? 0x10 + lo : lo ) << 6 * expectBytes;
                    }
                    else if ( expectBytes > 0 )
                    {
                        // update multi-byte UTF-8 sequence
                        codePoint |= ( ( 0x03 & hi ) << 4 | lo ) << 6 * --expectBytes;
                        if ( expectBytes == 0 )
                        {
                            buf.appendCodePoint( codePoint );
                        }
                    }
                    i += 2;
                }
                else
                {
                    buf.append( '%' );
                }
            }
            else
            {
                buf.append( c );
            }
        }
        return new File( buf.toString() );
    }

    // ----------------------------------------------------------------------
    // Implementation methods
    // ----------------------------------------------------------------------

    /**
     * Appends entries from the given sub-path to the cached list of named entries.
     * 
     * @param subPath The sub path
     */
    private void appendEntries( final String subPath )
    {
        final File[] listing = new File( rootPath + subPath ).listFiles();
        if ( null != listing )
        {
            for ( final File f : listing )
            {
                entryNames.add( normalizePath( f ).substring( rootPath.length() ) );
            }
        }
    }

    /**
     * Returns the normalized URI path of the given file.
     * 
     * @param file The file to normalize
     * @return Normalized URI path
     */
    private static String normalizePath( final File file )
    {
        return file.toURI().getPath();
    }
}
