<?php

/**
 * Wrapper around php tidy class.
 *
 * @param string $html
 *
 * @return void
 */
function applyTidy (&$html) {
  $config = array(
      'indent'          => TRUE,
      'output-xhtml'    => TRUE,
      'sort-attributes' => 'alpha',
      'wrap'            => 80,
  );
  $tidy = new tidy();
  $tidy->parseString($html, $config, 'utf8');
  $tidy->cleanRepair();
  $html = (string) $tidy;
}

class Context {
  protected $logLevelClasses = NULL;

  /**
   * Directory in which the current script is located.
   *
   * Follow symlinks if applicable, to obtain the actual implementation
   * directory, instead of just the main file symlink, in order to find the
   * other files.
   *
   * @var string
   */
  protected $dirname;

  /**
   * Graphics context
   *
   * @var GraphicsContext
   */
  protected $gc = NULL;

  /**
   * Logging level, as per RFC5424
   *
   * @link http://php.net/network.constants.php
   *
   * @var integer
   */
  protected $logLevel = NULL;

  /**
   * Messages for display.
   *
   * @var array
   */
  protected $messages = array();

  /**
   * Requested path: <$PHP_SELF>?q=a/b/c
   *
   * @var string
   */
  protected $path = NULL;

  /**
   * Tidy the output ?
   *
   * @var boolean
   */
  protected $tidy = NULL;

  /**
   * User information: logged or not ?
   *
   * @var boolean
   */
  protected $user = FALSE;

  function __destruct() {
    if (!empty($this->messages)) {
      $ret = (string) new Element('pre', array('class' => array('messages')),
        implode("\n", $this->getMessage(TRUE)));
      echo $ret;
    }
  }

  function __toString() {
    $ret = '<pre>' . print_r($this, TRUE) . '</pre>';
    return $ret;
  }

  public function getDirname() {
    if (!isset($this->dirname)) {
      $this->dirname = dirname(__FILE__);
    }

    return $this->dirname;
  }

  public function getLogLevel() {
    if (!isset($this->logLevel)) {
      $usLogLevel = NULL;
      foreach ($_GET as $key => $value) {
        if (strtolower($key) === 'loglevel') {
          $usLogLevel = (int) $value;
          break;
        }
      }
      if (!isset($usLogLevel)) {
        $usLogLevel = LOG_NOTICE;
      }

      if ($usLogLevel < LOG_EMERG) {
        $this->logLevel = LOG_EMERG;
      }
      elseif ($usLogLevel > LOG_DEBUG) {
        $this->logLevel = LOG_DEBUG;
      }
      else {
        $this->logLevel = $usLogLevel; // We now know it to be safe
      }
    }

    return $this->logLevel;
  }

  public function getLogLevelClass($logLevel) {
    if (!isset($this->logLevelClasses)) {
      $this->logLevelClasses = array(
        LOG_EMERG   => 'error',
        LOG_ALERT   => 'error',
        LOG_CRIT    => 'error',
        LOG_ERR     => 'error',
        LOG_WARNING => 'warning',
        LOG_NOTICE  => 'warning',
        LOG_INFO    => 'status',
        LOG_DEBUG   => 'status',
      );
    }
    if ($logLevel < LOG_EMERG) {
      $logLevel = LOG_EMERG;
    }
    elseif ($logLevel > LOG_DEBUG) {
      $logLevel = LOG_DEBUG;
    }
    return $this->logLevelClasses[$logLevel];
  }

  public function getMessage($clear = FALSE) {
    $ret = $this->messages;
    if ($clear) {
      $this->messages = array();
    }

    return $ret;
  }

  /**
   * Return the requested path.
   *
   * @param string $path
   */
  public function getPath() {
    if (!isset($this->path)) {
      $this->path = empty($_GET['q']) ? '' : $_GET['q'];
    }

    return $this->path;
  }

  public function getTidy() {
    if (!isset($this->tidy)) {
      $this->tidy = TRUE;
      foreach ($_GET as $key => $value) {
        if (strtolower($key) === 'tidy') {
          $this->tidy = !!$value;
          break;
        }
      }
    }

    return $this->tidy;
  }

