#!/usr/bin/env php 
<?php

main($argv);

function main($argv) {

    $php52 = false;

    $leftOver = array();

    array_shift($argv);

    foreach($argv as $arg) {

        switch($arg) {
            case '--php52' :
                $php52 = true;
                break;
            default :
                $leftOver[] = $arg;

        }

    }

    if (count($leftOver)<1 || count($leftOver)>2) {
        showUsage();
        die(1);
    }

    $directory = $leftOver[0];
    run($directory, isset($leftOver[1])?$leftOver[1]:'-',$php52);

}

function showUsage() {

    echo <<<USAGE
PHPIncludes 0.1.0 by Evert Pot

phpincludes is tool that allows you to easily generate an 'includes' file for
your package. This allows the user of your package to easily include all files
in one go, which is often faster than using an autoloader.

Class-dependencies between packages are automatically calculated, so the order
is always correct.

Usage: phpincludes [--php52] <directory> [outputfile]

  <directory> 
    This is the directory that will be scanned for PHP files. 

  [outputfile]
    Outputfile is the file PHPIncludes writes to. If it's not specified, it will
    be sent to STDOUT

    If the output file already exists, it will attempt to update the existing
    includes file. It does so by looking at two markers in the file:

    // Begin includes\\n
    and
    // End includes\\n

    Every before '// Begin includes\\n' will be retained, as well as everything
    after '// End includes\\n'. Everything in between will be overwritten. The
    \\n is a unix newline.

  --php52 

    By default every include will be prefixed with the __DIR__ constant, so that
    every line looks like:

    include __DIR__ . '/File.php';

    If the php52 option is supplied, the __DIR__ constant is not used, but
    instead every file will be prefixed with dirname(__FILE__).

USAGE;

}

function run($directory, $output, $php52) {

    $err = fopen('php://stderr','w');

    $files = findFiles($directory);
    fwrite($err,"Found " . count($files) . " php files\n");
    fwrite($err,"Parsing files\n");
    $result = findClasses($files);
    fwrite($err,"Calculating dependencies\n");

    $result = sortClasses($result);

    printResult($result, $output, $php52);

}

function findFiles($directory) {
    $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));

    while($it->valid()) {

        if (substr($it->getFileName(),-4)==='.php') {
            $files[] = $it->getPathName();
        }
        $it->next();

    }

    return $files;
}

function findClasses($files) {

    $classes = array();
    foreach($files as $file) {

        $tokens = token_get_all(file_get_contents($file));
        $index = 0;

        $lastClass = null;

        while($index<count($tokens)) {

            $token = $tokens[$index];

            // Classes and interfaces
            if ($token[0] === T_CLASS || $token[0]===T_INTERFACE) {
                while($tokens[$index][0] !== T_STRING) {
                    $index++;
                }
                $className = $tokens[$index][1];
                $classes[$className] = array('filename' => $file, 'dependencies' => array());
                $lastClass = $className;
            }

            // Extends, implements
            if ($tokens[$index][0] === T_EXTENDS || $tokens[$index][0] === T_IMPLEMENTS) {
                while($tokens[$index] !== '{') {
                    $index++;
                    if ($tokens[$index][0] === T_STRING) {
                        $classes[$lastClass]['dependencies'][] = $tokens[$index][1];
                    }
                }
            }
            $index++;

        }

    }
    return $classes;

}

/**
 * This function sorts classes based on their
 * dependencies.
 *
 * It does multiple loops through the classes list until every class
 * is in the sorted list.
 * usort didn't really work for this. Is there a more efficient way?
 *
 * The result is a list of filenames.
 *
 * @param string $classes
 * @return array 
 */
function sortClasses($classes) {

    $result = array();

    // We need a copy
    $fullClassList = $classes;

    $fileNames = array();

    while(count($classes) > 0) {

        foreach($classes as $class=>$info) {

            foreach($info['dependencies'] as $dep) {

                if (!isset($fullClassList[$dep])) {
                   // This dependency does not show up in the classlist at all, 
                   // so we can safely skip it.
                   continue;
                }
                if (!isset($result[$dep])) {
                    // This class is not in the resultset yet, but it will be, 
                    // so we'll skip it for now.
                    continue 2;
                }
            }

            // All dependencies have been met.
            // Adding it to the result, removing it from the source.
            $result[$class] = $info;
            $fileNames[] = $info['filename'];
            unset($classes[$class]);

        }


    }

    return $fileNames;

}

function printResult($result, $output, $php52) {

    $startMarker = "// Begin includes\n";
    $endMarker = "// End includes\n";

    $header = "<?php\n\n";
    $footer = '';  

    if ($output === '-') {

        $handle = fopen('php://stdout','w');

    } else {

        if (file_exists($output)) {

            // We're updating an existing file
            $found = preg_match(
                '#(.*)'.preg_quote($startMarker).'(.*)' . preg_quote($endMarker) . '(.*)$#smD',
                file_get_contents($output),
                $matches
            );
            if (!$found) {
                echo "File with name: " . $output . " was found, but we could not find the start and end-markers\n";
                die(1);
            }

            $header = $matches[1];
            $footer = $matches[3];

        }

        $handle = fopen($output,'w');

    }

    fwrite($handle, $header);
    fwrite($handle, $startMarker);
    foreach($result as $filename) {

        if (strpos($filename,'./')===0) $filename = substr($filename,2);

        if ($php52) {
            fwrite($handle,"include dirname(__FILE__) . '/" . $filename . "';\n");
        } else {
            fwrite($handle,"include __DIR__ . '/" . $filename . "';\n");
        }

    }
    fwrite($handle,$endMarker);
    fwrite($handle,$footer);
    fclose($handle);

}

?>
