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

/*
 * Pirum
 *
 * (c) 2009-2012 Fabien Potencier
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

// defined as of PHP 5.4
if (!defined('JSON_PRETTY_PRINT')) {
    define('JSON_PRETTY_PRINT', 128);
}

// only run Pirum automatically when this file is called directly
// from the command line
if (isset($argv[0]) && __FILE__ == realpath($argv[0])) {
    $cli = new Pirum_CLI($argv);
    exit($cli->run());
}

/**
 * Command line interface for Pirum.
 *
 * @package    Pirum
 * @author     Fabien Potencier <fabien@symfony.com>
 */
class Pirum_CLI
{
    const VERSION = '1.1.5';

    protected $options;
    protected $formatter;
    protected $commands = array(
        'build',
        'add',
        'remove',
    );

    public function __construct(array $options)
    {
        $this->options = $options;
        $this->formatter = new Pirum_CLI_Formatter();
    }

    public function run()
    {
        echo $this->getUsage();

        if (!isset($this->options[1])) {
            return 0;
        }

        $command = $this->options[1];
        if (!$this->isCommand($command)) {
            echo $this->formatter->formatSection('ERROR', sprintf('"%s" is not a valid command.', $command));

            return 1;
        }

        echo $this->formatter->format(sprintf("Running the %s command:\n", $command), 'COMMENT');

        if (!isset($this->options[2]) || !is_dir($this->options[2])) {
            echo $this->formatter->formatSection('ERROR', "You must give the root directory of the PEAR channel server.");

            return 1;
        }

        $target = $this->options[2];

        $ret = 0;
        try {
            switch ($command) {
                case 'build':
                    $this->runBuild($target);
                    break;
                case 'add':
                    $ret = $this->runAdd($target);
                    break;
                case 'remove':
                    $ret = $this->runRemove($target);
                    break;
            }

            if (0 == $ret) {
                echo $this->formatter->formatSection('INFO', sprintf("Command %s run successfully.", $command));
            }
        } catch (Exception $e) {
            echo $this->formatter->formatSection('ERROR', sprintf("%s (%s, %s)", $e->getMessage(), get_class($e), $e->getCode()));

            return 1;
        }

        return $ret;
    }

    public static function removeDir($target)
    {
        $fp = opendir($target);
        while (false !== $file = readdir($fp)) {
            if (in_array($file, array('.', '..'))) {
                continue;
            }

            if (is_dir($target.'/'.$file)) {
                self::removeDir($target.'/'.$file);
            } else {
                unlink($target.'/'.$file);
            }
        }
        closedir($fp);
        rmdir($target);
    }

    public static function version()
    {
        if (strpos(self::VERSION, '@package_version') === 0) {
            return 'DEV';
        } else {
            return self::VERSION;
        }
    }

    protected function runRemove($target)
    {
        if (!isset($this->options[3])) {
            echo $this->formatter->formatSection('ERROR', "You must pass a PEAR package name.");

            return 1;
        }

        if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $this->options[3])) {
            echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" filename is badly formatted.', $this->options[3]));

            return 1;
        }

        if (!is_file($target.'/get/'.basename($this->options[3]))) {
            echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" does not exist in this channel.', $this->options[3]));

            return 1;
        }

        unlink($target.'/get/'.basename($this->options[3]));
        unlink($target.'/get/'.substr_replace(basename($this->options[3]), '.tar', -4));

        $this->runBuild($target);
    }

    protected function runAdd($target)
    {
        if (!isset($this->options[3])) {
            echo $this->formatter->formatSection('ERROR', "You must pass a PEAR package file path.");

            return 1;
        }

        if (!is_file($this->options[3])) {
            echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" does not exist.', $this->options[3]));

            return 1;
        }

        if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $this->options[3])) {
            echo $this->formatter->formatSection('ERROR', sprintf('The PEAR package "%s" filename is badly formatted.', $this->options[3]));

            return 1;
        }

        if (!is_dir($target.'/get')) {
            mkdir($target.'/get', 0777, true);
        }

        copy($this->options[3], $target.'/get/'.basename($this->options[3]));

        $this->runBuild($target);
    }

    protected function runBuild($target)
    {
        $builder = new Pirum_Builder($target, $this->formatter);
        $builder->build();
    }

    protected function isCommand($cmd) {
        return in_array($cmd, $this->commands);
    }

    protected function getUsage()
    {
        return $this->formatter->format(sprintf("Pirum %s by Fabien Potencier\n", self::version()), 'INFO') .
               $this->formatter->format("Available commands:\n", 'COMMENT') .
               "  pirum build target_dir\n" .
               "  pirum add target_dir Pirum-1.0.0.tgz\n" .
               "  pirum remove target_dir Pirum-1.0.0.tgz\n\n";
    }
}

/**
 * Builds all the files for a PEAR channel.
 *
 * @package    Pirum
 * @author     Fabien Potencier <fabien@symfony.com>
 */
class Pirum_Builder
{
    protected $buildDir;
    protected $targetDir;
    protected $server;
    protected $packages;
    protected $formatter;
    protected $templateConf;

    public function __construct($targetDir, $formatter = false)
    {
        if (!file_exists($targetDir.'/pirum.xml')) {
            throw new InvalidArgumentException('You must create a pirum.xml configuration file in the root of the target directory.');
        }

        if (!is_dir($targetDir.'/get')) {
            mkdir($targetDir.'/get', 0777, true);
        }

        $this->server = simplexml_load_file($targetDir.'/pirum.xml');

        if (!$this->server) {
            throw new InvalidArgumentException('Your pirum.xml configuration is invalid - <server> tag missing.');
        }

        $emptyFields = array();
        if (empty($this->server->name)) {
            $emptyFields[] = 'name';
        }
        if (empty($this->server->summary)) {
            $emptyFields[] = 'summary';
        }
        if (empty($this->server->url)) {
            $emptyFields[] = 'url';
        }

        if (!empty($emptyFields)) {
            throw new InvalidArgumentException(sprintf('You must fill required tags in your pirum.xml configuration file: %s.', implode(', ', $emptyFields)));
        }

        $this->server->url = rtrim($this->server->url, '/');

        if (DIRECTORY_SEPARATOR == '/') {
            $configFile = '.pirumrc';
        } else {
            $configFile = 'pirum.ini';
        }

        $defaultConf = array(
            'pirum' => array(
                'template' => '', 
                'templatedir' => dirname(__FILE__) . '/templates'
            )
        );
        if (file_exists(dirname(__FILE__) . "/$configFile")) {
            $customConf = parse_ini_file($configFile, true);
            if ($customConf === false) {
                // if config file could not be parsed; use default values
                $customConf = array();
            }
        } else {
            $customConf = array();
        }
        $this->templateConf = array_merge($defaultConf, $customConf);
        
        $this->formatter = $formatter;
        $this->targetDir = $targetDir;
        $this->buildDir  = sys_get_temp_dir().'/pirum_build_'.uniqid();
        mkdir($this->buildDir.'/rest', 0777, true);
    }

