#!/usr/bin/env php
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 filetype=php: */

if (!extension_loaded('inotify')) throw new Exception("Please install inotify: pecl install inotify");

$inotifyR = inotify_init();
if ($inotifyR === false) throw new Exception("inotify_init failed");

if (!file_exists('watchngo.config')) throw new Exception("No watchngo.config");
include 'watchngo.config';
if (!isset($config)) throw new Exception('No $config in watchngo.config');

print "Parsing config...\n";
$manager = array();
$watchIdToConfigIdMap = array();
$watchIdToPathMap = array();
$i = 1;
$triggerFullMap = array();
foreach ($config as $id => $c) {
    print " [{$i}] => {$id}\n";
    $triggerFullMap["{$i}"] = $id;
    $i++;

    $manager[$id] = array(
        'monitor'  => is_array($c['monitor']) ? $c['monitor'] : array($c['monitor']),
        'perform'  => $c['perform'],
        'debounce' => 500 / 1000,
        'lastDts'  => $c['triggerOnStart'] ? 0 : NULL,
    );
}
print <<<EOL

Want to manually trigger a rebuild?
=> RETURN to rebuild all
=> ID from above to trigger individual module

EOL;

print PHP_EOL . "Installing watches...\n";
foreach ($manager as $id => $c) {
    foreach ($c['monitor'] as $f) {
        $watchId = inotify_add_watch($inotifyR, $f, IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM);   // IN_ATTRIB gets php::touch()
        $watchIdToConfigIdMap[$watchId] = $id;
        $watchIdToPathMap[$watchId] = $f;
    }
}

print "Staring monitoring...\n";
stream_set_blocking($inotifyR, 0);

// watch STDIN for input related to manual rebuilds
$stdin = fopen('php://stdin', 'r');
$oldStty = `stty -g`;
system("stty -icanon");
stream_set_blocking($stdin, 0);

while (true) {
    // watch for numeric input to run specific IDs
    $readId = fread($stdin, 10);
    if ($readId !== false and !empty($readId))
    {
        $manualRecompileList = array();
        if ($readId === PHP_EOL)
        {
            print PHP_EOL . "=> REBUILD ALL" . PHP_EOL;
            $manualRecompileList = array_values($triggerFullMap);
        }
        else
        {
            $readId = trim($readId);
            if (isset($triggerFullMap[$readId]))
            {
                print PHP_EOL . "=> [{$readId}] TRIGGER {$triggerFullMap[$readId]}" . PHP_EOL;
                $manualRecompileList[] = $triggerFullMap[$readId];
            }
            else
            {
                print PHP_EOL . "=> NO MATCH {$readId}" . PHP_EOL;
            }
        }
        foreach ($manualRecompileList as $managerId) {
            // touch all to ensure rebuild doesn't just say "unchanged" (i'm looking at you, compass)
            $res = array_map('touch', $manager[$managerId]['monitor']);
            fire($manager, $managerId);
        }
    }

    $events = inotify_read($inotifyR);
    if ($events === false)
    {
        checkDebounce($manager);
        sleep(1);
        continue;
    }

    $now = microtime(true);
    foreach ($events as $event) {
        $watchId = $event['wd'];
        $configId = $watchIdToConfigIdMap[$watchId];
        $manager[$configId]['lastDts'] = $now;

        logEvent($event, $watchId, $watchIdToPathMap);
    }
    checkDebounce($manager);
}
system("stty {$oldStty}");

function checkDebounce(&$manager)
{
    print '.';

    // check for debounce timeouts
    $now = microtime(true);
    foreach ($manager as $id => $c) {
        if ($c['lastDts'] === NULL)
        {
            continue;
        }

        $tdiff = $now - $c['lastDts'];
        if ($tdiff > $c['debounce'])
        {
            print "\n";
            fire($manager, $id);
        }
    }
}

function fire(&$manager, $id)
{
    print "[{$id}] {$manager[$id]['perform']}\n";
    print `{$manager[$id]['perform']}`;
    $manager[$id]['lastDts'] = NULL;
}

function logEvent($event, $watchId, $watchIdToPathMap)
{
    return;

    $in_events = array(
        'IN_ACCESS' => IN_ACCESS,
        'IN_MODIFY' => IN_MODIFY,
        'IN_ATTRIB' => IN_ATTRIB,
        'IN_CLOSE_WRITE' => IN_CLOSE_WRITE,
        'IN_CLOSE_NOWRITE' => IN_CLOSE_NOWRITE,
        'IN_OPEN' => IN_OPEN,
        'IN_MOVED_TO' => IN_MOVED_TO,
        'IN_MOVED_FROM' => IN_MOVED_FROM,
        'IN_CREATE' => IN_CREATE,
        'IN_DELETE' => IN_DELETE,
        'IN_DELETE_SELF' => IN_DELETE_SELF,
        'IN_MOVE_SELF' => IN_MOVE_SELF,
        'IN_CLOSE' => IN_CLOSE,
        'IN_MOVE' => IN_MOVE,
        'IN_ALL_EVENTS' => IN_ALL_EVENTS,
        'IN_UNMOUNT' => IN_UNMOUNT,
        'IN_Q_OVERFLOW' => IN_Q_OVERFLOW,
        'IN_IGNORED' => IN_IGNORED,
        'IN_ISDIR' => IN_ISDIR,
        'IN_ONLYDIR' => IN_ONLYDIR,
        'IN_DONT_FOLLOW' => IN_DONT_FOLLOW,
        'IN_MASK_ADD' => IN_MASK_ADD,
        'IN_ONESHOT' => IN_ONESHOT,
    );

    print "{$watchIdToPathMap[$watchId]}:\n";
    foreach ($in_events as $str => $val) {
        if ($event['mask'] & $val) print "  => {$str}\n";
    }
}

/**
 * @param mixed VARGS: any number of strings or arrays, each being a file path or a glob to watch.
 * @return array The set of files matches the passed globs.
 */
function recursive_glob($requestedGlobs)
{
    // expoode vargs
    $requestedGlobs = array();
    foreach (func_get_args() as $arg) {
        if (!is_array($arg))
        {
            $arg = array($arg);
        }
        $requestedGlobs = array_merge($requestedGlobs, $arg);
    }
    $requestedGlobs = array_unique($requestedGlobs);

    // expand all a/b/**/c/*.foo into globs
    $globsToProcess = array();
    foreach ($requestedGlobs as $glob) {
        $hasRecursiveDirGlob = (strpos($glob, '**') !== false);
        if ($hasRecursiveDirGlob)
        {
            list($baseDir, $subGlob) = explode('**', $glob, 2);
            $baseDir = rtrim($baseDir, '/');
            $subGlob = ltrim($subGlob, '/');

            // root glob
            $globsToProcess[] = "{$baseDir}/{$subGlob}";

            $rDirIterator = new RecursiveDirectoryIterator($baseDir);
            $itIterator = new RecursiveIteratorIterator($rDirIterator, RecursiveIteratorIterator::CHILD_FIRST);
            foreach ($itIterator as $path => $fileInfo) {
                if ($itIterator->getInnerIterator()->isDot())  continue;
                if ($itIterator->getInnerIterator()->isFile()) continue;

                $globsToProcess[] = "{$path}/{$subGlob}";
            }
        }
        else
        {
            $globsToProcess[] = $glob;
        }
    }

    $matches = array();
    $globsToProcess = array_unique($globsToProcess);
    foreach ($globsToProcess as $glob) {
        $matches = array_merge($matches, glob($glob));
    }

    $matches = array_unique($matches);
    return $matches;
}

