uses "array", "console", "filesystem", "posix", "regexp", "string", "xml";
uses "GetOptLong";

/*
 * TODO:
 *  Imediate: get this file split up [done]
 *  Need to check for libraries and headers and functions <requires /><optional /> 
 *    farm needs to auto-detect libraries, pkg-config, then $(lib)-config, then linking against -l$(lib)
 *  Need search paths for libraries and includes.
 *  Need to have config caching
 *  Need to have per-os includes for gcc etc [done -> rulesets/`uname`.fe]
 *  Ability to register custom tags: 
 *    string generation 
 *    custom actions [done -> Actions.fe]
 *    file description
 *    file lists [done -> <list type="" />], need to add the ability to reference a file list
 *    global and target specific tags [done-ish, currently got custom target types]
 *  A 'clean' phase = need to implemente Directory.remove()
 */
namespace modifies Farm {
    
    // Platform
    string platform;
    // Stage dir
    string toplevelDirectory;
    // <property /> tags with an global id
    array global_properties;
    // Our targets/phases
    array targets;
    array phases;
    // Phases that have been processed
    array processed_phases;
        
    function generateDerivedSourcesTag( string sourcefile ) {
        return Regexp.replaceAll( '[^A-Za-z]', sourcefile, '_' ) + "_ds";
    }
    function derivedSourcesDirForTarget( object target ) {
        return Farm.toplevelDirectory + "/FarmYard/DerivedSources/${target.uid}";
    }
    function compiledObjectsDirForTarget( object target ) {
        return Farm.toplevelDirectory + "/FarmYard/CompiledObjects/${target.uid}";
    }
    function productsDirForTarget( object target ) {
        return Farm.toplevelDirectory + "/FarmYard/Products";
    }
    function yardDir() {
        return Farm.toplevelDirectory + "/FarmYard";
    }
    
    function expandStringProperties( string str ) {
        object o = new Regexp( '\$\(([A-Za-z.-_]+)\)' );
        object p = new Regexp( '([A-Za-z.-_]+)\.([A-Za-z.-_]+)' );
        
        string s = o.replaceAll( str ) using ( match ) {
            string property = match.capture(0);
            object m = null;
            
            // Look for global propeties
            if( Array.keyExists(Farm.global_properties, property) ) {
                return Farm.global_properties[property];
                // We then look in the targets properties
            } else if( (m = p.match(property)) != null and Array.keyExists(.targets,m.capture(0)) ) {
                object target = .targets[m.capture(0)];
                string v = target.getProperty(m.capture(1));
                if( v != "" )
                    return v;
            }
            return '$(' + property + ')';
        };
        return s;
    }

    function destroyPath( string path ) {
        return Directory.remove( path, true );
    }

    function createPath( string path ) {
        return .createPath( path, 0755 );
    }