    public function __destruct()
    {
        Pirum_CLI::removeDir($this->buildDir);
    }

    public function build()
    {
        $this->extractInformationFromPackages();

        $this->fixArchives();
        $this->buildChannel();
        $this->buildMaintainers();
        $this->buildCategories();
        $this->buildPackages();
        $this->buildComposerRepository();
        $this->buildReleasePackages();
        $this->buildIndex();
        $this->buildCss();
        $this->buildFeed();

        $this->updateTargetDir();
    }

    protected function fixArchives()
    {
        // create tar files when missing
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->targetDir.'/get'), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
            if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $file->getFileName())) {
                continue;
            }

            $tar = preg_replace('/\.tgz/', '.tar', $file);
            if (!file_exists($tar)) {
                if (function_exists('gzopen')) {
                    $gz = gzopen($file, 'r');
                    if ($gz === false) {
                        throw new Exception(sprintf('Failed extracting archive "%s"!', $this->archive));
                    }
                    $fp = fopen(str_replace('.tgz', '.tar', $file), 'wb');
                    while (!gzeof($gz)) {
                        fwrite($fp, gzread($gz, 10000));
                    }
                    gzclose($gz);
                    fclose($fp);
                } else {
                    system('cd '.$this->targetDir.'/get/ && gunzip -c -f '.basename($file));
                }
            }
        }
    }

    protected function updateTargetDir()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Updating PEAR server files.");

        $this->updateComposerRepository();
        $this->updateChannel();
        $this->updateIndex();
        $this->updateCss();
        $this->updateFeed();
        $this->updatePackages();
    }

    protected function updateComposerRepository()
    {
        copy($this->buildDir.'/packages.json', $this->targetDir.'/packages.json');
    }

    protected function updateChannel()
    {
        if (!file_exists($this->targetDir.'/channel.xml') || file_get_contents($this->targetDir.'/channel.xml') != file_get_contents($this->buildDir.'/channel.xml')) {
            if (file_exists($this->targetDir.'/channel.xml')) {
                unlink($this->targetDir.'/channel.xml');
            }

            rename($this->buildDir.'/channel.xml', $this->targetDir.'/channel.xml');
        }
    }

    protected function updateIndex()
    {
        copy($this->buildDir.'/index.html', $this->targetDir.'/index.html');
    }

    protected function updateCss()
    {
        copy($this->buildDir.'/pirum.css', $this->targetDir.'/pirum.css');
    }

    protected function updateFeed()
    {
        copy($this->buildDir.'/feed.xml', $this->targetDir.'/feed.xml');
    }

    protected function updatePackages()
    {
        $this->mirrorDir($this->buildDir.'/rest', $this->targetDir.'/rest');
    }

    protected function buildFeed()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building feed.");

        $entries = '';
        foreach ($this->packages as $package) {
            foreach ($package['releases'] as $release) {
                $date = date(DATE_ATOM, strtotime($release['date']));

                reset($release['maintainers']);
                $maintainer = current($release['maintainers']);

                $entries .= <<<EOF
    <entry>
        <title>{$package['name']} {$release['version']} ({$release['stability']})</title>
        <link href="{$this->server->url}/get/{$package['name']}-{$release['version']}.tgz" />
        <id>{$this->server->url}/get/{$package['name']}-{$release['version']}.tgz</id>
        <author>
            <name>{$maintainer['nickname']}</name>
        </author>
        <updated>$date</updated>
        <content>
            {$package['description']}
            {$release['notes']}
        </content>
    </entry>
EOF;
            }
        }

        $date = date(DATE_ATOM);
        $index = <<<EOF
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>{$this->server->url}</id>
    <updated>$date</updated>
    <title>{$this->server->summary} Latest Releases</title>
    <link href="{$this->server->url}/feed.xml" rel="self" />
    <author>
        <name>{$this->server->url}</name>
    </author>

