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

/**
 * @file
 * Drupal manifest file creation tool. The manifest file represents
 * the release history of a Drupal project or module. This tool
 * helps to build a custom manifest file as the part of the OpenStack
 * CI process.
 */

// initial config settings
$config = array(
  'manifest-url' => 'http://tarballs.openstack.org/groups/7.x',
  'debug' => FALSE,
  'verbose' => FALSE,
  'outfile' => 'manifest.xml',
  'help' => FALSE,
);

// command line argument schema definition
$arg_schema = array(
  'version' => array(
    '#required' => TRUE,
    '#description' => 'release version (7.x-N.N or 7.x-N.x-dev)',
  ),
  'md5' => array(
    '#required' => TRUE,
    '#description' => 'basename of release tar.gz file (eg. groups-N.N.tar.gz)',
  ),
  'releasetar' => array(
    '#required' => TRUE,
    '#description' => 'md5 hash of release tar.gz file',
  ),
  'debug' => array(
    '#required' => FALSE,
    '#description' => 'enable debug messages, defaults to FALSE',
  ),
  'verbose' => array(
    '#required' => FALSE,
    '#description' => 'enable verbouse output, defaults to FALSE',
  ),
  'manifest-url' => array(
    '#required' => FALSE,
    '#description' => 'url of original manifest file',
  ),
  'outfile' => array(
    '#required' => FALSE,
    '#description' => 'write manifest to this file, defaults to manifest.xml',
  ),
  'help' => array(
    '#required' => FALSE,
    '#description' => 'Show the help',
  ),
);

// manifest template
$manifest_template = '<?xml version="1.0" encoding="utf-8"?>
<project xmlns:dc="http://purl.org/dc/elements/1.1/">
  <title>Groups Portal</title>
  <short_name>groups</short_name>
  <type>project_distribution</type>
  <api_version>7.x</api_version>
  <recommended_major>0</recommended_major>
  <supported_majors>0</supported_majors>
  <default_major>0</default_major>
  <project_status>published</project_status>
  <link>https://drupal.org/project/groups</link>
  <terms>
    <term>
      <name>Projects</name>
      <value>Distributions</value>
    </term>
  </terms>
  <releases>
  </releases>
</project>';

// log level constants
define('_LOG_DEBUG_', 'DEBUG');
define('_LOG_INFO_', 'INFO');
define('_LOG_ERROR_', 'ERROR');

/**
 * Write a log message depending on configuration settings.
 *
 * @param  string $type    log level (debug, info, error)
 * @param  string $message log message
 */
function write_log($type, $message) {
  global $config;
  if (($type == 'DEBUG') && ($config['debug'] == FALSE)) {
    // skip if debug mode disabled
    return;
  }
  if (($type == 'INFO') && ($config['verbose'] == FALSE)) {
    // skip if verbose disabled
    return;
  }
  echo sprintf("%s [%s] %s\n", date('c'), $type, $message);
}

/**
 * Convert an array into SimpleXML recursively.
 */
function __append_elements($item, &$parent) {
  foreach ($item as $k => $v) {
    if (is_array($v)) {
      $element = $parent->addChild($k);
      __append_elements($v, $element);
    } else {
      $parent->addChild($k, $v);
    }
  }
}

/**
 * Convert a SimpleXML into an array recursively.
 */
function simple_xml_to_array($xml){
  $array = (array)$xml;
  foreach ($array as $key => $value){
    if($value instanceof SimpleXMLElement) {
      $array[$key] = simple_xml_to_array($value);
    } else {
      $array[$key] = $value;
    }
  }
  return $array;
}

/**
 * Reorder release elements into a descending list by version_patch.
 * Fix drush download issues.
 */
function order_release_elements($xml) {
  $prod_releases = array();
  $dev_releases = array();
  $releases = $xml->xpath('releases/release');
  foreach ($releases as $i => $release) {
    if (isset($release->version_patch)) {
      $prod_releases[(int)$release->version_patch] = simple_xml_to_array($release);
    } else {
      $dev_releases[(string)$release->version_extra] = simple_xml_to_array($release);
    }
  }
  unset($xml->releases);
  $releases = $xml->addChild('releases');
  // reverse-order prod releases here
  krsort($prod_releases);
  foreach ($prod_releases as $item) {
    $release = $releases->addChild('release');
    __append_elements($item, $release);
  }
  foreach ($dev_releases as $item) {
    $release = $releases->addChild('release');
    __append_elements($item, $release);
  }
  return $xml;
}

/**
 * Validate and decode a Drupal format version string
 * into a key-value array.
 *
 * 7.x-1.0 converted to:
 * array(
 *   'major' => 1,
 *   'patch' => 0,
 * )
 *
 * 7.x-1.x-dev converted to:
 * array(
 *   'major' => 1,
 *   'extra' => 'dev',
 * )
 *
 * @param  string $version version string
 * @return array           decoded version as key value array
 */