  /**
   * Add a message to the messages list if it is above the current logging level.
   *
   * @param string $text
   * @param integer $logLevel
   *
   * @return void
   */
  public function setMessage($text, $logLevel = LOG_NOTICE) {
    if ($logLevel <= $this->getlogLevel()) {
      if (is_array($text) || (is_object($text) && !method_exists($text, '__toString'))) {
        $this->messages[] = array(print_r($text, TRUE), $logLevel);
      }
      else {
        $this->messages[] = array((string) $text, $logLevel);
      }
    }
  }
}

/**
 * A wrapper for XML elements.
 */
class Element {
  public $attributes = array();
  public $name = NULL;
  public $new_line; // Add a new line after element
  public $value = NULL;

  public function __construct($name, $attr = NULL, $value = NULL) {
    $this->name = $name;
    $this->attributes = $attr;
    $this->value = $value;
  }

  /**
   * @link drupal7/includes/common.inc#check_plain()
   */
  public static function check_plain($text) {
    return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
  }

  /**
   * @link drupal7/includes/common.inc#drupal_attributes()
   */
  public static function getSerializedAttributes(array $attributes = array()) {
    foreach ($attributes as $attribute => &$data) {
      $data = implode(' ', (array) $data);
      $data = $attribute . '="' . self::check_plain($data) . '"';
    }

  return $attributes ? ' ' . implode(' ', $attributes) : '';
  }

  public function __toString() {
    $ret = '<'. $this->name;
    if (!empty($this->attributes) && is_array($this->attributes)) {
      $ret .= self::getSerializedAttributes($this->attributes);
    }
    if (empty($this->value)) {
      $ret .= ' />';
    }
    else {
      $ret .= '>';
      if ($this->value instanceof Element) {
        $ret .= (string) $this->value; // force __toString()
      }
      elseif (is_array($this->value)) {
        $ret .= implode('', $this->value);
      }
      else {
        $ret .= $this->value;
      }
      $ret .= "</$this->name>";
    }
    return $ret;
  }
}

class GraphicsContext {
  protected $palette = array();
}

class Page extends Element {

  /**
   * The HTML body element of the page.
   *
   * @var array
   */
  protected $body;

  /**
   * @var Context
   */
  protected $context;

  /**
   * Ths HTML head element of the page.
   *
   * @var array
   */
  protected $head;
  /**
   * @var array
   */
  protected $finalized = array();

  /**
   * @var array
   */
  protected $styles;

  /**
   * Page constructor.
   *
   * @param Context $context
   * @param array $item
   *   A router info array.
   *
   * @see Router::getInfo()
   */
  public function __construct(Context $context, array $item) {
    parent::__construct('html');
    $this->context = $context;
  }

  public function finalizeBody() {
    if (isset($this->finalized['body'])) {
      throw new Exception('Attempt to finalize already finalized body');
    }
    if ($message = $this->context->getMessage(TRUE)) {
      $items = array();
      foreach ($message as $row) {
        $items[] = new Element('li', array(
          'class' => array($this->context->getLogLevelClass($row[1])),
        ), $row[0]);
      }
      $this->setBody(new Element('div', array('class' => array('messages')), $items));
    }
    $this->finalized['body'] = TRUE;
  }

  public function finalizeHead() {
    if (isset($this->finalized['head'])) {
      throw new Exception('Attempt to finalize already finalized head');
    }
    $cssLink = new Element('link', array(
      'rel' => 'stylesheet',
      'type' => 'text/css',
      'href' => $this->context->getPath() .'/memcache_ui.css',
    ));
    $this->setHead($cssLink);

    $this->finalized['head'] = TRUE;
  }

  public function getBody() {
    $this->finalizeBody();
    $body = new Element('body', NULL, $this->body);
    $ret = (string) $body;
    return $ret;
  }

  public function getHead() {
    $this->finalizeHead();
    $head = new Element('head', NULL, $this->head);
    $ret = (string) $head;
    return $ret;
  }

  public function getHeader($name) {
  }

  public function render() {
    $html = new Element('html', NULL, $this->getHead() . $this->getBody());
    return (string) $html;
  }

  public function setBody($fragment) {
    $this->body[] = $fragment;
  }

  public function setHead($item) {
    $this->head[] = $item;
  }

  public function setHeader($name, $value) {

  }

}