$entries
</feed>
EOF;

        file_put_contents($this->buildDir.'/feed.xml', $index);
    }

    protected function getTemplateFile($name)
    {
        if (array_key_exists($this->targetDir, $this->templateConf)) {
            // check first if a specific server template is defined 
            $templateConf = $this->templateConf[$this->targetDir];
            if (isset($templateConf['templatedir'])) {
                $dir = $templateConf['templatedir'];
            } else {
                // fallback to default template location
                $dir = $this->templateConf['pirum']['templatedir'];
            }
            $file = $dir;
            if (isset($templateConf['template']) 
                && !empty($templateConf['template'])
            ) {
                $file .= '/' . $templateConf['template'];
            }
        } else {
            // use default template
            $file = $this->templateConf['pirum']['templatedir'];
            if (!empty($this->templateConf['pirum']['template'])) {
                $file .= '/' . $this->templateConf['pirum']['template'];  
            }
        }
        $file .= "/$name";
        
        return $file;
    }
    
    protected function buildCss()
    {
        if (file_exists($file = $this->getTemplateFile('pirum.css')) ||
            file_exists($file = $this->buildDir.'/templates/pirum.css')) {
            $content = file_get_contents($file);
        } else {
            // fallback in case of external template is not readable 
            $content = <<<EOF
/*
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 2.8.0r4
*/
html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea,button{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}body{text-align:center;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main,.yui-g .yui-u .yui-g{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-g .yui-u{width:48.1%;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}.yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#hd:after,#bd:after,#ft:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#hd,#bd,#ft,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;}

/* Pirum stylesheet */
em { font-style: italic }
strong { font-weight: bold }
small { font-size: 80% }
h1, h2, h3 { font-family:Georgia,Times New Roman,serif; letter-spacing: -0.03em; }
h1 { font-size: 35px; margin-top: 20px; margin-bottom: 30px }
h2 { font-size: 30px; margin-bottom: 20px; margin-top: 15px }
h3 { font-size: 26px; margin-bottom: 10px; margin-top: 20px }
pre { background-color: #000; color: #fff; margin: 5px 0; overflow: auto; padding: 10px; font-family: monospace }
#ft { margin-top: 10px }
ul { margin-top: 5px }
li strong { color: #666 }
dd { margin-left: 20px }
p { margin-bottom: 10px }
table { width: 100% }
table th { width: 20% }
table td, table th { vertical-align: top; padding: 4px; border: 1px solid #ccc }
th { background-color: #eee }

EOF;
        }

        file_put_contents($this->buildDir.'/pirum.css', $content);
    }

    protected function buildIndex()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building index.");

        $version = Pirum_CLI::version();
        
        if (file_exists($file = $this->getTemplateFile('index.html')) ||
            file_exists($file = $this->buildDir.'/templates/index.html')) {
            ob_start();
            include $file;
            $html = ob_get_clean();

            file_put_contents($this->buildDir.'/index.html', $html);

            return;
        }
        // fallback in case of external template is not readable 
        ob_start();
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title><?php echo $this->server->summary ?></title>
    <link rel="stylesheet" type="text/css" href="pirum.css" />
    <link rel="alternate" type="application/rss+xml" href="<?php echo $this->server->url ?>/feed.xml" title="<?php echo  $this->server->summary ?> Latest Releases" />
</head>
<body>
    <div id="doc" class="yui-t7">
        <div id="hd">
            <h1><?php echo $this->server->summary ?></h1>
        </div>
        <div id="bd">
            <div class="yui-g">
                <em>Registering</em> the channel:
                <pre><code>pear channel-discover <?php echo $this->server->name ?></code></pre>
                <em>Listing</em> available packages:
                <pre><code>pear remote-list -c <?php echo $this->server->alias ?></code></pre>
                <em>Installing</em> a package:
                <pre><code>pear install <?php echo $this->server->alias ?>/package_name</code></pre>
                <em>Installing</em> a specific version/stability:
                <pre><code>pear install <?php echo $this->server->alias ?>/package_name-1.0.0
pear install <?php echo $this->server->alias ?>/package_name-beta</code></pre>
                <em>Receiving</em> updates via a <a href="<?php echo $this->server->url ?>/feed.xml">feed</a>:
                <pre><code><?php echo $this->server->url ?>/feed.xml</code></pre>

                <?php foreach ($this->packages as $package):
                    $deps = array();
                    if (isset($package['deps']['required']['package'])) {
                        if (isset($package['deps']['required']['package']['name'])) {
                            $package['deps']['required']['package'] = array($package['deps']['required']['package']);
                        }
                        foreach ($package['deps']['required']['package'] as $p) {
                            if (isset($p['conflicts'])) {
                                continue;
                            }
                            if (!isset($deps[$p['channel']])) {
                                $deps[$p['channel']] = array();
                            }
                            $deps[$p['channel']][] = $p;
                        }
                    }

                    $grps = array();
                    if (isset($package['deps']['group'])) {
                        $grps = $package['deps']['group'];
                    }
                ?>
                    <h3 id="<?php echo strtolower($package['name']) ?>"><?php echo $package['name'] ?><small> - <?php echo $package['summary'] ?></small></h3>
                    <p><?php echo $package['description'] ?></p>
                    <table>
                        <tr><th>Install command</th><td><strong><?php echo $package['extension'] != null ? 'pecl' : 'pear' ?> install <?php echo $this->server->alias ?>/<?php echo $package['name'] ?></strong></td></tr>
                        <?php if ($grps): ?><?php
                            $groups = array();
                            foreach ($grps as $grp) {
                                $groups[] = ($package['extension'] != null ? 'pecl' : 'pear').' install '.$this->server->alias.'/'.$package['name'].'#'.$grp['attribs']['name'].'&nbsp;<small>('.$grp['attribs']['hint'].')</small>';
                            }
                            $groups = implode('<br/>', $groups);
                        ?>
                        <tr><th>Install groups:</th><td><?php echo $groups ?></td></tr>
                        <?php endif; ?>
                        <tr><th>License</th><td><?php echo (empty($package['licenseuri']) ? $package['license'] : '<a href="' . $package['licenseuri'] . '">' . $package['license'] . '</a>') ?></td></tr>
                        <?php if ($deps): ?>
                            <tr><th>Dependencies</th><td>
                                <dl>
                                <?php foreach ($deps as $ch => $ch_deps): ?>
                                <dt><?php echo $ch ?></dt>
                                <dd>
                                <?php for ($i = 0, $count = count($ch_deps); $i < $count; $i++): ?>
                                    <?php echo $ch_deps[$i]['name'].($i < $count - 1 ? ', ' : '') ?>
                                <?php endfor; ?>
                                <dd>
                                <?php endforeach; ?>
                                </dl>
                            </td></tr>
                        <?php endif; ?>
                        <?php
                            $maintainers = array();
                            foreach ($package['current_maintainers'] as $maintainer) {
                                $maintainers[] = $maintainer['name'].' <small>(as '.$maintainer['role'].')</small>';
                            }
                            $maintainers = implode(', ', $maintainers);
                        ?>
                        <tr><th>Maintainers</th><td><?php echo $maintainers ?></td></tr>
                        <?php
                        $releases = array();
                        foreach ($package['releases'] as $release) {
                            $releases[] = "<a href=\"{$this->server->url}/get/{$package['name']}-{$release['version']}.tgz\">{$release['version']}</a> <small>({$release['stability']})</small>";
                        }
                        $releases = implode(', ', $releases);
                        ?>
                        <tr><th>Releases</th><td><?php echo $releases ?></td></tr>
                    </table>
                <?php endforeach; ?>
            </div>
        </div>
        <div id="ft">
            <p><small>The <em><?php echo $this->server->name ?></em> PEAR Channel Server is proudly powered by <a href="http://pirum.sensiolabs.org/">Pirum</a> <?php echo $version ?></small></p>
        </div>
    </div>
</body>
</html>
<?php
        $index = ob_get_clean();

        file_put_contents($this->buildDir.'/index.html', $index);
    }

    protected function buildComposerRepository()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building composer repository.");

        $repository = array();

        $vendor = !empty($this->server->alias) ? $this->server->alias : $this->server->name;

        foreach ($this->packages as $package) {
            // standard package fields
            $packageDef = array('versions' => array());
            $packageDef['name']        = 'pear-'.$vendor.'/'.$package['name'];
            $packageDef['description'] = !empty($package['summary']) ? $package['summary'] : $package['description'];

            // package maintainers
            foreach ($package['current_maintainers'] as $maintainer) {
                $packageDef['maintainers'][] = array(
                    'name'     => $maintainer['name'],
                    'email'    => $maintainer['email'],
                    'homepage' => $maintainer['url'],
                );
            }
            $repository[$packageDef['name']] = $packageDef;

            $pearChannels = array();
            foreach ($package['releases'] as $release) {
                // standard version fields
                $version = array(
                    'name'        => $packageDef['name'],
                    'description' => $packageDef['description'],
                    'dist'        => array(
                        'type' => 'pear',
                        'url'  => $this->server->url.'/get/'.$package['name'].'-'.$release['version'].'.tgz',
                    ),
                );
                if (isset($release['license'])) {
                    $version['license'] = $release['license'];
                }
                if (isset($release['version'])) {
                    $version['version'] = $release['version'];
                }
                if (isset($release['date'])) {
                    $version['time'] = date(DATE_ATOM, strtotime($release['date']));
                }

                // version requirements
                if (isset($release['php'])) {
                    $version['require']['php'] = '>='.$release['php'];
                }

                if (isset($release['deps']) && $deps = @unserialize($release['deps'])) {
                    $depsTypes = array(
                        'required' => 'require',
                        'optional' => 'suggest',
                    );
                    foreach ($depsTypes as $pearType => $composerType) {
                        $requirements = isset($deps[$pearType]) ? $deps[$pearType] : array();
                        foreach ($requirements as $reqType => $spec) {
                            // wrap single requirements as an array of requirements
                            if (isset($spec['name'])) {
                                $spec = array($spec);
                            }

                            if ('php' === $reqType) {
                                $version[$composerType]['php'] = $this->createComposerConstraint($spec);
                            } elseif ('package' === $reqType) {
                                foreach ($spec as $key => $value) {
                                    if (isset($value['providesextension'])) {
                                        // skip PECL dependencies
                                        continue;
                                    }
                                    if (isset($value['uri'])) {
                                        // skip uri-based dependencies
                                        continue;
                                    }

                                    if (is_array($value)) {
                                        $dataKey = $value['name'];
                                        if (false === strpos($dataKey, '/')) {
                                            $channelUrl = 'http://'.$value['channel'];
                                            if (!isset($pearChannels[$channelUrl])) {
                                                try {
                                                    $channelInfo = @new SimpleXMLElement($channelUrl.'/channel.xml', null, true);
                                                } catch (Exception $e) {
                                                    throw new Exception(sprintf('Channel "%s" could not be reached for package %s version %s.', $channelUrl, $package['name'], $version['version']));
                                                }
                                                if ($tmp = (string) $channelInfo->suggestedalias[0]) {
                                                    $pearChannels[$channelUrl] = $tmp;
                                                } else {
                                                    $pearChannels[$channelUrl] = (string) $channelInfo->name[0];
                                                }
                                            }
                                            $dataKey = $pearChannels[$channelUrl].'/'.$dataKey;
                                        }
                                        $version[$composerType]['pear-'.$dataKey] = $this->createComposerConstraint($value);
                                    }
                                }
                            } elseif ('extension' == $reqType) {
                                foreach ($spec as $key => $value) {
                                    $dataKey = 'ext-' . $value['name'];
                                    $version[$composerType][$dataKey] = $this->createComposerConstraint($value);
                                }
                            }
                        }
                    }
                }

                // version authors
                foreach ($package['maintainers'] as $maintainer) {
                    $version['authors'][] = array(
                        'name'     => $maintainer['name'],
                        'email'    => $maintainer['email'],
                        'homepage' => $maintainer['url'],
                    );
                }

                // autoload
                $version['autoload'] = $this->guessComposerAutoload($release['info']->getPackageXml());

                $repository[$packageDef['name']]['versions'][$version['version']] = $version;
            }
        }

        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
            file_put_contents($this->buildDir.'/packages.json',json_encode($repository, JSON_PRETTY_PRINT));
        } else {
            file_put_contents($this->buildDir.'/packages.json',json_encode($repository));
        }
    }

    protected function createComposerConstraint(array $data)
    {
        if (!isset($data['min']) && !isset($data['max'])) {
            return '*';
        }
        $versions = array();
        if (isset($data['min'])) {
            $versions[] = '>=' . $data['min'];
        }
        if (isset($data['max'])) {
            $versions[] = '<=' . $data['max'];
        }
        return implode(',', $versions);
    }

    protected function guessComposerAutoload(SimpleXMLElement $packageInfo)
    {
        $files = array();
        if (count($packageInfo->contents->dir)) {
            foreach ($packageInfo->contents->dir as $dir) {
                if (count($dir->file)) {
                    foreach ($dir->file as $file) {
                        if ('php' !== (string) $file['role']) {
                            continue;
                        }

                        $files[] = ltrim($dir['name'].$file['name'], '/');
                    }
                }
            }
        }

        // reduce to meaningful unique paths
        while (true) {
            foreach ($files as $key => $file) {
                foreach ($files as $key2 => $file2) {
                    if ($key === $key2) {
                        continue;
                    }
                    if (false !== strpos($file, '/') && dirname($file) === dirname($file2)) {
                        unset($files[$key2]);
                        $files[$key] = dirname($file);
                        continue 3;
                    }
                    if (0 === strpos($file2, $file.'/') || $file === $file2) {
                        unset($files[$key2]);
                        continue 3;
                    }
                }
            }
            break;
        }

        return array(
            'classmap' => $files ? $files : array(''),
        );
    }

    protected function buildReleasePackages()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building releases.");

        mkdir($this->buildDir.'/rest/r', 0777, true);

        foreach ($this->packages as $package) {
            mkdir($dir = $this->buildDir.'/rest/r/'.strtolower($package['name']), 0777, true);

            $this->buildReleasePackage($dir, $package);
        }
    }

    protected function buildReleasePackage($dir, $package)
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building releases for {$package['name']}.");

        $url = strtolower($package['name']);

        $alpha = '';
        $beta = '';
        $stable = '';
        $snapshot = '';
        $allreleases = '';
        $allreleases2 = '';
        $first = true;
        $this->formatter and print $this->formatter->formatSection('INFO', 'Building release', false);
        foreach ($package['releases'] as $release) {
            if ('stable' == $release['stability'] && !$stable) {
                $stable = $release['version'];
            } elseif ('beta' == $release['stability'] && !$beta) {
                $beta = $release['version'];
            } elseif ('alpha' == $release['stability'] && !$alpha) {
                $alpha = $release['version'];
            } elseif ('snapshot' == $release['stability'] && !$snapshot) {
                $snapshot = $release['version'];
            }

            $allreleases .= <<<EOF
    <r>
        <v>{$release['version']}</v>
        <s>{$release['stability']}</s>
    </r>

EOF;

            $allreleases2 .= <<<EOF
    <r>
        <v>{$release['version']}</v>
        <s>{$release['stability']}</s>
        <m>{$release['php']}</m>
    </r>

EOF;

            $this->buildRelease($dir, $package, $release, $first);
            $first = false;
        }
        $this->formatter and print "\n";

        if (count($package['releases'])) {
            file_put_contents($dir.'/latest.txt', $package['releases'][0]['version']);
        }

        if ($stable) {
            file_put_contents($dir.'/stable.txt', $stable);
        }

        if ($beta) {
            file_put_contents($dir.'/beta.txt', $beta);
        }

        if ($alpha) {
            file_put_contents($dir.'/alpha.txt', $alpha);
        }

        if ($snapshot) {
            file_put_contents($dir.'/snapshot.txt', $snapshot);
        }

        file_put_contents($dir.'/allreleases.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<a xmlns="http://pear.php.net/dtd/rest.allreleases" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink"     xsi:schemaLocation="http://pear.php.net/dtd/rest.allreleases http://pear.php.net/dtd/rest.allreleases.xsd">
    <p>{$package['name']}</p>
    <c>{$this->server->name}</c>
$allreleases
</a>
EOF
        );

        file_put_contents($dir.'/allreleases2.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<a xmlns="http://pear.php.net/dtd/rest.allreleases2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink"     xsi:schemaLocation="http://pear.php.net/dtd/rest.allreleases2 http://pear.php.net/dtd/rest.allreleases2.xsd">
    <p>{$package['name']}</p>
    <c>{$this->server->name}</c>
$allreleases2
</a>
EOF
        );
    }

    protected function buildRelease($dir, $package, $release, $first)
    {
        !$first and $this->formatter and print $this->formatter->format(',');
        $this->formatter and print $this->formatter->format(' ' . $release['version']);

        $url = strtolower($package['name']);

        reset($release['maintainers']);
        $maintainer = current($release['maintainers']);

        file_put_contents($dir.'/'.$release['version'].'.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<r xmlns="http://pear.php.net/dtd/rest.release" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.release http://pear.php.net/dtd/rest.release.xsd">
    <p xlink:href="/rest/p/$url">{$package['name']}</p>
    <c>{$this->server->name}</c>
    <v>{$release['version']}</v>
    <st>{$release['stability']}</st>
    <l>{$package['license']}</l>
    <m>{$maintainer['nickname']}</m>
    <s>{$package['summary']}</s>
    <d>{$package['description']}</d>
    <da>{$release['date']}</da>
    <n>{$release['notes']}</n>
    <f>{$release['filesize']}</f>
    <g>{$this->server->url}/get/{$package['name']}-{$release['version']}</g>
    <x xlink:href="package.{$release['version']}.xml"/>
</r>
EOF
        );

        file_put_contents($dir.'/v2.'.$release['version'].'.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<r xmlns="http://pear.php.net/dtd/rest.release2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.release2 http://pear.php.net/dtd/rest.release2.xsd">
    <p xlink:href="/rest/p/$url">{$package['name']}</p>
    <c>{$this->server->name}</c>
    <v>{$release['version']}</v>
    <a>{$release['api_version']}</a>
    <mp>{$release['php']}</mp>
    <st>{$release['stability']}</st>
    <l>{$package['license']}</l>
    <m>{$maintainer['nickname']}</m>
    <s>{$package['summary']}</s>
    <d>{$package['description']}</d>
    <da>{$release['date']}</da>
    <n>{$release['notes']}</n>
    <f>{$release['filesize']}</f>
    <g>{$this->server->url}/get/{$package['name']}-{$release['version']}</g>
    <x xlink:href="package.{$release['version']}.xml"/>
</r>
EOF
        );

        file_put_contents($dir.'/deps.'.$release['version'].'.txt', $release['deps']);

        $release['info']->copyPackageXml($dir."/package.{$release['version']}.xml");
    }

    protected function buildPackages()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building packages.");

        mkdir($this->buildDir.'/rest/p', 0777, true);

        $packages = '';
        foreach ($this->packages as $package) {
            $packages .= "  <p>{$package['name']}</p>\n";

            mkdir($dir = $this->buildDir.'/rest/p/'.strtolower($package['name']), 0777, true);
            $this->buildPackage($dir, $package);
        }

        file_put_contents($this->buildDir.'/rest/p/packages.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<a xmlns="http://pear.php.net/dtd/rest.allpackages" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allpackages http://pear.php.net/dtd/rest.allpackages.xsd">
    <c>{$this->server->name}</c>
$packages
</a>
EOF
        );
    }

    protected function buildPackage($dir, $package)
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building package {$package['name']}.");

        $url = strtolower($package['name']);

        file_put_contents($dir.'/info.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<p xmlns="http://pear.php.net/dtd/rest.package" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.package    http://pear.php.net/dtd/rest.package.xsd">
<n>{$package['name']}</n>
<c>{$this->server->name}</c>
<ca xlink:href="/rest/c/Default">Default</ca>
<l>{$package['license']}</l>
<s>{$package['summary']}</s>
<d>{$package['description']}</d>
<r xlink:href="/rest/r/{$url}" />
</p>
EOF
        );

        $maintainers = '';
        $maintainers2 = '';
        foreach ($package['current_maintainers'] as $nickname => $maintainer) {
            $maintainers .= <<<EOF
    <m>
        <h>{$nickname}</h>
        <a>{$maintainer['active']}</a>
    </m>

EOF;

            $maintainers2 .= <<<EOF
    <m>
        <h>{$nickname}</h>
        <a>{$maintainer['active']}</a>
        <r>{$maintainer['role']}</r>
    </m>

EOF;
        }

        file_put_contents($dir.'/maintainers.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<m xmlns="http://pear.php.net/dtd/rest.packagemaintainers" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.packagemaintainers http://pear.php.net/dtd/rest.packagemaintainers.xsd">
    <p>{$package['name']}</p>
    <c>{$this->server->name}</c>
$maintainers
</m>
EOF
        );

        file_put_contents($dir.'/maintainers2.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<m xmlns="http://pear.php.net/dtd/rest.packagemaintainers2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.packagemaintainers2 http://pear.php.net/dtd/rest.packagemaintainers2.xsd">
    <p>{$package['name']}</p>
    <c>{$this->server->name}</c>
$maintainers2
</m>
EOF
        );
    }

    protected function buildCategories()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building categories.");

        mkdir($this->buildDir.'/rest/c/Default', 0777, true);

        file_put_contents($this->buildDir.'/rest/c/categories.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<a xmlns="http://pear.php.net/dtd/rest.allcategories" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allcategories http://pear.php.net/dtd/rest.allcategories.xsd">
    <ch>{$this->server->name}</ch>
    <c xlink:href="/rest/c/Default/info.xml">Default</c>
</a>
EOF
        );

        file_put_contents($this->buildDir.'/rest/c/Default/info.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<c xmlns="http://pear.php.net/dtd/rest.category" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.category http://pear.php.net/dtd/rest.category.xsd">
    <n>Default</n>
    <c>{$this->server->name}</c>
    <a>Default</a>
    <d>Default category</d>
</c>
EOF
        );

        $packages = '';
        $packagesinfo = '';
        foreach ($this->packages as $package) {
            $url = strtolower($package['name']);

            $packages .= "  <p xlink:href=\"/rest/p/$url\">{$package['name']}</p>\n";

            $deps = '';
            $releases = '';
            foreach ($package['releases'] as $release) {
                $releases .= <<<EOF
            <r>
                <v>{$release['version']}</v>
                <s>{$release['stability']}</s>
            </r>

EOF;

                $deps .= <<<EOF
        <deps>
            <v>{$release['version']}</v>
            <d>{$release['deps']}</d>
        </deps>

EOF;
            }

            $packagesinfo .= <<<EOF
    <pi>
        <p>
            <n>{$package['name']}</n>
            <c>{$this->server->name}</c>
            <ca xlink:href="/rest/c/Default">Default</ca>
            <l>{$package['license']}</l>
            <s>{$package['summary']}</s>
            <d>{$package['description']}</d>
            <r xlink:href="/rest/r/$url" />
        </p>

        <a>
$releases
        </a>

$deps
    </pi>
EOF;
        }

        file_put_contents($this->buildDir.'/rest/c/Default/packages.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<l xmlns="http://pear.php.net/dtd/rest.categorypackages" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.categorypackages http://pear.php.net/dtd/rest.categorypackages.xsd">
$packages
</l>
EOF
        );

        file_put_contents($this->buildDir.'/rest/c/Default/packagesinfo.xml', <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<f xmlns="http://pear.php.net/dtd/rest.categorypackageinfo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.categorypackageinfo     http://pear.php.net/dtd/rest.categorypackageinfo.xsd">
$packagesinfo
</f>
EOF
        );
    }

    protected function buildMaintainers()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building maintainers.");

        mkdir($dir = $this->buildDir.'/rest/m/', 0777, true);

        $all = '';
        foreach ($this->packages as $package) {
            foreach ($package['maintainers'] as $nickname => $maintainer) {
                $dir = $this->buildDir.'/rest/m/'.$nickname;

                $info = <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<m xmlns="http://pear.php.net/dtd/rest.maintainer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.maintainer http://pear.php.net/dtd/rest.maintainer.xsd">
 <h>{$nickname}</h>
 <n>{$maintainer['name']}</n>
 <u>{$maintainer['url']}</u>
</m>
EOF;

                if (!is_dir($dir)) {
                    mkdir($dir, 0777, true);
                    $all .= "  <h xlink:href=\"/rest/m/{$nickname}\">{$nickname}</h>\n";
                }

                file_put_contents($dir.'/info.xml', $info);
            }
        }

        $all = <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<m xmlns="http://pear.php.net/dtd/rest.allmaintainers" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.allmaintainers http://pear.php.net/dtd/rest.allmaintainers.xsd">