function match_version($version) {
  $pattern = '/^7.x-(?P<major>\d+).((?P<patch>\d+)|x-(?P<extra>dev))$/';
  $matches = array();
  if (preg_match($pattern, $version, $matches)) {
    foreach ($matches as $key => $value) {
      if ((is_int($key)) || ($value == NULL)) {
        unset($matches[$key]);
      }
    }
  } else {
    throw new Exception(sprintf('Invalid version number format: %s', $version));
  }
  return $matches;
}

/**
 * Insert or update a release element in project xml.
 *
 * @param  class $xml          project xml as a simplexml object.
 * @param  string $version     release version
 * @param  string $releaseTar  basename of release tar.gz file
 * @param  string $md5         md5 hash of release tar.gz file
 * @param  string $fileSize    file size of release (optional)
 * @param  string $releaseDate release date of the release (optional)
 */
function append_release($xml, $version, $releaseTar, $md5, $fileSize = NULL, $releaseDate = NULL) {
  $downloadUrl = 'http://tarballs.openstack.org/groups/'.$releaseTar;
  // remove previous release entry with same version
  list($element) = $xml->xpath('/project/releases/release/version[. = "'.$version.'"]/parent::*');
  if (!empty($element)) {
    unset($element[0]);
    $verb = 'Override';
  } else {
    $verb = 'Insert';
  }
  write_log(_LOG_INFO_, sprintf('%s a release element [version=%s, releaseTar=%s, md5=%s]',
    $verb, $version, $releaseTar, $md5));
  // add release elements
  $release = $xml->releases->addChild('release');
  $release->addChild('name', 'groups '.$version);
  $release->addChild('version', $version);
  $release->addChild('tag', $version);
  $release->addChild('status', 'published');
  $release->addChild('download_link', $downloadUrl);
  $release->addChild('mdhash', $md5);
  // append version
  $v = match_version($version);
  if (empty($v)) {
    throw new Exception('Invalid version format.');
  }
  foreach ($v as $key => $value) {
    $release->addChild('version_'.$key, $value);
  }
  $files = $release->addChild('files');
  $file = $files->addChild('file');
  $file->addChild('url', $downloadUrl);
  $file->addChild('archive_type', 'tar.gz');
  $file->addChild('variant', 'full');
  $file->addChild('md5', $md5);
}

/**
 * Show the help and construct parameter list by
 * argument schema.
 *
 * @param  array $arg_schema  argument schema
 */
function print_help($arg_schema) {
  echo "Generate Drupal manifest file to represent a project release history.\n\n";
  echo "Example:\n";
  echo "  release-manifest.php --version=7.x-1.0 --releasetar=groups-1.0.tar.gz --md5=c59611415cea4bc6397b1351b2b36b7c\n\n";
  echo "Options:\n";
  foreach ($arg_schema as $key => $value) {
    echo sprintf("  --%-16s  %s\n", $key, $value['#description']);
  }
  echo "\n";
}

/**
 * Parse and validate command line parameters based on
 * predefined argument schema.
 *
 * @param  array $argv       command line arguments
 * @param  array $arg_schema argument schema
 * @return array             parsed parameters
 */
function get_cli_parameters($argv, $arg_schema) {
  $params = array();
  // parse cli arguments
  foreach ($argv as $arg) {
    if (strpos($arg, '--') !== false) {
      list($key, $value) = explode("=",$arg);
      $key = substr($key, 2);
      if (isset($arg_schema[$key]) == FALSE) {
        print_help($arg_schema);
        throw new Exception(sprintf('Invalid command line argument: %s', $key));
      }
      if (isset($key)) {
        $params[$key] = isset($value) ? $value : TRUE;
      }
    }
  }
  if (empty($params['help'])) {
    // check required parameters
    foreach ($arg_schema as $key => $value) {
      if (($value['#required']) && (empty($params[$key]))) {
        print_help($arg_schema);
        throw new Exception(sprintf('Mandatory parameter %s missing.', $key));
      }
    }
  }
  return $params;
}

try {
  // parse cli arguments and merge with config
  $params = get_cli_parameters($argv, $arg_schema);
  $config = array_replace($config, $params);
  if ($config['help']) {
    print_help($arg_schema);
    exit(0);
  }
  if ($config['debug']) {
    write_log(_LOG_DEBUG_, 'Command line args:');
    foreach ($config as $key => $value) {
      write_log(_LOG_DEBUG_, sprintf('  %-16s = %s', $key, (string)$value));
    }
  }
  // load original manifest
  $xml = @simplexml_load_file($config['manifest-url']);
  if (!$xml) {
    write_log(_LOG_INFO_, 'Create a new manifest file, failed to fetch from remote url.');
    $xml = simplexml_load_string($manifest_template);
  }
  append_release($xml, $params['version'], $params['releasetar'], $params['md5']);
  // reorder xml here
  $xml = order_release_elements($xml);
  $xml_content = $xml->asXML();
  write_log(_LOG_DEBUG_, sprintf("Generated manifest:\n %s", $xml_content));
  file_put_contents($config['outfile'], $xml_content);
} catch (Exception $e) {
  write_log(_LOG_ERROR_, $e->getMessage());
  exit(1);
}