class Page_Main extends Page {
  function finalizeBody() {
    $hello = new Element('p', NULL, 'Hello world');
    $this->setBody($hello);
    parent::finalizeBody();
  }

}

class Router {
  function __construct(Context $context) {
    $this->context = $context;
  }

  function getInfo() {
    $ret = array(
      '^server/(\w+)/flush/(\w+)$' => array(
        'page class' => 'page_server_flush',
        'page arguments' => array('$1', '$2'),
        'title callback' => 'title_server',
        'title arguments' => 'page arguments',
      ),
      '^server/(\w+)/flush$' => array(
        'page class' => 'page_server_flush',
        'page arguments' => array('$1'),
        'title callback' => 'title_server',
        'title arguments' => 'page arguments',
      ),

      '^server/(\w+)/slab/(\d+)$' => array(
        'page class' => 'page_slab_view',
        'page arguments' => array('$1', '$2'),
        'title callback' => 'title_slab',
        'title arguments' => 'page arguments',
      ),

      '^server/(\w+)/key/(.+)/delete/(\w+)$' => array(
        'page class' => 'page_variable_delete_confirm',
        'page arguments' => array('$1', '$2', '$3'),
        'title callback' => 'title_variable',
        'title arguments' => 'page arguments',
      ),
      '^server/(\w+)/key/(.+)/delete$' => array(
        'page class' => 'page_variable_delete_form',
        'page arguments' => array('$1', '$2'),
        'title callback' => 'title_variable',
        'title arguments' => 'page arguments',
      ),

      '^server/(\w+)/key/(.+)/dump$' => array(
        'page class' => 'page_variable_view_php',
        'page arguments' => array('$1', '$2'),
        'title callback' => 'title_variable',
        'title arguments' => 'page arguments',
      ),
      '^server/(\w+)/key/(.+)/php$' => array(
        'page class' => 'page_variable_view_php',
        'page arguments' => array('$1', '$2'),
        'title callback' => 'title_variable',
        'title arguments' => 'page arguments',
      ),
      '^server/(\w+)/key/(.+)$' => array(
        'page class' => 'page_variable_view_text',
        'page arguments' => array('$1', '$2'),
        'title callback' => 'title_variable',
        'title arguments' => 'page arguments',
      ),

      '^slabs$' => array(
        'page class' => 'page_slab_overview',
        'title' => 'Slabs per server',
      ),

      '^$' => array(
        'page class' => 'Page_Main',
        'title' => 'Overview',
      ),
    );

    return $ret;
  }

  function getRoute() {
    $found = FALSE;
    $path = $this->context->getPath();
    $matches = array();
    $infoDefaults = array(
      'page arguments' => array(),
    );
    foreach ($this->getInfo() as $regex => $info) {
      $regex = "@$regex@";
      $count = preg_match($regex, $path, $matches);
      if ($count) {
        $found = TRUE;
        break;
      }
    }
    if ($found) {
      $this->context->setMessage("Found at $regex", LOG_DEBUG);
      $this->context->setMessage("Info: ". print_r($info, TRUE), LOG_DEBUG);
      $info = array_merge($infoDefaults, $info);
      if (!empty($info['page arguments'])) {
        $regexes = array_fill(0, count($info['page arguments']), $regex);
        $paths = array_fill(0, count($info['page arguments']), $path);
        $info['page arguments'] = preg_replace($regexes, $info['page arguments'], $paths);
      }
    }
    else {
      $info = NULL;
    }

    return $info;
  }
}

function main() {
  try {
    ob_start();
    $context = new Context();
    $context->setMessage("Dirname: [". $context->getDirname() . "]", LOG_DEBUG);
    $context->setMessage("Path: [". $context->getPath() . "]", LOG_DEBUG);

    $router = new Router($context);
    $item = $router->getRoute();
    $page = new $item['page class']($context, $item);
    // echo '<pre>'; var_dump($page);
    echo $page->render();

    $html = ob_get_clean();
    if ($context->getTidy()) {
      applyTidy($html);
    }
    echo $html;
  }
  catch (Exception $e) {
    echo '<pre>';
    echo $e->getMessage() . PHP_EOL;
    echo $e->getTraceAsString();
    echo "</pre>";
  }
}

main();