$all
</m>
EOF;

        file_put_contents($this->buildDir.'/rest/m/allmaintainers.xml', $all);
    }

    protected function buildChannel()
    {
        $this->formatter and print $this->formatter->formatSection('INFO', "Building channel.");

        $suggestedalias = '';
        if (!empty($this->server->alias)) {
            $suggestedalias = '
    <suggestedalias>'.$this->server->alias.'</suggestedalias>';
        }

        $validator = '';
        if (!empty($this->server->validatepackage) && !empty($this->server->validateversion)) {
            $validator = '
    <validatepackage version="'.$this->server->validateversion.'">'.$this->server->validatepackage.'</validatepackage>';
        }

        $content = <<<EOF
<?xml version="1.0" encoding="UTF-8" ?>
<channel version="1.0" xmlns="http://pear.php.net/channel-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/channel-1.0 http://pear.php.net/dtd/channel-1.0.xsd">
    <name>{$this->server->name}</name>
    <summary>{$this->server->summary}</summary>{$suggestedalias}
    <servers>
        <primary>
            <rest>
                <baseurl type="REST1.0">{$this->server->url}/rest/</baseurl>
                <baseurl type="REST1.1">{$this->server->url}/rest/</baseurl>
                <baseurl type="REST1.2">{$this->server->url}/rest/</baseurl>
                <baseurl type="REST1.3">{$this->server->url}/rest/</baseurl>
            </rest>
        </primary>

EOF;

        foreach ($this->server->mirror as $mirror) {
            $mirror = rtrim($mirror, '/');

            $ssl = '';
            if ('https' === substr($mirror, 0, 5)) {
                $ssl = ' ssl="yes"';
            }

            $content .= <<<EOF
        <mirror host="{$mirror}"{$ssl}>
            <rest>
                <baseurl type="REST1.0">{$mirror}/rest/</baseurl>
                <baseurl type="REST1.1">{$mirror}/rest/</baseurl>
                <baseurl type="REST1.2">{$mirror}/rest/</baseurl>
                <baseurl type="REST1.3">{$mirror}/rest/</baseurl>
            </rest>
        </mirror>

EOF;
        }

        $content .= <<<EOF
    </servers>{$validator}
</channel>
EOF;

        file_put_contents($this->buildDir.'/channel.xml', $content);
    }

    protected function extractInformationFromPackages()
    {
        $this->packages = array();

        // get all package files
        $files = array();
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->targetDir.'/get'), RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
            if (!preg_match(Pirum_Package::PACKAGE_FILE_PATTERN, $file->getFileName(), $match)) {
                continue;
            }

            $files[$match['release']] = (string) $file;
        }

        // order files to have latest versions first
        uksort($files, 'version_compare');
        $files = array_reverse($files);

        // get information for each package
        $packages = array();
        foreach ($files as $file) {
            $package = new Pirum_Package($file);
            if (file_exists($file = $this->targetDir.'/rest/r/'.strtolower($package->getName()).'/package.'.$package->getVersion().'.xml')) {
                $package->loadPackageFromFile($file);
            } else {
                $package->loadPackageFromArchive();
            }

            $packages[$file] = $package;
        }

        $first = true;
        $this->formatter and print $this->formatter->formatSection('INFO', 'Parsing package', false);
        foreach ($packages as $file => $package) {
            !$first and $this->formatter and print $this->formatter->format(',');
            $this->formatter and print $this->formatter->format(' ' . $package->getName() . ' ' . $package->getVersion());

            if ($package->getChannel() != $this->server->name) {
                throw new Exception(sprintf('Package "%s" channel (%s) is not %s.', $package->getName(), $package->getChannel(), $this->server->name));
            }

            if (!isset($this->packages[$package->getName()])) {
                $this->packages[$package->getName()] = array(
                    'name'        => htmlspecialchars($package->getName()),
                    'license'     => htmlspecialchars($package->getLicense()),
                    'licenseuri'  => htmlspecialchars($package->getLicenseUri()),
                    'summary'     => htmlspecialchars($package->getSummary()),
                    'description' => htmlspecialchars($package->getDescription()),
                    'extension'   => $package->getProvidedExtension(),
                    'releases'    => array(),
                    'maintainers' => array(),
                    'deps'        => unserialize($package->getDeps()),
                    'current_maintainers' => $package->getMaintainers(),
                );
            }

            $this->packages[$package->getName()]['releases'][] = array(
                'version'     => $package->getVersion(),
                'api_version' => $package->getApiVersion(),
                'stability'   => $package->getStability(),
                'date'        => $package->getDate(),
                'filesize'    => $package->getFilesize(),
                'php'         => $package->getMinPhp(),
                'deps'        => $package->getDeps(),
                'notes'       => htmlspecialchars($package->getNotes()),
                'maintainers' => $package->getMaintainers(),
                'info'        => $package,
            );

            $this->packages[$package->getName()]['maintainers'] = array_merge($package->getMaintainers(), $this->packages[$package->getName()]['maintainers']);

            $first = false;
        }
        $this->formatter and print "\n";

        ksort($this->packages);
    }

    protected function mirrorDir($build, $target)
    {
        if (!is_dir($target)) {
            mkdir($target, 0777, true);
        }

        $this->removeFilesFromDir($target, $build);

        $this->copyFiles($build, $target);
    }

    protected function copyFiles($build, $target)
    {
        $fp = opendir($build);
        while (false !== $file = readdir($fp)) {
            if (in_array($file, array('.', '..'))) {
                continue;
            }

            if (is_dir($build.'/'.$file)) {
                if (!is_dir($target.'/'.$file)) {
                    mkdir($target.'/'.$file, 0777, true);
                }

                $this->copyFiles($build.'/'.$file, $target.'/'.$file);
            } else {
                rename($build.'/'.$file, $target.'/'.$file);
            }
        }
        closedir($fp);
    }

    protected function removeFilesFromDir($target, $build)
    {
        $fp = opendir($target);
        while (false !== $file = readdir($fp)) {
            if (in_array($file, array('.', '..'))) {
                continue;
            }

            if (is_dir($target.'/'.$file)) {
                if (!in_array($file, array('.svn', 'CVS'))) {
                    $this->removeFilesFromDir($target.'/'.$file, $build.'/'.$file);
                    if (!is_dir($build.'/'.$file)) {
                        rmdir($target.'/'.$file);
                    }
                }
            } else {
                unlink($target.'/'.$file);
            }
        }
        closedir($fp);
    }
}