    function createPath( string path, number perms ) {
        if( path != "" )
        {
            array a = String.toArray(path,'/');
            if( Array.last(a) == "" )
                return true;
            if( Farm.createPath( Array.join(a[..-2],'/'), perms ) )
            {
                if( Posix.stat(path) )
                    return true;
                if( Directory.make(path, perms) ) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
    function copyFile( string source, string dest, number destp ) {
        object s = File.open( source );
        object d = File.create( dest, destp );
        if( !d ) {
            Farm.displayAction('error', "Couldn't open file: " + dest );
            return false;
        }
        if( !s ) {
            Farm.displayAction('error', "Couldn't open file: " + source );
            return false;
        }
        Farm.displayAction('copy', Farm.basename(source) + ' to ' + dest);
        d.write( s.read( s.length() ) );
        return true;
    }
    function filterCopyFile( string source, string dest, number destp ) {
        object s = File.open( source );
        object d = File.create( dest, destp );
        if( !d ) {
            Farm.displayAction('error', "Couldn't open file: " + dest );
            return false;
        }
        if( !s ) {
            Farm.displayAction('error', "Couldn't open file: " + source );
            return false;
        }
        Farm.displayAction('filtercopy', Farm.basename(source) + ' to ' + dest);
        d.write( Farm.expandStringProperties(s.read( s.length() )) );
        return true;
    }

    function invokeExternalProgram( string command ) {
        if( Sys.system( command + " >>${Farm.yardDir()}/Build.log 2>>${Farm.yardDir()}/Build.log" ) == 0 )
            return true;
        
        Console.println( "\nError Invoking '$command'\n" );
        return false;
    }
    
    function basename( string path ) {
        array components = String.toArray( path, "/" );
        return Array.last(components);
    }
    function dirname( string path ) {
        array components = String.toArray( path, "/" );
        if( Array.size(components) > 1 )
            return Array.join(components[..-2], '/');
        else
            return ".";
    }
        
    object fileTypeRegexp;    
    function sourceFileType( string file ) {
        object match = null;
        if( .fileTypeRegexp == null )
            .fileTypeRegexp = new Regexp('\.([A-Za-z0-9_+]+)$');
        match = .fileTypeRegexp.match(file);
        if( match != null )
            return String.toLower(match.capture(0));
        return "";
    }
        
    function targetExists( string name ) {
        if( Array.keyExists(.targets,name) )
            return true;
        return false;
    }

    function addTarget( string name, object t ) {
        .targets[name] = t;
    }
    
    function invokeTargetAction( string target, string action ) {
        if( Array.keyExists( .targets, target ) ) {
            return .targets[target].invokeAction( action );
        }
        return false;
    }
    
    array paths;
    function addSearchPath( string path ) {
        Array.unshift(.paths, path );
    }
    function searchPaths() {
        return .paths;
    }
    
    array locatedPrograms;
    function locateBinary( string name, array paths ) {
        string fullPath = "";
        if( Array.keyExists(.locatedPrograms, name ) )
            return .locatedPrograms[name];
        else
        {
            Array.each( paths ) using ( path ) {
                if( Posix.stat( path + '/' + name ) != null ) {
                    fullPath = "$path/$name";
                    .locatedPrograms[name] = fullPath;
                    return false;
                }                
            };
        }
        return fullPath;
    }
    
    function programOutput( string prog, string args )
    {
        string program = Farm.locateBinary(prog,Farm.searchPaths());
        if( program !=  "" )
        {
            string value;
            object conf = Posix.openprocess( "$program $args", "r" );
            
            value = conf.readln( );
            if( value != "" )
                value = value[..-2]; // remove the carriage returns
            
            conf.close();
            return value;
        }
        return "";
    }
    
    function sourceDirectoryContents( string dir ) {
        object o = new Regexp(Farm.RuleSet.sourceTypes());
        array files;
        Directory.open( dir ) using ( entry ) {
            if( o.match( entry ) )
                files[] = "$dir/$entry";
        };
        return files;
    }
    function displayAction( string action, string description ) {
        string a = "[$action]";
        Console.println( String.pad("", 14 - String.length(a), " ") + "$a $description" );
    }
    
    function go() {
        string action = "build";
        
        if( Array.size(argv) > 0 )
            action = Array.last(argv);
        
        Console.println('');
        if( Array.keyExists(.phases,action) ) {
            object p = .phases[action];
            if( p.invoke() ) {
                Farm.displayAction('success',':)');
                Console.println('');
                return true;
            } else {
                Farm.displayAction('failed',':(');
                Console.println('');
                return true;
            }
        } 
        Farm.displayAction('error','Unknown phase ' + action );
        return false;
    }

    function installDefaultPhases() {
        Array.each( [ 'build', 'install' ] ) using ( phaseid ) {
            if( not Array.keyExists(.phases,phaseid) ) {
                object phase = new Farm.Phase(phaseid);
                Array.each( .targets ) using ( target ) {
                    object action = new Farm.PerformAction( target.uid, phaseid );
                    phase.addAction( action );
                };            
                .phases[phaseid] = phase;            
            }
        };
        
        .phases['clean'] = new Farm.CleanPhase();
    }

    function parseOptions() {
        object getopt = new GetOptLong(argv);
        array arg = [];
        
        getopt.set_options( [
            [GetOptLong.REQUIRED_ARGUMENT, "--yard"],
            [GetOptLong.REQUIRED_ARGUMENT, "--prefix"],
            [GetOptLong.NO_ARGUMENT, "--help"],
            [GetOptLong.NO_ARGUMENT, "--version"]
            ] );
        
        while( arg = getopt.get() )
        {
            switch( arg[0] )
            {
                case "--yard":
                    Farm.global_properties['YardFile'] = arg[1];
                    break;
                case "--prefix":
                    Farm.global_properties['prefix'] = arg[1];
                    break;
                case "--help":
                    .displayHelp();
                    Sys.exit(0);
                    break;
                case "--version":
                    .displayVersion();
                    Sys.exit(0);
                    break;
            }
        }
    }
    function displayHelp()
    {
        .displayVersion();
        Console.println( "" );
        displayUsage(false);
        Console.println( "" );
        
        Console.println( " Option                 Description" );
        Console.println( "--------------------------------------------------------------" );
        Console.println( " --yard value           Use 'value' as the file to parse for the yard (default: farm.yard)" );
        Console.println( " --prefix value         Use 'value' as the prefix for installed items (default: /usr/local/)" );
        Console.println( "                        NB. The path *must* have a path separator at the end." );
        Console.println( " --version              Display the version number and exit" );
        Console.println( " --help                 This help display" );
    }

    function displayVersion()
    {
        Console.println( "farm v1.0" );
    }
    function displayUsage( number show_help )
    {
        Console.println( "Usage: farm [options] (build|install|clean|...)" );
        if( show_help )
            Console.println( "  Type 'farm --help' for options" );
    }
}

// Load the support files
uses "Yard.fe";
uses "Targets.fe";
uses "Actions.fe";
uses "rulesets/RuleSets.fe";

// This will load our yard file
object yard = new Farm.Yard();
object uname = new Posix.Uname();

// Get the system paths
array systemPaths = String.toArray( Sys.env.read('PATH'), ':' );

Array.each( systemPaths[-1..0] ) using ( path ) {
    Farm.addSearchPath( path );
};

Farm.toplevelDirectory = Directory.getCurrent();
Farm.platform = uname.sysname;

include( "rulesets/${Farm.platform}.fe" );
Farm.displayAction('ruleset', uname.sysname);

// Setup the FarmYard
Directory.make( "FarmYard", 0755 );
Directory.make( "FarmYard/DerivedSources", 0755 );
Directory.make( "FarmYard/CompiledObjects", 0755 );
Directory.make( "FarmYard/Products", 0755 );

// Setup the defaults
Farm.global_properties['DerivedSourcesDir'] = "${Farm.yardDir()}/DerivedSources";
Farm.global_properties['CompiledObjectsDir'] = "${Farm.yardDir()}/CompiledObjects";
Farm.global_properties['ProductDir'] = "${Farm.yardDir()}/Products";

// Ferite defaults
Farm.global_properties['FeriteLibraryDir'] = Farm.programOutput('ferite-config', '--library-path');
Farm.global_properties['FeriteModuleSourceDir'] = Farm.programOutput('ferite-config', '--module-library-path');
Farm.global_properties['FeriteModuleDescriptionDir'] = "${Farm.programOutput('ferite-config', '--library-path')}/module-descriptions";
Farm.global_properties['FeriteModuleNativeDir'] = Farm.programOutput('ferite-config', '--native-library-path');

Farm.global_properties['YardFile'] = 'farm.yard';
Farm.global_properties['prefix'] = '/usr/local/';

Farm.parseOptions();
yard.loadYard(Farm.global_properties['YardFile']);
Farm.installDefaultPhases();

if( not Farm.go() )
    return 1;
return 0;
