package org.codehaus.plexus.components.io.attributes;

/*
 * Copyright 2007 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 org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.cli.CommandLineCallable;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.regex.Pattern;

@SuppressWarnings( { "NullableProblems" } )
public final class PlexusIoResourceAttributeUtils
{

    private PlexusIoResourceAttributeUtils()
    {
    }


    public static PlexusIoResourceAttributes mergeAttributes( PlexusIoResourceAttributes override,
                                                              PlexusIoResourceAttributes base,
                                                              PlexusIoResourceAttributes def )
    {
        if ( override == null )
        {
            return base;
        }
        SimpleResourceAttributes result;
        if ( base == null )
        {
            result = new SimpleResourceAttributes();
        }
        else
        {
            result = new SimpleResourceAttributes( base.getUserId(), base.getUserName(), base.getGroupId(),
                                                   base.getGroupName(), base.getOctalMode() );
            result.setSymbolicLink(  base.isSymbolicLink() );
        }

        if ( override.getGroupId() != null && override.getGroupId() != -1 )
        {
            result.setGroupId( override.getGroupId() );
        }

        if ( def != null && def.getGroupId() >= 0 && ( result.getGroupId() == null || result.getGroupId() < 0 ) )
        {
            result.setGroupId( def.getGroupId() );
        }

        if ( override.getGroupName() != null )
        {
            result.setGroupName( override.getGroupName() );
        }

        if ( def != null && result.getGroupName() == null )
        {
            result.setGroupName( def.getGroupName() );
        }

        if ( override.getUserId() != null && override.getUserId() != -1 )
        {
            result.setUserId( override.getUserId() );
        }

        if ( def != null && def.getUserId() >= 0 && ( result.getUserId() == null || result.getUserId() < 0 ) )
        {
            result.setUserId( def.getUserId() );
        }

        if ( override.getUserName() != null )
        {
            result.setUserName( override.getUserName() );
        }

        if ( def != null && result.getUserName() == null )
        {
            result.setUserName( def.getUserName() );
        }

        if ( override.getOctalMode() > 0 )
        {
            result.setOctalMode( override.getOctalMode() );
        }

        if ( def != null && result.getOctalMode() < 0 )
        {
            result.setOctalMode( def.getOctalMode() );
        }

        return result;
    }

    public static boolean isGroupExecutableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_GROUP_EXECUTE );
    }

    public static boolean isGroupReadableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_GROUP_READ );
    }

    public static boolean isGroupWritableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_GROUP_WRITE );
    }

    public static boolean isOwnerExecutableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_OWNER_EXECUTE );
    }

    public static boolean isOwnerReadableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_OWNER_READ );
    }

    public static boolean isOwnerWritableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_OWNER_WRITE );
    }

    public static boolean isWorldExecutableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_WORLD_EXECUTE );
    }

    public static boolean isWorldReadableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_WORLD_READ );
    }

    public static boolean isWorldWritableInOctal( int mode )
    {
        return isOctalModeEnabled( mode, AttributeConstants.OCTAL_WORLD_WRITE );
    }

    public static boolean isOctalModeEnabled( int mode, int targetMode )
    {
        return ( mode & targetMode ) != 0;
    }

    @SuppressWarnings( { "UnusedDeclaration" } )
    public static PlexusIoResourceAttributes getFileAttributes( File file )
        throws IOException
    {
        Map<String, PlexusIoResourceAttributes> byPath = getFileAttributesByPath( file, false, true );
        final PlexusIoResourceAttributes o = byPath.get( file.getAbsolutePath() );
        if ( o == null )
        {
            // We're on a crappy old java version (5) or the OS from hell. Just "fail".
            return SimpleResourceAttributes.lastResortDummyAttributesForBrokenOS();

        }
        return o;
    }

    public static Map<String, PlexusIoResourceAttributes> getFileAttributesByPath( File dir )
        throws IOException
    {
        return getFileAttributesByPath( dir, true, true );
    }

    public static Map<String, PlexusIoResourceAttributes> getFileAttributesByPath( File dir, boolean recursive,
                                                                                   boolean includeNumericUserId )
        throws IOException
    {
        if ( Java7Reflector.isAtLeastJava7() )
        {
            return getFileAttributesByPathJava7( dir, recursive );
        }

        if ( !enabledOnCurrentOperatingSystem() )
        {
            //noinspection unchecked
            return Collections.emptyMap();
        }

        return getFileAttributesByPathScreenScrape( dir, recursive, includeNumericUserId );
    }

    public static void main( String[] args )
        throws IOException
    {
        if ( args.length < 0 )
        {
            System.out.println( "You must supply one directory to scan:" );
            return;
        }
        File dir = new File( args[0] );
        final Map<String, PlexusIoResourceAttributes> fileAttributesByPathScreenScrape =
            getFileAttributesByPathScreenScrape( dir, true, true );
        for ( String s : fileAttributesByPathScreenScrape.keySet() )
        {
            System.out.println( s + ":" + fileAttributesByPathScreenScrape.get( s ) );
        }


    }

    static Map<String, PlexusIoResourceAttributes> getFileAttributesByPathScreenScrape( File dir, boolean recursive,
                                                                                        boolean includeNumericUserId )
        throws IOException
    {
        StringBuilder loggerCache = new StringBuilder();
        StreamConsumer logger = createStringBuilderStreamConsumer( loggerCache );

        AttributeParser.NumericUserIDAttributeParser numericIdParser = null;
        FutureTask<Integer> integerFutureTask = null;
        Commandline numericCli;
        if ( includeNumericUserId )
        {
            numericIdParser = new AttributeParser.NumericUserIDAttributeParser( logger );

            String lsOptions1 = "-1nla" + ( recursive ? "R" : "d" );
            StreamConsumer stdErr = new ErrorMessageStreamConsumer();
            try
            {
                numericCli = setupCommandLine( dir, lsOptions1, logger );

                CommandLineCallable commandLineCallable =
                    CommandLineUtils.executeCommandLineAsCallable( numericCli, null, numericIdParser, stdErr, 0 );

                integerFutureTask = new FutureTask<Integer>( commandLineCallable );
                new Thread( integerFutureTask ).start();
            }
            catch ( CommandLineException e )
            {
                IOException error = new IOException(
                    "Failed to quote directory: '" + dir + "':" + e.getMessage() + "\n" + stdErr.toString() + logger.toString() );
                error.initCause( e );
                throw error;
            }
        }

//        loggerCache.setLength( 0 );
        AttributeParser.SymbolicUserIDAttributeParser userId = getNameBasedParser( dir, logger, recursive );

        if ( includeNumericUserId )
        {
            final Integer result;
            try
            {
                result = integerFutureTask.get();
            }
            catch ( InterruptedException e )
            {
                throw new RuntimeException( e );
            }
            catch ( ExecutionException e )
            {
                throw new RuntimeException( e );
            }

            if ( result != 0 )
            {
                throw new IOException( "Failed (3) to retrieve numeric file attributes using:\n" + logger.toString() );
            }
        }

        return userId.merge( numericIdParser );
    }

    private static AttributeParser.SymbolicUserIDAttributeParser getNameBasedParser( File dir, StreamConsumer logger,
                                                                                     boolean recursive )
        throws IOException
    {
        AttributeParser.SymbolicUserIDAttributeParser userId =
            new AttributeParser.SymbolicUserIDAttributeParser( logger );

        StreamConsumer stdErr = new ErrorMessageStreamConsumer();
        String lsOptions2 = "-1la" + ( recursive ? "R" : "d" );
        try
        {
            executeLs( dir, lsOptions2, userId, logger );
        }
        catch ( CommandLineException e )
        {
            IOException error =
                new IOException( "Failed to quote directory(2): '" + dir + "':" + e.getMessage() + "\n" + stdErr.toString() + logger.toString() );
            error.initCause( e );

            throw error;
        }
        return userId;
    }

    private static @Nonnull Map<String, PlexusIoResourceAttributes> getFileAttributesByPathJava7( @Nonnull File dir, boolean recursive )
        throws IOException
    {
        Map<Integer, String> userCache = new HashMap<Integer, String>();
        Map<Integer, String> groupCache = new HashMap<Integer, String>();
        final List<String> fileAndDirectoryNames;
        if ( recursive && dir.isDirectory() )
        {
            fileAndDirectoryNames = FileUtils.getFileAndDirectoryNames( dir, null, null, true, true, true, true );
        }
        else
        {
            fileAndDirectoryNames = Collections.singletonList( dir.getAbsolutePath() );
        }

        final Map<String, PlexusIoResourceAttributes> attributesByPath =
            new LinkedHashMap<String, PlexusIoResourceAttributes>();

        for ( String fileAndDirectoryName : fileAndDirectoryNames )
        {
            attributesByPath.put( fileAndDirectoryName,
                                  new Java7FileAttributes( new File( fileAndDirectoryName ), userCache, groupCache ) );
        }
        return attributesByPath;
    }


    private static boolean enabledOnCurrentOperatingSystem()
    {
        return !Os.isFamily( Os.FAMILY_WINDOWS ) && !Os.isFamily( Os.FAMILY_WIN9X );
    }


    private static void executeLs( File dir, String options, StreamConsumer parser, StreamConsumer logger )
        throws IOException, CommandLineException
    {
        Commandline numericCli = setupCommandLine( dir, options, logger );

        StreamConsumer stdErr = new ErrorMessageStreamConsumer();
        try
        {
            int result = CommandLineUtils.executeCommandLine( numericCli, parser, stdErr );

            if ( result != 0 )
            {
                throw new IOException( stdErr.toString() +
                                           "When scraping numeric file attributes:\n" + logger.toString() );
            }
        }
        catch ( CommandLineException e )
        {
            IOException error = new IOException(
                "Failed (2) to retrieve numeric file attributes using:\n" + stdErr.toString() + "\n"
                    + logger.toString() );
            error.initCause( e );

            throw error;
        }
    }

    private static Commandline setupCommandLine( @Nonnull File dir, String options, StreamConsumer logger )
    {
        Commandline numericCli = new Commandline();

        numericCli.getShell().setQuotedArgumentsEnabled( true );
        numericCli.getShell().setQuotedExecutableEnabled( false );

        numericCli.addEnvironment( "LANG", "C" );
        numericCli.addEnvironment( "TIME_STYLE", "long-iso" );
        numericCli.setExecutable( "ls" );

        numericCli.createArg().setLine( options );

        numericCli.createArg().setValue( dir.getAbsolutePath() );

        logger.consumeLine( "\nExecuting: " + numericCli.toString() + "\n" );
        return numericCli;
    }

    static class ErrorMessageStreamConsumer
        implements StreamConsumer
    {

        StringBuilder errorOutput = new StringBuilder();

        public synchronized void consumeLine( String line )
        {
            errorOutput.append( line ).append( "\n" );
        }

        public synchronized String toString()
        {
            return errorOutput.toString();
        }
    }

    private static @Nonnull StreamConsumer createStringBuilderStreamConsumer( @Nonnull final StringBuilder sb )
    {
        return new StreamConsumer()
        {
            public synchronized void consumeLine( String line )
            {
                sb.append( line ).append( "\n" );
            }

            public synchronized String toString()
            {
                return sb.toString();
            }
        };
    }

    static final Pattern totalLinePattern = Pattern.compile( "\\w*\\s\\d*" );
}