/**
 * Parses a PEAR package and retrieves useful information from it.
 *
 * @package    Pirum
 * @author     Fabien Potencier <fabien@symfony.com>
 */
class Pirum_Package
{
    const PACKAGE_FILE_PATTERN = '#^(?P<release>(?P<name>.+)\-(?P<version>[\d\.]+((?:RC|beta|alpha|dev|snapshot|a|b)\d*)?))\.tgz$#i';

    protected $package;
    protected $tmpDir;
    protected $name;
    protected $version;
    protected $archive;
    protected $packageFile;

    public function __construct($archive)
    {
        $this->archive = $archive;
        if (!preg_match(self::PACKAGE_FILE_PATTERN, $filename = basename($archive), $match)) {
            throw new InvalidArgumentException(sprintf('The archive "%s" does not follow PEAR conventions.', $filename));
        }

        $this->name    = $match['name'];
        $this->version = $match['version'];

        $this->tmpDir = sys_get_temp_dir().'/pirum_package_'.uniqid();

        mkdir($this->tmpDir, 0777, true);
    }

    public function __destruct()
    {
        Pirum_CLI::removeDir($this->tmpDir);
    }

    public function getPackageXml()
    {
        return $this->package;
    }

    public function getDate($format = 'Y-m-d H:i:s')
    {
        return date($format, strtotime($this->package->date.' '.$this->package->time));
    }

    public function getLicense()
    {
        return (string) $this->package->license;
    }

    public function getLicenseUri()
    {
        return (string) $this->package->license['uri'];
    }

    public function getDescription()
    {
        return (string) $this->package->description;
    }

    public function getSummary()
    {
        return (string) $this->package->summary;
    }

    public function getChannel()
    {
        return (string) $this->package->channel;
    }

    public function getNotes()
    {
        return (string) $this->package->notes;
    }

    public function getFileSize()
    {
        return filesize($this->archive);
    }

    public function getApiVersion()
    {
        return (string) $this->package->version->api;
    }

    public function getApiStability()
    {
        return (string) $this->package->stability->api;
    }

    public function getStability()
    {
        return (string) $this->package->stability->release;
    }

    public function getMaintainers()
    {
        $maintainers = array();

        foreach (array('lead', 'developer', 'contributor', 'helper') as $role) {
            foreach ($this->package->{$role} as $person) {
                $maintainers[(string) $person->user] = array(
                    'nickname' => (string) $person->user,
                    'role'     => $role,
                    'email'    => (string) $person->email,
                    'name'     => (string) $person->name,
                    'url'      => (string) $person->url,
                    'active'   => strtolower((string) $person->active) == 'yes' ? 1 : 0,
                );
            }
        }

        return $maintainers;
    }

    public function getDeps()
    {
        $deps = $this->XMLToArray($this->package->dependencies);
        if (isset($this->package->dependencies->group) && $this->package->dependencies->group->length > 0) {
            $deps['group'] = $this->XMLGroupDepsToArray($this->package->dependencies->group);
        }

        return serialize($deps);
    }

    public function getMinPhp()
    {
        return isset($this->package->dependencies->required->php->min) ? (string) $this->package->dependencies->required->php->min : null;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getVersion()
    {
        return $this->version;
    }

    public function getProvidedExtension() {
        return isset($this->package->providesextension) ? (string)$this->package->providesextension : null;
    }

    public function copyPackageXml($target)
    {
        copy($this->packageFile, $target);
    }

    public function loadPackageFromFile($file)
    {
        $this->packageFile = $file;

        $this->package = new SimpleXMLElement(file_get_contents($file));

        // check name
        if ($this->name != (string) $this->package->name) {
            throw new InvalidArgumentException(sprintf('The package.xml name "%s" does not match the name of the archive file "%s".', $this->package->name, $this->name));
        }

        // check version
        if ($this->version != (string) $this->package->version->release) {
            throw new InvalidArgumentException(sprintf('The package.xml version "%s" does not match the version of the archive file "%s".', $this->package->version->release, $this->version));
        }
    }

    public function loadPackageFromArchive()
    {
        if (!function_exists('gzopen')) {
            copy($this->archive, $this->tmpDir.'/archive.tgz');
            system('cd '.$this->tmpDir.' && tar zxpf archive.tgz');

            if (!is_file($this->tmpDir.'/package.xml')) {
                throw new InvalidArgumentException('The PEAR package does not have a package.xml file.');
            }

            $this->loadPackageFromFile($this->tmpDir.'/package.xml');

            return;
        }

        $gz = gzopen($this->archive, 'r');
        if ($gz === false) {
            throw new Exception(sprintf('Failed extracting archive "%s"!', $this->archive));
        }
        $tar = '';
        while (!gzeof($gz)) {
            $tar .= gzread($gz, 10000);
        }
        gzclose($gz);

        while (strlen($tar)) {
            $filename = rtrim(substr($tar, 0, 100), chr(0));
            $filesize = octdec(rtrim(substr($tar, 124, 12), chr(0)));

            if ($filename != 'package.xml') {
                $offset = $filesize % 512 == 0 ? $filesize : $filesize + (512 - $filesize % 512);
                $tar = substr($tar, 512 + $offset);

                continue;
            }

            $checksum = octdec(rtrim(substr($tar, 148, 8), chr(0)));
            $cchecksum = 0;
            $tar = substr_replace($tar, '        ', 148, 8);
            for ($i = 0; $i < 512; $i++) {
                $cchecksum += ord($tar[$i]);
            }

            if ($checksum != $cchecksum) {
                throw new InvalidArgumentException('The PEAR archive is not a valid archive.');
            }

            $package = substr($tar, 512, $filesize);
            $this->packageFile = $this->tmpDir.'/package.xml';

            file_put_contents($this->packageFile, $package);

            $this->loadPackageFromFile($this->tmpDir.'/package.xml');

            return;
        }

        throw new InvalidArgumentException('The PEAR package does not have a package.xml file.');
    }

    protected function XMLToArray($xml)
    {
        $array = array();
        foreach ($xml->children() as $element => $value) {
            $key = (string) $element;
            $value = count($value->children()) ? $this->XMLToArray($value) : (string) $value;

            if (array_key_exists($key, $array)) {
                if (!is_array($array[$key]) || !isset($array[$key][0])) {
                    $array[$key] = array($array[$key]);
                }
                $array[$key][] = $value;
            } else {
                $array[$key] = $value;
            }
        }

        return $array;
    }

    protected function XMLGroupDepsToArray($xml)
    {
        if ($xml->count() == 0) {
            return array();
        }

        $deps = array();
        foreach ($xml as $group) {
            // Extract group attribs (shared by all children)
            $attribs = array();
            foreach ($group->attributes() as $key => $val) {
                $attribs[$key] = (string) $val;
            }

            // Create one group dependency per childern
            foreach ($group->children() as $type => $value) {
                $deps[] = array('attribs' => $attribs, $type => $this->XMLToArray($value));
            }
        }

        return $deps;
    }
}

/**
 * Command line colorizer for Pirum.
 *
 * @package    Pirum
 * @author     Fabien Potencier <fabien@symfony.com>
 */
class Pirum_CLI_Formatter
{
    protected $styles = array(
        'ERROR_SECTION'   => array('bg' => 'red', 'fg' => 'white'),
        'INFO_SECTION'    => array('bg' => 'green', 'fg' => 'white'),
        'COMMENT_SECTION' => array('bg' => 'yellow', 'fg' => 'white'),
        'ERROR'           => array('fg' => 'red'),
        'INFO'            => array('fg' => 'green'),
        'COMMENT'         => array('fg' => 'yellow'),
    );
    protected $options    = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8);
    protected $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37);
    protected $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47);
    protected $supportsColors;

    public function __construct()
    {
        $this->supportsColors = DIRECTORY_SEPARATOR != '\\' && function_exists('posix_isatty') && @posix_isatty(STDOUT);
    }

    /**
     * Formats a text according to the given style or parameters.
     *
     * @param  string   $text  The text to style
     * @param  string   $style A style name
     *
     * @return string The styled text
     */
    public function format($text = '', $style = 'NONE')
    {
        if (!$this->supportsColors) {
            return $text;
        }

        if ('NONE' == $style || !isset($this->styles[$style])) {
            return $text;
        }

        $parameters = $this->styles[$style];

        $codes = array();
        if (isset($parameters['fg'])) {
            $codes[] = $this->foreground[$parameters['fg']];
        }
        if (isset($parameters['bg'])) {
            $codes[] = $this->background[$parameters['bg']];
        }
        foreach ($this->options as $option => $value) {
            if (isset($parameters[$option]) && $parameters[$option]) {
                $codes[] = $value;
            }
        }

        return "\033[".implode(';', $codes).'m'.$text."\033[0m";
    }

    /**
     * Formats a message within a section.
     *
     * @param string  $section  The section name
     * @param string  $text     The text message
     * @param boolean $newline  End with line break?
     */
    public function formatSection($section, $text, $newline = true)
    {
        $section = $style = array_key_exists($section, $this->styles) ? $section : 'INFO';
        $section = " $section ".str_repeat(' ', max(0, 5 - strlen($section)));
        $style .= '_SECTION';

        return sprintf('  %s %s%s', $this->format($section, $style), $text, $newline ? "\n" : '');
    }
}
