<?php
// $Id: patterns.module,v 1.1.2.2.2.75 2009/08/18 08:51:51 sarvab Exp $

/**
 * @file
 * Enables extremely simple adding/removing features to your site with minimal to no configuration
 */

/**
 * @todo:
 * @ Enable pattern configurations
 * @ **done**Enable actions to see ids created/updated from other actions inside the pattern (tokens?)
 * @ **semi-done** Reset patterns
 * @ Enable components to analyze the current pattern for better validation
 * @ Allow module version restricting
 * @ Put in functionality to auto-download modules (and the correct version)
 * @ Enable backups before running patterns and reverting back to those backups
 * @ Implement a progress meter
 * @ Handle default values better to allow for absolute minimal actions
 * @ Enable interactive actions by allowing patterns to resume from a saved position
 * @ Implement an export feature for all available form_ids
 * @ Allow resuming failed patterns
 * @ In the pattern details, list any sub-patterns directly on the patterns listing page
 */

/**
 * Implementation of hook_perm().
 */
function patterns_perm() {
  return array('administer patterns');
}

/**
 * Implementation of hook_menu().
 */
function patterns_menu() {
  $items = array();

  $items['admin/build/patterns'] = array(
    'title' => 'Patterns',
    'description' => 'Administer patterns available for your site',
    'page callback' => 'patterns_list',
    'access arguments' => array('administer patterns')
  );
  $items['admin/build/patterns/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10
  );
  $items['admin/build/patterns/edit'] = array(
    'title' => 'Edit Pattern',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_edit'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_CALLBACK
  );
  $items['admin/build/patterns/enable'] = array(
    'title' => 'Enable Pattern',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_enable_pattern'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_CALLBACK
  );
  $items['admin/build/patterns/publish'] = array(
    'title' => 'Publish Pattern',
    'page callback' => 'patterns_publish_pattern',
    'access arguments' => array('administer patterns'),
    'type' => MENU_CALLBACK
  );
  $items['admin/build/patterns/unpublish'] = array(
    'title' => 'Unpublish Pattern',
    'page callback' => 'patterns_unpublish_pattern',
    'access arguments' => array('administer patterns'),
    'type' => MENU_CALLBACK
  );
  $items['patterns.xml'] = array(
    'title' => 'Published Patterns',
    'page callback' => 'patterns_feed',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );

  $items['admin/build/patterns/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_settings'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10
  );

  $items['admin/build/patterns/get'] = array(
    'title' => 'Download Pattern Source',
    'page callback' => 'patterns_get_source',
    'access callback' => TRUE,
    'type' => MENU_CALLBACK
  );

  //     $items[] = array('path' => 'admin/build/patterns/configure',
  //       'title' => t('Configure Pattern'),
  //       'callback' => 'drupal_get_form',
  //       'callback arguments' => array('patterns_configure_pattern'),
  //       'type' => MENU_CALLBACK
  //     );

  //     $items[] = array('path' => 'admin/build/patterns/info',
  //       'title' => t('Pattern Details'),
  //       'callback' => 'patterns_info',
  //       'type' => MENU_CALLBACK
  //     );

  //     $items[] = array('path' => 'admin/build/patterns/disable',
  //       'access' => user_access('administer patterns'),
  //       'title' => t('Disable Pattern'),
  //       'callback' => 'drupal_get_form',
  //       'callback arguments' => array('patterns_disable_pattern'),
  //       'type' => MENU_CALLBACK
  //     );

  $items['admin/build/patterns/modules'] = array(
    'title' => 'Pattern Modules',
    'page callback' => 'patterns_modules_page',
    'access arguments' => array('administer patterns'),
    'type' => MENU_CALLBACK
  );

  //     $items[] = array('path' => 'admin/build/patterns/revert',
  //       'access' => user_access('administer patterns'),
  //       'title' => t('Revert Pattern'),
  //       'callback' => 'patterns_revert',
  //       'type' => MENU_CALLBACK
  //     );


  $items['admin/build/patterns/import'] = array(
    'title' => 'Import',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_import_source'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_LOCAL_TASK
  );
  $items['admin/build/patterns/import/source'] = array(
    'title' => 'Import Source Code',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10
  );
  $items['admin/build/patterns/import/file'] = array(
    'title' => 'Import File',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_import_file'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_LOCAL_TASK
  );
  $items['admin/build/patterns/import/url'] = array(
    'title' => 'Import from URL',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_import_url'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_LOCAL_TASK
  );
  $items['admin/build/patterns/server'] = array(
    'title' => 'Patterns Server',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('patterns_import_server'),
    'access arguments' => array('administer patterns'),
    'type' => MENU_LOCAL_TASK,
    'weight' => -5
  );

  return $items;
}



/**
 * Implementation of hook_help().
 */
function patterns_help($section, $arg = NULL) {
  $output = '';
  switch ($section) {
    case 'admin/build/patterns':
      $output = t('Patterns will be looked for in files under the following locations: ') . theme('item_list', patterns_paths());
      break;
  }
  return $output;
}

/**
 * Display the pattern settings form
 */
function patterns_settings(&$form_state) {
  $form['patterns_form_helper'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable Patterns Form Helper'),
    '#description' => t('When enabled, patterns form helper will dump $form_id and $form_state variables at the bottom of each page. Dump will always contain values from the latest form submission. This may be very helpful while writing your own patterns.'),
    '#default_value' => variable_get('patterns_form_helper', FALSE),
  );
  $form['patterns_allow_publish'] = array(
    '#type' => 'checkbox',
    '#title' => t('Share your patterns'),
    '#description' => t('When enabled, you will be able to "publish" selected patterns and make them available to other patterns users on the following URL: %url.', array('%url' => url(NULL, array('absolute' => TRUE)) . (variable_get('clean_url', 0) ? '' : '?q=') .'patterns.xml')),
    '#default_value' => variable_get('patterns_allow_publish', FALSE),
  );
  // Reload patterns while we are here and ensure the lists are up to date
  patterns_get_patterns(TRUE);
  
  return system_settings_form($form);
}

/**
 * Display the import pattern form
 */
function patterns_import_source(&$form_state) {
  if (empty($form_state['post'])) {
    drupal_set_message(t('Import feature currently supports only XML file format.'), 'warning');
  }
  $form['xmlname'] = array(
    '#type' => 'textfield',
    '#title' => t('Pattern Identifier'),
    '#description' => t('Machine readable name for the pattern. The actual title should be included in the pattern itself.'),
    '#required' => true
  );
  $form['xmlsource'] = array(
    '#type' => 'textarea',
    '#rows' => 15,
    '#title' => t('Enter Pattern Source Code'),
    '#description' => t('Imported patterns are not executed until you run them manually.')
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Import')
  );

  $form['#validate'][] = 'patterns_import_validate';
  $form['#submit'][] = 'patterns_import_submit';

  return $form;
}

/**
 * Menu callback - returns source code of the requested pattern
 * if the pattern is public
 *
 * @param $pid
 *   pattern id
 * @return
 *   source code of the requested pattern
 */
function patterns_get_source($pid) {
  if (!is_numeric($pid)) {
    exit;
  }

  $pattern = patterns_get_pattern($pid);

  // make sure pattern is public (published)
  if (!$pattern->public) {
    exit;
  }

  $content_type = 'text/plain';
  if (substr($pattern->file,-4) == '.xml') {
    $content_type = 'text/xml';
  }

  drupal_set_header('Content-Type: '. $content_type .'; charset=utf-8');
  print file_get_contents($pattern->file);
  exit;
}
/**
 * Prints XML Feed of published (public) patterns
 *
 */
function patterns_feed() {
  global $base_url;

  if (!variable_get('patterns_allow_publish', FALSE)) {
    return drupal_not_found();
  }

  $patterns = variable_get('patterns_allow_publish', FALSE) ? patterns_get_patterns() : array();

  foreach ($patterns as $pattern) {
    if (!$pattern->public) {
      continue;
    }
    preg_match('/[^\.]*$/', $pattern->file, $matches);
    $extension = $matches[0];
    $item = "    <pattern>\n";
    $item .= "      <pid>". $pattern->pid ."</pid>\n";
    $item .= "      <name>". $pattern->name . "</name>\n";
    $item .= "      <title>". $pattern->title . "</title>\n";
    $item .= "      <description>". $pattern->description . "</description>\n";
    $item .= "      <category>". $pattern->pattern['info']['category'] . "</category>\n";
    $item .= "      <file_type>". $extension . "</file_type>\n";
    $item .= "    </pattern>\n";

    $items .= $item;
  }

  $header = "  <info>\n";
  $header .= "    <url>". $base_url ."</url>\n";
//  $header .= "    <description>". $description ."</description>\n";
  $header .= "  </info>\n";

  $feed = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  $feed .= "<patterns_feed>\n";
  $feed .= $header;
  $feed .= "  <patterns>\n";
  $feed .= $items;
  $feed .= "  </patterns>\n";
  $feed .= "</patterns_feed>\n";

  drupal_set_header('Content-Type: text/xml; charset=utf-8');
  print $feed;
  exit;
}

/**
 * Display the import pattern from server form
 */
function patterns_import_server(&$form_state) {
  if (empty($form_state['storage'])) {

    $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
    if (!file_check_directory($path, true)) {
      $message = t("In order to save imported patterns, directory %path must be writable.<br />", array('%path' => $path));
      $link = l(t('status report'), 'admin/reports/status');
      $message .= t('You might want to check the !link to ensure your files directory exists and is writable.', array('!link' => $link));
      drupal_set_message($message, 'error');
      return array();
    }

    $form['server_url'] = array(
      '#type' => 'textfield',
      '#title' => t('Specify Patterns Server URL'),
      '#description' => t('URL of the web site you want to import patterns from. Example: http://patterns.graviteklabs.com'),
      '#default_value' => variable_get('patterns_default_server', 'http://patterns.graviteklabs.com'),
      '#size' => 48
    );
  }
  else {
    $feed = $form_state['storage']['feed'];

    $form['patterns'] = array(
      '#type' => 'fieldset',
      '#tree' => TRUE,
      '#title' => t('Patterns'),
    );

    foreach ($feed['patterns'] as $pattern) {
      $form['patterns'][$pattern['pid']] = array(
        '#type' => 'checkbox',
        '#title' => $pattern['title'],
        '#description' => $pattern['description'],
        '#disabled' => in_array($pattern['file_type'], patterns_file_types()) ? FALSE : TRUE,
      );
    }
  }

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => empty($form_state['storage']) ? t('Get Patterns List') : t('Import')
  );

  $form['#validate'][] = 'patterns_import_server_validate';
  $form['#submit'][] = 'patterns_import_server_submit';

  return $form;
}

function patterns_import_server_validate($form, &$form_state) {
  if (empty($form_state['storage'])) {
    if (!ini_get('allow_url_fopen')) {
      form_set_error('fopen', t('allow_url_fopen must be enabled in your php configuration in order to use this feature.'));
      return;
    }
    $feed_url = $form_state['values']['server_url'] .'/patterns.xml';
    if (empty($feed_url)) {
      form_set_error('server_url', t('Please enter Patterns Server URL.'));
      return;
    }
    if (!$xml = file_get_contents($feed_url)) {
      form_set_error('server_url', t('Failed to retreive the patterns feed from: %url. Please check your URL and try again.', array('%url' => $form_state['values']['server_url'])));
      return;
    }
    // @todo: replace this with proper XML validation
    if (strpos($xml, '<?xml') !== 0) {
      form_set_error('', t('URL %url is not a valid XML file.', array('%url' => $form_state['values']['server_url'])));
      return;
    }

    $feed = patterns_from_source($xml);
    if (!$feed) {
      form_set_error('', t('URL %url is not a valid patterns feed.', array('%url' => $form_state['values']['server_url'])));
      return;
    }
    $feed = patterns_feed_rearrange_data($feed);
    if (!$feed) {
      form_set_error('', t('URL %url is not a valid patterns feed.', array('%url' => $form_state['values']['server_url'])));
      return;
    }

    $form_state['storage']['step'] = 'get url';
    $form_state['storage']['server_url'] = $form_state['values']['server_url'];
    $form_state['storage']['feed_url'] = $feed_url;
    $form_state['storage']['feed'] = $feed;
  }
  else {
    $form_state['storage']['step'] = 'select patterns';
  }
}

function patterns_import_server_submit($form, &$form_state) {

  if ($form_state['storage']['step'] == 'get url') {
    return;
  }

  $pids = array_keys(array_filter($form_state['values']['patterns']));
  $feed = $form_state['storage']['feed'];
  $errors = array();

  foreach ($pids as $pid) {
    $url = $feed['info']['url'] .'/admin/build/patterns/get/'. $pid;
    $pattern_info = $feed['patterns'][$pid];

    if (!$source = file_get_contents($url)) {
      $errors['get file'][] = $pattern_info;
      continue;
    }

    // save file
    $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
    $path .= '/'. $pattern_info['name'] .'.'. $pattern_info['file_type'];
    if (!$saved = file_save_data($source, $path, FILE_EXISTS_ERROR)) {
      $errors['save file'][] = $pattern_info;
      continue;
    }

      // choose appropriate function based on the file extension
    $func = 'patterns_load_'. $pattern_info['file_type'];

    // Load pattern
    if (!$pattern = $func($saved)) {
      $errors['load pattern'][] = $pattern_info;
      unlink($saved);
      continue;
    }

    patterns_save_pattern($pattern, $saved, $pattern_info['name']);
  }

  if (!empty($errors)) {
    $patterns = array();
    foreach ($errors as $type => $files) {
      foreach ($files as $file) {
        $patterns[] = $file['title'] .' (cause: "'. $type .'" failed)';
      }
      $patterns = theme('item_list', $patterns);
    }
    drupal_set_message(t('Import failed for the following patterns: ') .'<br>'. $patterns, 'error');
    return;
  }

  unset($form_state['storage']);
  $form_state['redirect'] = 'admin/build/patterns';

}

function patterns_feed_rearrange_data($feed) {
  unset($feed['tag']);
  foreach ($feed as $key => $section) {
    $tag = $section['tag'];
    if ($tag == 'pattern') {
      unset($section['tag']);
      if (!isset($section['value'])) {
        foreach ($section as $t) {
          if ($t['tag'] == 'pid') {
            $pid = $t['value'];
            break;
          }
        }
        $result[$pid] = patterns_feed_rearrange_data($section);
      }
      else {
        $result[$tag] = $section['value'];
      }
    }
    else {
      unset($section['tag']);
      if (!isset($section['value'])) {
        $result[$tag] = patterns_feed_rearrange_data($section);
      }
      else {
        $result[$tag] = $section['value'];
      }
    }
  }
  return $result;
}

function patterns_publish_pattern($pid) {
  if (is_numeric($pid)) {
    $result = db_query("UPDATE {patterns} SET public = 1 WHERE pid = %d", $pid);
  }
  drupal_set_message('Pattern published.');
  drupal_goto('admin/build/patterns');
}

function patterns_unpublish_pattern($pid) {
  if (is_numeric($pid)) {
    $result = db_query("UPDATE {patterns} SET public = 0 WHERE pid = %d", $pid);
  }
  drupal_set_message('Pattern unpublished.');
  drupal_goto('admin/build/patterns');
}

/**
 * Display the import pattern file form
 */
function patterns_import_file(&$form_state) {
  if (empty($form_state['post'])) {
    drupal_set_message(t('Import feature currently supports only XML file format.'), 'warning');
  }
  $form['#attributes']['enctype'] = 'multipart/form-data';
  $form['xmlfile'] = array(
    '#type' => 'file',
    '#title' => t('Upload Pattern File'),
    '#description' => t('Imported patterns are not executed until you run them manually.'),
    '#size' => 48
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Import')
  );

  $form['#validate'][] = 'patterns_import_validate';
  $form['#submit'][] = 'patterns_import_submit';

  return $form;
}

/**
 * Display the import pattern url form
 */
function patterns_import_url(&$form_state) {
  if (empty($form_state['post'])) {
    drupal_set_message(t('Import feature currently supports only XML file format.'), 'warning');
  }
  $form['xmlurl'] = array(
    '#type' => 'textfield',
    '#title' => t('Specify an URL'),
    '#description' => t('Import a pattern from a remote URL. Imported patterns are not executed until you run them manually.'),
    '#size' => 48
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Import')
  );

  $form['#validate'][] = 'patterns_import_validate';
  $form['#submit'][] = 'patterns_import_submit';

  return $form;
}

function patterns_import_validate($form, &$form_state) {

  $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  if (!file_check_directory($path, true)) {
    $message = t("Destination folder doesn't exist: !path<br />", array('!path' => $path));
    $link = l(t('status report'), 'admin/reports/status');
    $message .= t('You might want to check the !link to ensure your files directory exists and is writable.', array('!link' => $link));
    form_set_error('xmlfile', $message);
    return;
  }
  $validators = array('file_validate_extensions' => array('xml'));
  if (isset($form_state['values']['xmlfile']) && ($file = file_save_upload('xmlfile', $validators))) {
    $form_state['values']['xmlsource'] = file_get_contents($file->filepath);
    $form_state['pattern_file'] = $file;
    $form_state['pattern_file']->destination = $path;
  }
  else if (isset($form_state['values']['xmlfile'])) {
    form_set_error('files[xmlfile]', t('Error uploading XML file.'));
    return;
  }
  else if ($form_state['values']['xmlurl']) {
    if (!ini_get('allow_url_fopen')) {
      form_set_error('xmlsource', t('allow_url_fopen must be enabled in your php configuration in order to use this feature.'));
      return;
    }

    if (strtolower(substr($form_state['values']['xmlurl'], -4)) != '.xml') {
      form_set_error('xmlsource', t('Invalid file extension. Only "XML" file extension is allowed.'));
      return;
    }

    if (!($form_state['values']['xmlsource'] = file_get_contents($form_state['values']['xmlurl']))) {
      form_set_error('xmlurl', t('Failed to retreive the pattern specified: '. $form_state['values']['xmlurl']) .'. Check your URL and try again.');
      return;
    }

    $pattern = array('/\.[^\.]*$/', '/[^a-zA-Z0-9_]/');
    $replacement = array('', '_');
    $form_state['values']['xmlname'] = preg_replace($pattern, $replacement, basename($form_state['values']['xmlurl']));
  }

  if (strpos($form_state['values']['xmlsource'], '<?xml') !== 0) {
    $form_state['values']['xmlsource'] = '<?xml version="1.0" encoding="ISO-8859-1"?>' . $form_state['values']['xmlsource'];
  }

  if ($form_state['values']['xmlname'] && preg_match('/[^a-zA-Z0-9_]/', $form_state['values']['xmlname'])) {
    form_set_error('xmlname', t('You can only include letters, numbers, and underscores in the pattern identifier.'));
  }
  else if ($form_state['values']['xmlname'] && preg_match('/^_/', $form_state['values']['xmlname'])) {
    form_set_error('xmlname', t('You cannot start the pattern identifier with an underscore.'));
  }

  // @TODO validate XML and don't allow import if validation fails
  $parse = drupal_xml_parser_create($form_state['values']['xmlsource']);
  $success = xml_parse_into_struct($parse, $form_state['values']['xmlsource'], $vals, $index);

  // Check that the xml was properly parsed and also that the
  // root <pattern> tag and also an <info> tag were used.
  if (!$success || !$vals || $vals[0]['tag'] != 'PATTERN' || $vals[1]['tag'] != 'INFO') {
    form_set_error('xmlsource', t('Error parsing the XML, please check your syntax and try again.'));
  }
}

function patterns_import_submit($form, &$form_state) {
  if (isset($form_state['pattern_file'])) {
    $saved = file_copy($form_state['pattern_file']->filepath, $form_state['pattern_file']->destination);
  }
  else if ($form_state['values']['xmlsource']) {
    $saved = file_save_data($form_state['values']['xmlsource'], variable_get('patterns_save_xml', 'patterns') .'/'. $form_state['values']['xmlname'] .'.xml', FILE_EXISTS_REPLACE);
  }

  if ($saved) {
    // Reload patterns
    patterns_get_patterns(true);
    drupal_set_message('Pattern successfully imported.');
  }
  else {
    drupal_set_message("File couldn't be saved on the server. Import failed.", 'error');
  }
  $form_state['redirect'] = 'admin/build/patterns';
}

function patterns_list() {
  drupal_add_css(drupal_get_path('module', 'patterns') .'/patterns.css');
  drupal_add_js(drupal_get_path('module', 'patterns') .'/patterns.js');

  patterns_load_components();
  $patterns = patterns_get_patterns();
  if (empty($patterns)) return t('No patterns available.');

  //   $header = array(t('Title'), t('Status'), t('Version'), t('Public'), t('Actions'));
  $header = array(t('Title'), t('Status'), t('Version'), t('Actions'));

  // List all patterns
  $rows = array();
  foreach($patterns as $pid => $pattern) {
    $actions = array();
    if (!$pattern->status) {
      $actions[] =  l(t('Run'), 'admin/build/patterns/enable/'. $pid);
    }
    else if ($pattern->enabled >= $pattern->updated) {
      $actions[] =  l(t('Re-Run'), 'admin/build/patterns/enable/'. $pid);
    }
    else {
      $actions[] = l(t('Run Update'), 'admin/build/patterns/enable/'. $pid);
    }
    $actions[] = l(t('Edit'), 'admin/build/patterns/edit/'. $pid);
    if (variable_get('patterns_allow_publish', FALSE)) {
      $actions[] = $pattern->public ? l(t('Unpublish'), 'admin/build/patterns/unpublish/'. $pid) : l(t('Publish'), 'admin/build/patterns/publish/'. $pid);
    }
    $actions = implode('&nbsp;&nbsp;', $actions);

    $cells = array();
    //      $title = l($pattern->title, 'admin/build/patterns/info/'. $pid, array('attributes' => array('class' => 'pattern-title', 'id' => 'pid-'. $pid)));
    $title = '<span id="pid-'. $pid .'" class="pattern-title">'. $pattern->title .'</span>';
    //     $view_more = '<div>'. t('Clik on pattern title to see more details.') .'</div>';
    $info = array();
    $info[] = t('Author: ') . $pattern->info['author'];
    $info[] = t('Email: ') . $pattern->info['author_email'];
    $info[] = t('Web: ') . $pattern->info['author_website'];
    $author = theme('item_list', $info);
    $title .= '<div id="pid-'. $pid .'-info" class="pattern-info">'. $author . $pattern->description . $view_more .'</div>';
    $cells[] = array('data' => $title, 'class' => 'title');
    $cells[] = array('data' => $pattern->status ? t('Enabled') : t('Disabled'), 'class' => 'status');
    $cells[] = array('data' => $pattern->info['version'], 'class' => 'version');
    //     $cells[] = $pattern->public ?  t('Yes') : t('No');
    $cells[] = array('data' => $actions, 'class' => 'actions');
    $category = $pattern->info['category'] ? $pattern->info['category'] : t('Other');
    $rows[$category][] = $cells;
  }

  ksort($rows);
  foreach ($rows as $title => $category) {
    $fieldset = array(
      '#title' => t($title),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => theme('table', $header, $category, array('class' => 'patterns-list')),
    );
    $output .= theme('fieldset', $fieldset);
  }

  return $output;
}

/**
 * Menu callback to undo a patterns update changes
 */
// function patterns_revert($pid) {
//   if ($name = db_result(db_query('SELECT name FROM {patterns} WHERE pid = "%d"', $pid))) {
//     $path = file_create_path(variable_get('patterns_save_xml', 'patterns') .'/enabled/'. $name .'.xml');
//     $new = db_result(db_query('SELECT file FROM {patterns} WHERE pid = "%d"'));
//   }
//   else {
//     drupal_set_message(t('The pattern you specified does not exist.'), 'error');
//     drupal_goto('admin/build/patterns');
//   }
//
//   if (file_exists($path)) {
//     if (file_move($path, $new, FILE_EXISTS_REPLACE)) {
//       drupal_set_message(t('This pattern was reverted to the state it was at when it was enabled.'));
//       drupal_goto();
//     }
//   }
//   else {
//     drupal_set_message(t('The patterns enabled-state was not saved properly, therefore it cannot be reverted.'), 'error');
//   }
//
//   drupal_goto('admin/build/patterns');
// }

/**
 * Menu callback to display patterns details page
 */
// function patterns_info($pid = null) {
//   if (!is_numeric($pid)) {
//     drupal_set_message(t('You must specify a pattern.'));
//     return;
//   }
//
//   $pattern = patterns_get_pattern($pid);
//
//   $output = '';
//   return $output;
// }

/**
 * Menu callback to edit a patterns data
 */
function patterns_edit(&$form_state, $pid = null) {
  if (!is_numeric($pid)) {
    drupal_set_message(t('You must specify a pattern to edit.'));
    return;
  }

  $pattern = patterns_get_pattern($pid);

  // TODO: Turn php into xml here.

  // For now just allow modifying the original xml, which
  // means the modification cannot be further modified

  if (!$pattern->file) {
    drupal_set_message(t('This pattern does not seem to have an XML source file to base the modifications off of.'), 'error');
    return;
  }

  $xml = file_get_contents($pattern->file);

  $form['name'] = array(
    '#type' => 'value',
    '#value' => $pattern->name
  );
  $form['pid'] = array(
    '#type' => 'value',
    '#value' => $pattern->pid
  );
  //   if ($pattern->enabled <= $pattern->updated) {
  //     $form['revert'] = array(
  //       '#type' => 'markup',
  //       '#value' => l(t('Undo update changes to the state when you enabled the pattern.'), 'admin/build/patterns/revert/'. $pid, array(), drupal_get_destination())
  //     );
  //   }
  $form['format'] = array(
    '#type' => 'select',
    '#title' => t('Pattern syntax'),
    '#options' => array_combine(patterns_file_types(), patterns_file_types()),
    '#default_value' => 'xml'
  );
  $form['xml'] = array(
    '#type' => 'textarea',
    '#title' => t('Pattern\'s code'),
    '#rows' => 25,
    '#default_value' => $xml
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit')
  );

  return $form;
}

/**
 * Validate pattern modifications (make sure proper XML)
 */
function patterns_edit_validate($form, &$form_state) {
  // @TODO Do validations....
  $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));

  if (!file_check_directory($path, true)) {
    form_set_error('form_token', t('Unable to create @path to save the new pattern to.', array('@path' => $path)));
  }
}

/**
 * Submit edits to the pattern
 */
function patterns_edit_submit($form, &$form_state) {
  // If this is an enabled pattern, make sure the enabled pattern is saved in its current state
  if ($file = db_result(db_query('SELECT file FROM {patterns} WHERE status = 1 AND name = "%s"', $form_state['values']['name']))) {
    $dir = file_directory_path() .'/'. variable_get('patterns_save_xml', 'patterns') .'/enabled';
    file_check_directory($dir, true);
    $path =  $dir .'/'. $form_state['values']['name'] .'.'. $form_state['values']['format'];

    if (!file_exists($path)) {
      file_copy($file, $path, FILE_EXISTS_ERROR);
    }
  }

  // Save the new pattern into the pattern files dir.
  $path = file_directory_path() .'/'. variable_get('patterns_save_xml', 'patterns') .'/'. $form_state['values']['name'] .'.'. $form_state['values']['format'];

  file_save_data($form_state['values']['xml'], $path, FILE_EXISTS_REPLACE);

  $old = db_result(db_query('SELECT file FROM {patterns} WHERE name = "%s"', $form_state['values']['name']));

  // Load and save pattern
  $load_func = 'patterns_load_' .$form_state['values']['format']; 
  if ($pattern = $load_func($path)) {
    if ($old) {
      db_query('UPDATE {patterns} SET file = "%s", updated = "%s" WHERE pid = "%d"', $path, time(), $form_state['values']['pid']);
    }

    patterns_save_pattern($pattern, $path, $form_state['values']['name']);
    drupal_set_message(t('%name was saved.', array('%name' => $form_state['values']['name'])));
    $form_state['redirect'] = 'admin/build/patterns';
  }
  else {
    drupal_set_message(t("Pattern '%name' couldn't be saved. Make sure edited code is well-formed.", array('%name' => $form_state['values']['name'])), 'error');
  }
}

/**
 * List the modules used by a particular pattern
 */
function patterns_modules_page($pid) {
  $pattern = patterns_get_pattern($pid);

  drupal_set_title($pattern->title .' '. t('(Pattern Modules)'));

  $modules = isset($pattern->pattern['modules']) ? $pattern->pattern['modules'] : array();
  $modules_info = module_rebuild_cache();
  $modules_list = module_list();

  // Get module name, whether its to be disabled or enabled,
  // whether the module is available or not, and whether it is
  // currently enabled or not
  foreach($modules as $module) {
    $row = array();
    $delete = is_array($module) ? $module['delete'] : false;
    $module = is_array($module) ? $module['value'] : $module;
    $available = array_key_exists($module, $modules_info);
    $enabled = array_key_exists($module, $modules_list);
    $row[] = $module;
    $row[] = $available ? t('Yes') : '<span class="alert">'. t('No') .'</span>';
    $row[] = $enabled ? t('Yes') : '<span class="alert">'. t('No') .'</span>';
    $row[] = $delete ? t('Delete') : t('Enable');
    $rows[] = $row;

    if (!$available) {
      $not_available = true;
    }
  }

  if ($not_available) {
    drupal_set_message(t('Some modules are not availalble, please download them before running this pattern.'), 'error');
  }
  else {
    drupal_set_message(t('All modules required by this module are available. Click !here to run this pattern.', array('!here' => l(t('here'), 'admin/build/patterns/enable/'. $pid))));
  }

  return theme('table', array(t('Name'), t('Available'), t('Enabled'), t('Pattern Action')), $rows, t('Modules used for this pattern'));
}

function patterns_load_components() {
  static $loaded = false;

  if ($loaded) {
    return;
  }

  require_once drupal_get_path('module', 'patterns') .'/patterns.inc';

  // Get a list of directories to search
  $paths = module_invoke_all('patterns_directory');

  foreach($paths as $path) {
    foreach(file_scan_directory($path .'/components', '.\.inc$') as $file) {
      include_once $file->filename;
    }
  }

  $loaded = true;
}

function patterns_get_patterns($reset = true) {
  patterns_load_components();

  if ($reset || !variable_get('patterns_loaded', false)) {
    // Get a listing of enabled patterns
    $enabled = array();
    $result = db_query('SELECT file FROM {patterns} WHERE status = 1');

    while ($pattern = db_fetch_object($result)) {
      $enabled[] = $result->file;
    }

    $priority = array();
    $errors = array();
    
    // Get list of directories to scan for patterns
    $patterns_paths = patterns_paths();

    // get valid file extensions
    $mask = '.\.('. implode('|', patterns_file_types()) .')$';

    // prepare list of files/folders to be excluded
    // 'enabled' - Don't save enabled pattern backups
    $no_mask = array('.', '..', 'CVS', '.svn', 'enabled');

    foreach ($patterns_paths as $path) {
      foreach(file_scan_directory($path, $mask, $no_mask) as $file) {
        // Can't update existing patterns that are enabled
        if (in_array($file->filename, $enabled) || in_array($file->name, $priority)) {
          continue;
        }

        $priority[] = $file->name;

        // choose appropriate function based on the file extension
        $func = 'patterns_load_'. substr($file->basename, strlen($file->name) + 1);

        // Load and save pattern
        if ($pattern = $func($file->filename)) {
          patterns_save_pattern($pattern, $file->filename, $file->name);
        }
        else {
          $errors[] = $file->filename;
        }
      }
    }

    variable_set('patterns_loaded', time());
  }

  $result = db_query('SELECT * FROM {patterns}');

  $messages = array();
  while ($pattern = db_fetch_object($result)) {

    // skip pattern if its file is missing
    if (!is_file($pattern->file)) continue;

    // skip pattern if loading failed and report that to the user
    if (in_array($pattern->file, $errors)) {
      $messages[] = t("Pattern couldn't be loaded from the file '%file'", array('%file' => $pattern->file));
      continue;
    }

    $patterns[$pattern->pid] = $pattern;
    $data = unserialize($pattern->pattern);
    $patterns[$pattern->pid]->pattern = $data;
    $patterns[$pattern->pid]->info = $data['info'];
  }

  if (!empty($messages)) {
    drupal_set_message(implode('<br>', $messages) .'<br>'. t('Make sure that above file(s) are readable and contain valid data.'), 'error');
  }

  return $patterns;
}

/**
 * return a list of paths that will be scanned for patterns
 */
function patterns_paths() {
  $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));
  global $profile;

  if (!isset($profile)) {
    $profile = variable_get('install_profile', 'default');
  }

  // array of all the paths where we should look for patterns
  $patterns_paths = array(
    conf_path() .'/patterns',
    'profiles/'. $profile .'/patterns',
    'sites/all/patterns'
  );
  
  // allow any module to include patterns too
  foreach(module_invoke_all('patterns_directory') as $path) {
    if (is_dir($path)) {
      $patterns_paths[] = $path .'/patterns';
    }
  }

  // also prepend files folder if it's valid
  if (file_check_directory($path)) {
    array_unshift($patterns_paths, $path);
  }
  return $patterns_paths;
}

/**
 * Implementation of hook_patterns_directory()
 * 
 * Let us know about where the pattern files are at
 */
function patterns_patterns_directory() {
  return drupal_get_path('module', 'patterns');
}

function patterns_save_pattern($pattern, $path = '', $name = '') {

  $title = $pattern['info']['title'];
  $description = $pattern['info']['description'];
  $author = $pattern['info']['author'];

  if ($pid = db_result(db_query('SELECT pid FROM {patterns} WHERE name = "%s"', $name))) {
    $updated = db_result(db_query('SELECT updated FROM {patterns} WHERE pid = "%d"', $pid));
    if (($new_updated = filemtime($path)) > $updated) {
      db_query('UPDATE {patterns} SET pattern = "%s", title = "%s", file = "%s", updated = "%s", description = "%s" WHERE pid = %d', serialize($pattern), $title, $path, $new_updated, $description, $pid);
    }
    else {
      db_query('UPDATE {patterns} SET pattern = "%s", title = "%s", file = "%s", description = "%s" WHERE pid = %d', serialize($pattern), $title, $path, $description, $pid);
    }
  }
  else {
    db_query('INSERT INTO {patterns} (name, status, file, updated, enabled, title, description, pattern) VALUES ( "%s", 0, "%s", "%s", 0, "%s", "%s", "%s")', $name, $path, time(), $title, $description, serialize($pattern));
  }
}

function patterns_get_pattern($id) {
  if (is_numeric($id)) {
    $pattern = db_fetch_object(db_query('SELECT * FROM {patterns} WHERE pid = "%d"', $id));
  }
  else if (is_string($id)) {
    $pattern = db_fetch_object(db_query('SELECT * FROM {patterns} WHERE name = "%s"', $id));
  }

  if (!$pattern) return FALSE;

  // Get the actual data. Data is stored in serialized form in the db.
  $pattern->pattern = unserialize($pattern->pattern);

  return $pattern;
}

/**
 * Check if pattern array contains only allowed keys
 *
 * @param $pattern
 *   pattern array obtained by parsing pattern file
 * @return
 *   TRUE when only allowed array keys are found, FALSE otherwise
 *
 * @todo expand this function to include much more detailed validation
 */
function patterns_validate_pattern($pattern) {
  if (empty($pattern)) {
    return FALSE;
  }

  $allowed_keys = array('info', 'modules', 'actions');
  $diff = array_diff(array_keys($pattern), $allowed_keys);
  return empty($diff) ? TRUE : FALSE;
}

/**
 * Return file extensions supported by patterns module
 *
 * @return array of supported file types
 *
 * @todo convert this into pluggable system
 */
function patterns_file_types() {
  $result = array('xml', 'php');
  if (file_exists(drupal_get_path('module', 'patterns') .'/spyc/spyc.php')) {
    $result[] = 'yaml';
  }
  return $result;
}

function patterns_load_yaml($path, $local = TRUE) {
  if ($local && !file_exists($path)) {
    return FALSE;
  }

  include_once 'spyc/spyc.php';
  $pattern = Spyc::YAMLLoad($path);

  if (!patterns_validate_pattern($pattern)) {
    return FALSE;
  }

  return $pattern;
}

function patterns_load_string_yaml($source) {
  // loading yaml from source doesn't preserve line breaks
  // so we need to save it as a file first
  $path = file_directory_temp() .'/import.yaml';
  file_save_data($source, $path, FILE_EXISTS_REPLACE);
  $pattern = patterns_load_yaml($path);
  unlink($path);

  return $pattern;
}

function patterns_load_xml($path, $local = TRUE) {
  if ($local && !file_exists($path)) {
    return FALSE;
  }

  if (!$xml = file_get_contents($path)) {
    return FALSE;
  }

  return patterns_load_string_xml($xml);
}

function patterns_load_string_xml($source) {
  $pattern = patterns_from_source($source);
  if (empty($pattern) || $pattern['tag'] != 'pattern') {
    return FALSE;
  }

  // Rearrange the data in a nice way for each component.
  // Make sure actions are processed differently so order is preserved.
  $pattern = patterns_rearrange_data($pattern);

  foreach($pattern as $key => $values) {
    $pattern[$values['tag']] = $values;
    unset($pattern[$values['tag']]['tag']);
    unset($pattern[$key]);
  }

  if (!patterns_validate_pattern($pattern)) {
    return FALSE;
  }

  return $pattern;
}

/**
 * Read and evaluate a php file to return a 'pattern'
 */
function patterns_load_php($path, $local = TRUE) {
  if ($local && !file_exists($path)) {
    return FALSE;
  }
  $pattern = array();

  @include($path);
  // That should have declared a 'pattern' into current scope.

  if (!patterns_validate_pattern($pattern)) {
    trigger_error("Failed to evaluate a useful pattern from the input file $path. Pattern did not validate. May have been invalid syntax. ", E_USER_WARNING);
    return FALSE;
  }
  return $pattern;
}

/**
 * Create a pattern from an XML data source
 */
function patterns_from_source($xml) {
  $parse = drupal_xml_parser_create($xml);

  if (!xml_parse_into_struct($parse, $xml, $vals, $index)) {
    return false;
  }

  // Create a multi-dimensional array representing the XML structure
  $pattern = current(_patterns_parse_tag($vals));

  return $pattern;
}

/**
 * Recurse through the values of a parsed xml file to create a
 * multi-dimensional representation of the data.
 */
function _patterns_parse_tag($data, &$index = 0) {
  $pattern = array();

  while ($current = $data[$index]) {
    $type = $current['type'];

    foreach((array)$current['attributes'] as $key => $value) {
      $current[strtolower($key)] = $value;
    }

    $current['tag'] = strtolower($current['tag']);

    unset($current['type'], $current['level'], $current['attributes']);

    if (!trim($current['value']) && $current['value'] != "0") {
      unset($current['value']);
    }

    switch($type) {
      case 'open':
        $index++;
        $current += _patterns_parse_tag($data, $index);
        $pattern[] = $current;
        break;
      case 'close':
        $index++;
        return $pattern;
        break;
      case 'complete':
        // In order to support more complex/non-standard features we can use serialized data
        if ($current['attributes']['serialized']) {
          $value = unserialize($current['value']);

          if (isset($value)) {
            $current['value'] = $value;
          }
        }

        // If no value was specified, make sure an empty value is there
        if (!isset($current['value'])) {
          $current['value'] = '';
        }

        $pattern[] = $current;
        break;
    }

    $index++;
  }

  return $pattern;
}

// function patterns_disable_pattern($pid) {
//     $form['pid'] = array(
//     '#type' => 'value',
//     '#value' => $pid
//   );
//
//   $pattern = patterns_get_pattern($pid);
//
//   return confirm_form($form, t('Proceed with disabling pattern %pattern?', array('%pattern' => $pattern->title)), 'admin/build/patterns', '');
// }

function patterns_enable_pattern(&$form_state, $pid) {
  $form['pid'] = array(
    '#type' => 'value',
    '#value' => $pid
  );

  $disclaimer = t('Please be sure to backup your site before running a pattern. Patterns are not guaranteed to be reversable in case they do not execute well or if unforseen side effects occur.');

  $pattern = patterns_get_pattern($pid);

  return confirm_form($form, t('Proceed with running pattern %pattern?', array('%pattern' => $pattern->title)), 'admin/build/patterns', $disclaimer);
}

// function patterns_disable_pattern_submit($form_id, $form_values) {
//   $pid = $form_values['pid'];
//   $pattern = patterns_get_pattern($pid);
//
//   if (patterns_execute_pattern($pattern, true, true)) {
//     return 'admin/build/patterns';
//   }
// }

function patterns_enable_pattern_submit($form, &$form_state) {
  $pid = $form_state['values']['pid'];

  patterns_load_components();
  $pattern = patterns_get_pattern($pid);

  patterns_execute_pattern($pattern, $form_state['values']);

  $form_state['redirect'] = 'admin/build/patterns';
}

/**
 * Execute default configuration for module during the module installation
 *
 * This function should be called by other modules from
 * within their hook_enable() implementation.
 * Module should also provide modulename.config file containing PHP array
 * with the actions that need to be executed.
 *
 * @param $module
 *   name of the module calling the function
 */
function patterns_execute_config($module) {

  // since this function is called from hook_enable(), we need to ensure that
  // it's executed only at module installation (first time module is enabled)
  if (drupal_get_installed_schema_version($module) == SCHEMA_INSTALLED) return;

  $path = drupal_get_path('module', $module) .'/'. $module .'.config';

  if (file_exists($path)) {
    include_once($path);

    if (empty($actions)) return;

    $pattern = new stdClass();
    $pattern->title = t('Default configuration for @module module', array('@module' => $module));
    $pattern->status = false;
    $pattern->pattern['actions'] = $actions;

    patterns_execute_pattern($pattern);
  }
}

function patterns_execute_pattern($pattern, $params = array(), $mode = 'batch') {

  $args = array($pattern, $params);
  $function = 'patterns_execute_pattern_'. $mode;

  if (!function_exists($function) || !is_object($pattern)) return FALSE;

  return call_user_func_array($function, $args);
}

function patterns_execute_pattern_batch($pattern, $params = array()) {

  set_time_limit(0);

  if (!is_object($pattern)) {
    $pattern = patterns_get_pattern($pattern);

    if (!$pattern) {
      return FALSE;
    }
  }

  $pattern_details = patterns_get_pattern_details($pattern, TRUE);

  $modules = $pattern_details['modules'];
  $actions = $pattern_details['actions'];
  $actions_map = array('patterns' => $pattern_details['info'], 'map' => $pattern_details['actions_map']);
  $info = reset($pattern_details['info']);

  // If there are no actions or modules, most likely the pattern
  // was not created correctly.
  if (empty($actions) && empty($modules)) {
    drupal_set_message(t('Could not recognize pattern %title, aborting.', array('%title' => $info['title'])), 'error');
    return FALSE;
  }

  $result = patterns_install_modules($modules);
  if (!$result['success']) {
    drupal_set_message($result['error_message'], 'error');
    return FALSE;
  }

  $result = patterns_prepare_actions($actions, $actions_map);
  if (!$result['success']) {
    drupal_set_message($result['error_message'], 'error');
    return FALSE;
  }

  $batch = array(
    'title' => t('Processing pattern %pattern', array('%pattern' => $info['title'])),
//    'init_message' => t('Running action @current out of @total', array('@current' => 1, '@total' => count($actions))),
    'progress_message' => t('Running action @current out of @total'),
    'operations' => array(),
    'finished' => 'patterns_batch_finish'
  );

  for($i=0;$i<count($actions);$i++) {
    $batch['operations'][] = array('patterns_batch_actions', array($actions[$i], $i, $actions_map));
  }

  $_SESSION['patterns_batch_info'] = $pattern_details['info'];

  batch_set($batch);
  return TRUE;
}

function patterns_install_modules(&$modules) {
  $result = array('success' => TRUE);

  if (empty($modules)) return $result;

  $missing = patterns_check_module_dependencies($modules, TRUE);
  if (!empty($missing)) {
    $result['success'] = FALSE;
    $result['error_message'] = t('Following required modules are missing: %modules', array('%modules' => implode(', ', $missing)));
    $result['missing_modules'] = $missing;
    return $result;
  }

  require_once './includes/install.inc';
  drupal_install_modules($modules);
  module_rebuild_cache();

  $result['installed_modules'] = $modules;

  return $result;
}

function patterns_locate_action($key, $actions_map) {
  $result['key'] = $actions_map['map'][$key]['index'];
  $result['title'] = $actions_map['patterns'][$actions_map['map'][$key]['pid']]['title'];
  $result['file'] = $actions_map['patterns'][$actions_map['map'][$key]['pid']]['file'];
  return $result;
}

function patterns_prepare_actions(&$actions, $actions_map) {

  $result = array('success' => TRUE);

  if (empty($actions)) return $result;

  patterns_load_components();

  // Keep a list of what modules handle what tags
  $tag_modules = patterns_invoke($empty, 'tag modules');

  // TODO Finish basic setup and execution of the 'config' operation
  // TODO Make a better streamlined config framework process. For instance collect form_ids
  // from here and give the form_id and matching data to the 'build' process
  foreach($actions as $key => &$data) {
    if (($config = patterns_invoke($actions[$key], 'config')) && !empty($config)) {
      patterns_config_data($data, $config);
    }
  }

  // Prepare actions for validation/processing
  foreach($actions as $key => &$data) {
    patterns_invoke($actions[$key], 'prepare');
  }

  // Pre validate tags with their appropriate components
  foreach($actions as $key => &$data) {

    $action_location = patterns_locate_action($key, $actions_map);
    $index = $action_location['key'];
    $pattern_title = $action_location['title'];
//    $pattern_file = $action_location['file'];

    if (!array_key_exists($data['tag'], $tag_modules)) {
      $errors[] = t('Action #%num (%tag) in pattern %title: <%tag> is not a valid tag', array('%num' => $index+1, '%tag' => $data['tag'], '%title' => $pattern_title));
    }
    else {
      $error = patterns_invoke($actions[$key], 'pre-validate');
      if ($error) {
        $errors[] = t('Action #%num (%tag) in pattern %title: !msg', array('!msg' => $error, '%num' => $index+1, '%tag' => $data['tag'], '%title' => $pattern_title));
      }
    }
  }

  if (count($errors)) {
    $message = t('Errors encountered during pre-processing:') .'<br>'. implode('<br>', $errors);
    $result['success'] = FALSE;
    $result['error_message'] = $message;
  }

  return $result;
}

/**
 * Execute a batch action
 */
function patterns_batch_actions($action, $place, $actions_map, &$context) {

  patterns_load_components();

  // Nothing to do if there is no action
  if (empty($action)) {
    $context['finished'] = 1;
    return;
  }

  // Start a timer. Since we want each action to be its own http request, we need
  // to ensure the batch api will decide to do it like that by making each action
  // take at least a second to execute
  timer_start('patterns_action');

  // skip action execution if an error is encountered in some of the previous operations
  if (!empty($context['results']['abort'])) return;

  $result = patterns_implement_action($action, $context['results']['identifiers'], $place, $actions_map);

  if (!$result['success']) {
    // we use 'results' to keep track of errors and abort execution if required
    $context['results']['abort'] = TRUE;
    $context['results']['error_message'] = $result['error_message'];
  }

  if (timer_read('patterns_action') < 1000) {
    @usleep(1000 - timer_read('patterns_action'));
  }
}

/**
 * Finish a batch operation
 */
function patterns_batch_finish($success, $results, $operations) {
  $info = $_SESSION['patterns_batch_info'];
  if (!($results['abort'])) {
    foreach ($info as $key => $i) {
      drupal_set_message(t('Pattern "@pattern" ran successfully.', array('@pattern' => $i['title'])));
      db_query('UPDATE {patterns} SET status = 1, enabled = "%s" WHERE pid = %d', time(), $key);
    }
  }
  else {
    $pattern = reset($info);
    drupal_set_message(t('Pattern "@pattern" ran with the errors. Check the error messages to get more details.', array('@pattern' => $pattern['title'])));
    drupal_set_message($results['error_message'], 'error');
  }
  unset($_SESSION['patterns_batch_info']);
  drupal_flush_all_caches();
}

/**
 * Setup and run an action
 */
function patterns_implement_action($action, &$identifiers, $place = 0, $actions_map = NULL) {

  patterns_set_error_handler();

  $result = array('success' => TRUE);

  // Prepare actions for processing, ensure smooth pattern executions, and return form ids for execution
  $return = patterns_invoke($action, 'form_id');

  // If prepare removed the data, dont continue with this action
  if (!$action || !$return) {
    return $result;
  }

  if (is_string($return)) {
    $form_ids = array($return);
  }
  else if ($return) {
    $form_ids = $return;
  }

  $action_descriptions = patterns_invoke($action, 'actions');
  $action_location = patterns_locate_action($place, $actions_map);
  $index = $action_location['key'] + 1;
  $pattern_title = $action_location['title'];
  $pattern_file = $action_location['file'];

  // Build the action
  foreach($form_ids as $form_id) {
    $clone = $action;

    $action_description = isset($action_descriptions[$form_id]) ? $action_descriptions[$form_id] : t('System: Execute form');

    $result['action_descriptions'][$place][] = $action_description;

    // If tokens are enabled, apply tokens to the action values
    // before processing
    if (module_exists('token')) {
      _patterns_recurse_tokens($clone, $identifiers);
      //array_walk($clone, '_patterns_replace_tokens', $identifiers);
    }

    $error = patterns_invoke($clone, 'validate', $form_id);
    if ($message = patterns_error_get_last('validate', $index, $action_description, $pattern_title, $pattern_file)) {
      $result['error_message'] = $message;
      $result['success'] = FALSE;
      return $result;
    }

    if ($error) {
      $message = t('An error occured while validating action #%num (%action) in %title pattern', array('%num' => $index, '%action' => $action_description, '%title' => $pattern_title));
      $result['error_message'] = $message .'<br>'. $error;
      $result['success'] = FALSE;
      return $result;
    }

    // Get the form data for the action. This can either just be the form values,
    // or it can be the full form_state object
    $form_obj = patterns_invoke($clone, 'build', $form_id);

    if ($message = patterns_error_get_last('build', $index, $action_description, $pattern_title, $pattern_file)) {
      $result['error_message'] = $message;
      $result['success'] = FALSE;
      return $result;
    }

    // Dont execute the action if a string was returned, indicating the pattern component
    // most likely handled the action on its own and this is the message to display.
    if (is_string($form_obj)) {
      drupal_set_message($form_obj);
    }
    else {
      // We check for the 'storage' and 'submitted' values in the object to see
      // if it is a form_state instead of form_values. There could be a better way
      // to do this.
      if (array_key_exists('submitted', (array)$form_obj) && array_key_exists('storage', (array)$form_obj)) {
        $action_state = $form_obj;
      }
      else {
        $action_state = array(
          'storage' => null,
          'submitted' => false,
          'values' => $form_obj
        );
      }

      // Get any extra parameters required for the action
      $params = patterns_invoke($clone, 'params', $form_id, $action_state);
      if ($message = patterns_error_get_last('params', $index, $action_description, $pattern_title, $pattern_file)) {
        $result['error_message'] = $message;
        $result['success'] = FALSE;
        return $result;
      }

      // A single, simple value can be returned as a parameter, which is then
      // put into an array here.
      if (isset($params) && !is_array($params)) {
        $params = array($params);
      }

      // Execute action
      patterns_execute_action($form_id, $action_state, $params);

      if ($message = patterns_error_get_last('execute', $index, $action_description, $pattern_title, $pattern_file)) {
        $result['error_message'] = $message;
        $result['success'] = FALSE;
        return $result;
      }

      if ($errors = form_get_errors()) {
        $result['error_message'] = t('Above error(s) occured while executing action #%num (%action) in %title pattern. Error location(s) are: %errors', array('%num' => $index, '%action' => $action_description, '%title' => $pattern_title, '%errors' => str_replace('][', '->', implode(', ', array_keys($errors)))));
        $result['success'] = FALSE;
        return $result;
      }

      // Let a component cleanup after each action
      patterns_invoke($clone, 'cleanup', $form_id, $action_state);
      if ($message = patterns_error_get_last('cleanup', $index,  $action_description, $pattern_title, $pattern_file)) {
          $result['error_message'] = $message;
          $result['success'] = FALSE;
          return $result;
      }
    }

    // Clear the cache in case it causes problems
    cache_clear_all();

    // Rebuild the menu
    // TODO Should this go at the end only when a pattern successfully runs?
    variable_set('menu_rebuild_needed', true);
  }

  // Get any primary identifiers from the action for further actions to take advantage of
  $id = null;
  $id = patterns_invoke($clone, 'identifier', $form_id, $action_state);
  if (isset($id)) {
    $index = isset($clone['action_label']) ? $clone['action_label'] : $place+1;
    $identifiers[$index] = $id;
  }

  patterns_restore_error_handler();

  return $result;
}

/**
 * Execute an action
 */
function patterns_execute_action($form_id, &$form_state, $params) {
  // Make sure we always have a clear cache for everything
  // Beware - this direct database access needs to be db-prefix-safe!
  global $db_prefix;
  $result = db_query('SHOW TABLES LIKE "{cache}_%"');

  while ($table = db_fetch_array($result)) {
    // Remove the db prefix if any. cache_clear_all() will put it back again.
    $table = substr(current($table), strlen($db_prefix));
    cache_clear_all(null, $table);
  }

  $args = array($form_id, &$form_state);

  if (is_array($params)) {
    $args = array_merge($args, $params);
  }

  patterns_executing(true);

  // If we are in batch mode, trick the form api to think
  // otherwise to avoid potential problems
  $batch =& batch_get();
  $batch_clone = $batch;
  $batch = null;

  //$form = call_user_func_array('drupal_retrieve_form', $args);
  //$form['#post'] = $values;
  //$return = drupal_process_form($form_id, $form);
  //dpm($args);

  // drupal_execute fails to keep $form_state in-sync through the
  // whole FAPI process. Issue http://drupal.org/node/346244
  //$return = call_user_func_array('drupal_execute', $args);

  // Fix some parts of the #post values based on the original form
  patterns_sync_form_values($args);

  // Copy of drupal_execute until above issue is fixed
  $form = call_user_func_array('drupal_retrieve_form', $args);

  $form['#post'] = $form_state['values'];
  drupal_prepare_form($form_id, $form, $form_state);

  // If you call drupal_validate_form() on the same form more
  // than once per page request, validation is not performed
  // on any but the first call.
  // see issue: http://drupal.org/node/260934
  // drupal_process_form($form_id, $form, $form_state);

  // Until above issue is fixed we use our own implementation
  // of drupal_process_form() and drupal_validate_form().
  _patterns_process_form($form_id, $form, $form_state);

  patterns_executing(false);
  $batch = $batch_clone;

  return $return;
}

function patterns_executing($b = null) {
  static $executing = false;

  if (is_bool($b)) {
    $executing = $b;
  }

  return $executing;
}

function patterns_rearrange_data($pattern) {
  foreach($pattern as $key => $value) {
    if (is_string($key)) {
      unset($pattern[$key]);
    }
    else {
      if ($value['tag'] == 'actions') {
        $pattern[$key] = patterns_rearrange_data($value);
        $pattern[$key]['tag'] = 'actions';
      }
      else {
        $pattern[$key] = _patterns_rearrange_data($value);
      }
    }
  }

  return $pattern;
}

/**
 * Return an array with detailed information about the pattern
 */
function patterns_get_pattern_details($pattern, $recursive = FALSE, &$pids = array()) {

  // prevent infinite recursion
  if (in_array($pattern->pid, $pids)) return array();
  $pids[] = $pattern->pid;

  $actions = !empty($pattern->pattern['actions']) ? $pattern->pattern['actions'] : array();
  $modules = !empty($pattern->pattern['modules']) ? $pattern->pattern['modules'] : array();

  $patterns[$pattern->pid] = (array)$pattern;
  $patterns[$pattern->pid] = array_merge($patterns[$pattern->pid], $patterns[$pattern->pid]['pattern']['info']);
  unset($patterns[$pattern->pid]['pattern']);

  if ($recursive) {
    $result = array('modules' => $modules, 'info' => $patterns);

    foreach($actions as $key => $action) {
      if ($action['tag'] == 'pattern') {
        // determine pattern name
        if (!empty($action['value'])) {
          $name = $action['value'];
        }
        elseif (!empty($action['name'])) {
          $name = $action['name'];
        }
        if (!$p = patterns_get_pattern($name)) {
          // just give a warning and try to continue
          drupal_set_message(t('Action #%key in %file: Pattern %pattern not found.<br>Pattern execution will try to continue without it.', array('%key' => $key+1, '%file' => $pattern->title, '%pattern' => $name)), 'warning');
          continue;
        }
        $a = patterns_get_pattern_details($p, TRUE, $pids);
        // array_merge doesn't preserve numeric array keys
        // so we handle 'info' separately
        $info = $result['info'];
        $result = array_merge_recursive($result, $a);
        $result['info'] = $info + $a['info'];
      }
      else {
        $result['actions'][] = $action;
        $result['actions_map'][] = array(
          'pid'   => $pattern->pid,
          'index' => $key,
        );
      }
    }
    $result['modules'] = array_merge(array_unique($result['modules']));
    return $result;
  }
  return array('actions' => $actions, 'modules' => $modules, 'info' => $patterns);
}

//function patterns_process_modules($modules, $op = 'enable') {
//  // Enable at the beginning of the pattern. Disable at the end.
//  for($i=0;$module=$modules[$i];$i++) {
//    if (($op == 'enable' && $module['delete']) || ($op == 'disable' && !$module['delete'])) {
//      unset($modules[$i]);
//    }
//  }
//
//  patterns_invoke($empty, 'tag modules');
//  $error = patterns_invoke($modules, 'pre-validate');
//
//  // Error validating modules
//  if ($error) {
//    return $error;
//  }
//
//  patterns_invoke($modules, 'prepare');
//}

function patterns_invoke(&$data, $op, $form_id = null, &$a = null) {
  static $tag_modules;

  if (!is_array($tag_modules) || $op == 'tag modules') {
    // Get a list of tags and their modules
    foreach(module_implements('patterns') as $module) {
      $tags = module_invoke($module, 'patterns', 'tags');

      foreach($tags as $tag => $value) {
        if (is_array($value)) {
          $tag_modules[$tag] = $module;
        }
        else {
          $tag_modules[$value] = $module;
        }
      }
    }
  }

  // If you just want the modules list
  if ($op == 'tag modules') {
    return $tag_modules;
  }

  $tag = $data['tag'];
  unset($data['tag']);

  $module = $tag_modules[$tag];
  $func = $module .'_patterns';

  if (function_exists($func)) {
    if ($form_id) {
      $return = $func($op, $form_id, $data, $a);
    }
    else {
      $return = $func($op, $tag, $data, $a);
    }
  }

  $data['tag'] = $tag;
  return $return;
}

function _patterns_rearrange_data($data, $parent = '') {
  $numeric = array();
  $count=0;

  foreach($data as $key => $value) {
    if ($value['value'] == 'false') {
      $value['value'] = false;
    }
    else if ($value['value'] == 'true') {
      $value['value'] = true;
    }

    if (is_numeric($key) && is_array($value) && count($value) == 2 && isset($value['tag']) && isset($value['value'])) {
      unset($data[$key]);
      if (isset($data[$value['tag']])) {
        $numeric[] = $value['tag'];
        $data[$count++] = $data[$value['tag']];
        $data[$count++] = $value['value'];
        unset($data[$value['tag']]);
      }
      else if (in_array($value['tag'], $numeric)) {
        $data[$count++] = $value['value'];
      }
      else {
        $data[$value['tag']] = $value['value'];
      }
    }
    else if (is_numeric($key)) {
      $tag = $value['tag'];
      unset($value['tag']);
      $data[$tag][] = _patterns_rearrange_data($value, $tag);
      unset($data[$key]);
    }
  }

  foreach($data as $key => $value) {
    if (is_array($value) && count($value) == 1 && $value[0]) {
      $data[$key] = $data[$key][0];
    }
  }

  return $data;
}


function patterns_form_alter(&$form, &$form_state, $form_id) {

  if (user_access('administer patterns') && variable_get('patterns_form_helper', FALSE)) {
    $form['#after_build'][] = 'patterns_form_helper';
  }

  if (patterns_executing()) {
    // Ensure that parent and related dropdowns display all the terms
    // including those created during current pattern execution.
    // Without this, those terms would be omitted due to the static
    // caching within taxonomy_get_tree().
    $form_ids = array('taxonomy_form_term');
    if (in_array($form_id, $form_ids)) {
      $tid = $form['#term']['tid'];
      $vid = $form['#vocabulary']['vid'];
      $parent = array_keys(taxonomy_get_parents($tid));

      $children = _patterns_taxonomy_get_tree($vid, $tid);
      // A term can't be the child of itself, nor of its children.
      foreach ($children as $child) {
        $exclude[] = $child->tid;
      }
      $exclude[] = $tid;

      $form['advanced']['parent'] = _patterns_taxonomy_term_select(t('Parents'), 'parent', $parent, $vid, t('Parent terms') .'.', 1, '<'. t('root') .'>', $exclude);
      $form['advanced']['relations'] = _patterns_taxonomy_term_select(t('Related terms'), 'relations', array_keys(taxonomy_get_related($tid)), $vid, NULL, 1, '<'. t('none') .'>', array($tid));
    }

    // Ensure that parent item dropdown displays all the menu items
    // including those created during current pattern execution.
    // Without this, those menu items would be omitted due to the
    // static caching within menu_tree_all_data().
    $form_ids = array('menu_edit_item');
    if (in_array($form_id, $form_ids)) {
      if (empty($form['menu']['#item'])) {
        $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $form_state['values']['menu']['menu_name'], 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
      }
      else {
        $item = $form['menu']['#item'];
      }
      $form['menu']['parent']['#options'] = _patterns_menu_parent_options(menu_get_menus(), $item);
    }

    // Ensure that taxonomy dropdowns on node edit form display all the terms
    // including those created during current pattern execution.
    if (!empty($form['taxonomy']) && isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
      foreach ($form['taxonomy'] as $vid => $v) {
        if (!is_numeric($vid)) continue;
        $form['taxonomy'][$vid] = _patterns_taxonomy_form($vid, $form['taxonomy'][$vid]['#default_value'], $form['taxonomy'][$vid]['#description']);
      }
    }
  }

}

function patterns_form_helper($form, $form_state) {
  static $form_id;

  if (!$form_id && $form_state['submitted'] && !form_get_errors()) {
    $form_id = $form_state['values']['form_id'];
    $_SESSION['patterns_form_helper'] = array('form_id' => $form_id, 'form_values' => $form_state['values']);
  }

  return $form;
}

function patterns_exit($destination = null) {
  $batch =& batch_get();
  if (variable_get('patterns_form_helper', FALSE) && $_SESSION['patterns_form_helper'] && !$destination && empty($batch)) {
    if (module_exists('devel')) {
      //dpm($_SESSION['patterns_form_helper']);
      kprint_r($_SESSION['patterns_form_helper']);
    }
    else {
      print theme('patterns_form_helper', $_SESSION['patterns_form_helper']['form_id'], $_SESSION['patterns_form_helper']['form_values']);
    }

  }
}

/**
 * Implementation of hook_token_values()
 *
 * @If these get implementated directly into token.module, this should be removed
 */
function patterns_token_values($type, $object = NULL, $options = array()) {
  if ($type == 'global') {
    $path = conf_path();
    $tokens['confpath'] = $path;
    return $tokens;
  }
}

/**
 * Function callback
 */
function _patterns_modify_value(&$form) {
  foreach($form as $key => $value) {
    if (is_array($value) && isset($value['#type']) && $value['#type'] == 'value') {
      $form[$key]['#default_value'] = $value['#value'];
      unset($form[$key]['#value']);
    }
    else if (is_array($value)) {
      _patterns_modify_value($form[$key]);
    }
  }
}

/**
 * Recurse an array and replace with tokens
 * @ This is used instead of array_walk_recursive because
 *   of some strange issues with token_get_values failing.
 */
function _patterns_recurse_tokens(&$object, $identifiers) {
  foreach($object as $key => $value) {
    if (is_array($value)) {
      _patterns_recurse_tokens($object[$key], $identifiers);
    }
    else if (is_scalar($value)) {
      $old_key = $key;
      _patterns_replace_tokens($object[$key], $key, $identifiers);

      // The key was changed, change it
      if ($old_key != $key) {
        $keys = array_keys($object);
        $keys[array_search($old_key, $keys)] = $key;
        $object = array_combine($keys, array_values($object));
      }
    }
  }
}

/**
 * Array walk callback to replace tokens inside form values
 */
function _patterns_replace_tokens(&$a, &$b, $identifiers = array()) {
  static $count = 0;

  // Replace IDs with identifiers from the current executing pattern
  if (preg_match('/@([a-zA-Z0-9_]+)@/', $a, $match)) {
    $a = str_replace($match[0], $identifiers[$match[1]], $a);
  }
  if (preg_match('/__([a-zA-Z0-9_]+)__/', $b, $match)) {
    $b = str_replace($match[0], $identifiers[$match[1]], $a);
  }

  // Replace tokens
  $a = token_replace($a, 'global', NULL, '@', '@');
  $b = token_replace($b, 'global', NULL, '__', '__');
}

/**
 * Check if a .htaccess file exists to prevent downloads of pattern files
 */
function _patterns_check_file_dir() {
  return false;
  $path = file_create_path(variable_get('patterns_save_xml', 'patterns'));

  if (!is_file($path .'/.htaccess')) {
    $content = '# Prevent downloading site patterns
<FilesMatch "\.xml$">
  Order allow,deny
</FilesMatch>
';
    file_save_data($content, $path .'/.htaccess');
  }
}

function theme_patterns_form_helper_menu($forms) {
  $output = '<ul class="patterns-form-menu">';

  foreach ($forms as $form_id => $values) {
    $output .= '<li class="patterns-form-menu-item">'. $form_id .'</li>';
  }

  $output .= '</li>';

  return $output;
}

function theme_patterns_form_helper($form_id, $values) {

  $output = '<div class="patterns-form" id="patterns-form-'. $form_id .'">';

  $output .= '<div class="patterns-form-title">'. t('Form values for %key', array('%key' => $form_id)) .'</div>';

  foreach($values as $key => $value) {
    $output .= '<div class="patterns-form-item"><div class="patterns-form-key">'. $key .' => </div>';
    $output .= '<div class="patterns-form-value">'. print_r($value, true) .'</div></div>';
  }

  $output .= '</div>';

  return $output;
}

/**
 * Implementation of hook_theme().
 */
function patterns_theme() {
  return array(
    'patterns_form_helper' => array(
      'arguments' => array('form_id' => NULL, 'values' => NULL),
  ),
    'patterns_form_helper_menu' => array(
      'forms' => NULL
  )
  );
}

/**
 * Make some modifications to the form values based on the form
 * In particular, make sure form elements with #options and #multiple
 * set the keys of the array as the key of the value as how FAPI does it,
 * but XML of course does not.
 */
function patterns_sync_form_values($args) {
  $form_id = $args[0];
  // References inside the form_state can cause potential problems,
  // so we'll ensure no references to outside data exists
  $form_state = unserialize(serialize($args[1]));
  $form_values = $form_state['values'];
  unset($args[1]['values']);

  // Get the fully built fapi object with cloned form_state.
  // We need to do this with a separate form_state because
  // this can mess up the form values if they are setup incorrectly
  // (it'll get fixed here)
  $form = call_user_func_array('drupal_retrieve_form', $args);
  drupal_prepare_form($form_id, $form, $form_state);
  $form = form_builder($form_id, $form, $form_state);

  // Loop through all form values looking for #options
  $queue = array(&$form);

  while (!empty($queue)) {
    $check = &$queue[0];
    array_shift($queue);

    // Skip disabled items
    if ($check['#disabled']) {
      continue;
    }

    // Set default values for everything that the form is expecting values for.
    // This can save work in the components as well as avoid bugs when a module
    // expects values to be there that are not.
    // Do not set default values when it is the type of value that a form submit
    // would not set data in $_POST for. Like selects with no options or unchecked
    // checkboxes
    if ($check['#input'] && isset($check['#default_value']) &&
        !($check['#type'] == 'select' && empty($check['#options'])) &&
        !(in_array($check['#type'], array('checkbox', 'checkboxes')) && empty($check['#default_value']))) {
      $match = &$form_values;
      $found = true;
      foreach($check['#parents'] as $path) {
        if (!isset($match[$path])) {
          $found = false;
          $match[$path] = array();
        }
        // If this is not an array, the data was probably not completed properly
        // in the pattern.
        if (!is_array($match)) {
          $message = t('Invalid pattern syntax at: !path. Expecting more child elements.', array('!path' => implode('->', $check['#parents'])));
          patterns_error_handler(1, $message, '', '');
          return;
        }
        $match = &$match[$path];
      }

      // Set the default value unless it is a type of value that would originally
      // not be set in $_POST during form submit, like empty selects
      if (!$found) {
        $check['#missing_in_pattern'] = true;
        $match = $check['#default_value'];
      }
    }

    if (!empty($check['#options'])) {
      $scalar = false;
      if ((!$check['#tree'] && !$check['#multiple']) || (isset($check['#multiple']) && !$check['#multiple']) || in_array($check['#type'], array('radio', 'radios'))) {
        $scalar = true;
      }

      // Find possible corresponding data in form values
      $match = &$form_values;
      $found = true;
      foreach($check['#parents'] as $path) {
        if (isset($match[$path])) {
          $match = &$match[$path];
        }
        else {
          $found = false;
          break;
        }
      }

      if ($found && isset($match)) {
        // Check if we are overwriting or not.
        //
        // First, if the pattern value is there, but empty, we obviously
        // should be overwriting. Next is to check if overwrite was specified
        // manually
        if (!$check['#missing_in_pattern'] && (empty($match) || (is_array($match) && isset($match['_overwrite_'])))) {
          $overwrite = true;
          if (is_array($match) && isset($match['_overwrite_'])) {
            $match = $match['_overwrite_'];
          }
        }

        // If overwriting, start the values array with all the
        // possible values but empty. We will possibly fill them
        // in with default values on update actions. Select types
        // do not work when empty, non-selected, values are set
        if (!$scalar && $overwrite && $check['#type'] != 'select') {
          $values = $check['#options'];
          foreach($values as &$v) {
            $v = '';
          }
        }
        else {
          $values = array();
        }

        if (!$overwrite && !empty($check['#default_value'])) {
          foreach((array)$check['#default_value'] as $value) {
            $values[$value] = $value;
          }

          // Make sure we are still restricted to values available in #options
          $values = array_intersect_key($values, $check['#options']);
        }

        // Flatten out the options in case it is trying to use optgroups
        $options = patterns_options_flatten($check['#options']);

        foreach((array)$match as $value) {
          $key = array_search($value, $options);

          // If the supplied value is an actual options value (not the label) use that
          if (array_key_exists($value, $options)) {
            $values[$value] = $value;
          }
          // Or if the supplied value matches an options label, use that option
          else if ($key !== FALSE) {
            $values[$key] = $key;
          }
          // If we are trying to set an invalid value, at least select
          // the first value for select forms since it is impossible to
          // not select a good value there
          else if ($check['#type'] == 'select') {
            reset($options);
            $values[key($options)] = key($options);
          }
        }

        // If we are not on a multiple/tree form, the value should be singular/not an array.
        // We make sure to use the last value here because the last value will absolutely
        // be the user/pattern supplied value as opposed to the default_value
        if ($scalar) {
          $match = end($values);
        }
        else {
          $match = $values;
        }
      }
    }

    foreach(element_children($check) as $element) {
      $queue[] = &$check[$element];
    }
  }

  // Make sure the new form_values will be used in the real executions
  $args[1]['values'] = $form_values;
}

/**
 * Helper function to flatter options, but keep the title/names in
 */
function patterns_options_flatten($array, $reset = TRUE) {
  static $return;

  if ($reset) {
    $return = array();
  }

  foreach ($array as $key => $value) {
    if (is_object($value)) {
      patterns_options_flatten($value->option, FALSE);
    }
    else if (is_array($value)) {
      patterns_options_flatten($value, FALSE);
    }
    else {
      $return[$key] = $value;
    }
  }

  return $return;
}

/**
 * Take a $data and $config object and adjust $data
 * based on the supplied configuration
 *
 * TODO Look for ways to optimize and increase performance
 */
function patterns_config_data(&$data, $configs) {
  foreach($configs as $path => $config) {
    // An alias key requires multiple paths
    // for each alias and then a #real set
    // to change all of them to the same key
    // TODO test #alias
    if ($config['#alias'] && is_array($config['#alias'])) {
      $tokens = array_slice(preg_split('/(?<!=)\/(?![a-z]*\])/', $path), 1);
      $real = end($tokens);

      // Check for the real key
      $matches = patterns_array_fetch($path, $data);

      // If the real key was not found, get the aliases
      if (empty($matches)) {
        if (!$config['#key']) {
          $config['#key'] = $real;
        }
        foreach($config['#alias'] as $alias) {
          $tokens[count($tokens)-1] = $alias;
          $matches += patterns_array_fetch('/'. implode('/', $tokens), $data);
        }
      }
    }
    else {
      $matches = patterns_array_fetch($path, $data);
    }

    // If no matches were found and it is a required element
    // return an error
    if (empty($matches) && $config['#required']) {
      $error = true;
    }

    foreach($matches as &$match) {
      // Collect form_ids here.
      // TODO Think of a better way to migrate components form_id operations here
      if ($config['#form_id']) {
        patterns_config_form_ids($data['tag'], $config['#form_id'], $match);
      }

      // Make sure values that should be arrays are set as such
      if ($config['#array'] && !array_key_exists(0, $match['item'])) {
        if (is_null($match['item']) || (is_array($match['item'] && empty($match['item'])))) {
          $match['item'] = array();
        }
        else {
          $match['item'] = array($match['item']);
        }

        // Update the context object to reflect new changes
        $match = _patterns_array_context($match['item'], $match);
      }

      // Change the key of this match. Can get the key value via xpath as well.
      // Any duplicate keys will return an error
      if ($config['#key']) {
        // Check that a single match is found via this xpath and that the resulting value is scalar
        if ($config['#key']{0} == '/' && ($sub_matches = patterns_array_fetch($config['#key'], $match['item'], $match)) && count($sub_matches) == 1 && is_scalar($sub_matches[0]['item'])) {
          // an xpath from the matched item is used to get the key value
          $new_key = $sub_matches[0]['item'];
        }
        else if ($config['#default key']) {
          $new_key = $config['#default key'];
        }
        else if ($config['#key']{0} == '/') {
          // Could not find the appropriate key value from the xpath
          $error = true;
        }
        else {
          // Set the key to a static value. Typically used for allowing more user readable keys
          $new_key = $config['#key'];
        }

        if ($new_key && array_key_exists($new_key, $match['parent']['item'])) {
          // The new key already exists.
          $error = true;
        }
        else if ($new_key) {
          $key = $match['key'];
          $match['parent']['item'][$new_key] = $match['item'];
          // TODO This is buggy. Skip for now.
          if(is_int($key) && false) {
            array_splice($match['parent']['item'], $key, 1);
          }
          else {
            unset($match['parent']['item'][$key]);
          }

          // Update the parent context object to reflect new changes
          $match['parent'] = _patterns_array_context($match['parent']['item'], $match['parent']);

          // Update to the new current match
          for($i=0;$i<count($match['parent']);$i++) {
            if ($match['parent'][$i]['key'] == $new_key) {
              $match = &$match['parent'][$i];
              break;
            }
          }
        }
      }

      // Create empty key elements if they don't exist but should (and are not required)
      // This is useful when using 'move' to ensure that the destination exists
      if ($config['#create'] && is_array($match['item']) && !array_key_exists($config['#create'], $match['item'])) {
        $match['item'][$config['#create']] = $config['#create value'];
      }

      // Move(or copy) this match to somewhere else in the object. If more than one destination
      // match is found, this value is copied to each one
      // TODO Make move automatically create the destination if it doesn't exist
      // TODO Support move in (put inside array) and move to (replace) actions
      if (!empty($match['parent']) && ($config['#move']{0} == '/' && ($dest_path = $config['#move']) && ($op = 'move')) ||
      ($config['#copy']{0} == '/' && ($dest_path = $config['#copy']) && ($op = 'copy'))) {
        $dests = patterns_array_fetch($dest_path, $match['item'], $match);
        $parent = &$match['parent'];
        $obj = $match['item'];

        foreach($dests as $dest) {
          // Can't move to items that are not an array
          if (!is_array($dest['item'])) {
            continue;
          }
          if (!is_int($match['key'])) {
            $dest['item'][$match['key']] = $obj;
          }
          else {
            $dest['item'][] = $obj;
          }

          // Update the dest object to reflect the change
          $dest = _patterns_array_context($dest['item'], $dest);
        }

        if ($op == 'move') {
          if (!is_int($match['key'])) {
            unset($parent['item'][$match['key']]);
          }
          else {
            array_splice($parent['item'], $match['key'], 1);
          }

          // Update the parent object to reflect the change (removal)
          $parent = _patterns_array_context($parent['item'], $parent);
          unset($match);
        }
      }
    }
  }
}

/**
 * Find parts of an array based on
 * a semi-compatible xpath syntax.
 *
 * Returns an array of constructs that includes the
 * references 'item' and 'parent' from the matching values
 * in the $data object along with extra keys 'key' for the key
 * of the current match and 'trace' for a full list of keys
 * till the root of the $data object
 *
 * Loosely based off of Cake function Set::extract
 *
 * @Note: Ensure this always only returns matches from a single level
 * in the array. Changes made to matches in different levels can possibly
 * mess up the above-level matches. To this end, you cannot change the keys
 * of a current match because the other matches cannot change their keys as well
 * to sync with the new parent array.
 * @TODO: Think of a solution for the above note (BROKEN RIGHT NOW)
 */
function patterns_array_fetch($path, &$data, $context = null) {
  if (!$context && (empty($data) || !is_array($data))) {
    return array();
  }
  if ($path === '/') {
    return $data;
  }

  // Construct our contexts object that allows us to traverse the array
  if(!$context) {
    $context = _patterns_array_context($data);
  }

  // Make our context actually a list of contexts
  $context = array($context);

  // Create a list of tokens based on the supplied path
  $tokens = array_slice(preg_split('/(?<!=)\/(?![a-z]*\])/', $path), 1);

  while(!empty($tokens)) {
    $token = array_shift($tokens);

    // TODO Implement better conditionals for each token
    // Currently only supports element=value conditions
    $conditions = array();
    if (preg_match('/(=)(.*)/', $token, $m)) {
      $conditions[$m[1]] = $m[2];
      $token = substr($token, 0, strpos($token, $m[1]));
    }

    $matches = array();
    foreach ($context as &$piece) {
      if ($token === '..') {
        $matches[] = &$piece['parent'];
        continue;
      }
      $match = false;
      if (is_array($piece['item']) && ($token == '*' || array_key_exists($token, $piece['item']))) {
        $i=0;
        while(isset($piece[$i])) {
          if ($piece[$i]['key'] === $token) {
            $matches[] = &$piece[$i];
            break;
          }
          else if ($token === '*') {
            $matches[] = &$piece[$i];
          }
          $i++;
        }
      }
      else if ($token === '.') {
        $matches[] = &$piece;
      }
    }

    // Filter matches from the matches list based on our conditions
    foreach($conditions as $operator => $value) {
      _patterns_array_filter($matches, $operator, $value);
    }

    // Update the context area to the next set of matches to dig into
    $context = $matches;
  }

  // Return the list of matches containing references to their respective data objects
  return $matches;
}

/**
 * Helper function to create a context array based on the supplied object
 * Supplying a parent object will set the parent for this context
 */
function _patterns_array_context(&$obj, &$current = null) {
  // If a current context is set, use it's parent and key values
  if (!($trace = $current['trace'])) {
    $trace = array();
  }
  if (!($key = $current['key'])) {
    $key = null;
  }
  if (!($parent = &$current['parent'])) {
    $parent = null;
  }
  $context = array('trace' => $trace, 'key' => $key, 'item' => &$obj, 'parent' => &$parent);
  $refs = array(&$context);

  while (!empty($refs)) {
    $ref = &$refs[0];
    $parent = &$ref['item'];
    array_splice($refs, 0, 1);

    if (is_array($parent) && !empty($parent)) {
      $i = 0;
      foreach($parent as $index => &$child) {
        // TODO possible optimizations can be done here (with the parent trace)
        $ref[$i] = array('trace' => _patterns_array_trace($ref), 'key' => $index, 'item' => &$child, 'parent' => &$ref);
        array_unshift($refs, '');
        $refs[0] = &$ref[$i++];
      }
    }
  }

  return $context;
}

/**
 * Helper function to filter values of the list of matches
 */
function _patterns_array_filter(&$matches, $operator, $value = null) {
  for($i=count($matches)-1;$i>=0;$i--) {
    $match = &$matches[$i];

    switch($operator) {
      case '=':
        if ($match['item'] != $value) {
          array_splice($matches, $i, 1);
        }
        break;
    }
  }
}

/**
 * Helper function to create a list of parent keys given a context item
 */
function _patterns_array_trace($obj) {
  // Loop back up through the parents to fill in the trace value.
  $up = &$obj;
  $trace = array();
  while(isset($up['parent'])) {
    array_unshift($trace, $up['key']);
    $up = &$up['parent'];
  }

  return $trace;
}

/**
 * Check if all the module dependencies are available
 *
 * @param $modules
 *   array of module names
 * @param $update_list
 *   if TRUE, add all the dependecies to pattern's module list
 * @return
 *   empty array if all dependencies are available
 *   array of missing module's names if some dependencies are not available
 *
 */
function patterns_check_module_dependencies(&$modules, $update_list = FALSE) {

  if (empty($modules)) return array();

  $modules_info = module_rebuild_cache();

  $result = array();
  $dependencies = array();

  foreach($modules as $module) {
    $module = is_array($module) ? $module['value'] : $module;
    if (array_key_exists($module, $modules_info)) {
      // check also for module's dependencies
      foreach ($modules_info[$module]->info['dependencies'] as $dependency) {
        if (array_key_exists($dependency, $modules_info)) {
          $dependencies[] = $dependency;
        }
        else {
          $result[] = $dependency;
        }
      }
    }
    else {
      $result[] = $module;
    }
  }

  if ($update_list && empty($result) && !empty($dependencies)) {
    $modules = array_unique(array_merge($modules, $dependencies));
  }

  return $result;
}

function patterns_set_error_handler() {
  // set custom error handler
  set_error_handler('patterns_error_handler');
  // trigger dummy error
  // this will be used as a refrence to determine if any real error
  // occured during the pattern execution
  @trigger_error('patterns_error');
}

function patterns_restore_error_handler() {
  restore_error_handler();
}

/**
 * Custom error handler used only during patterns execution
 * in order to catch and properly handle PHP errors.
 * Based on drupal_error_handler().
 */
function patterns_error_handler($errno, $message, $filename, $line, $context) {

  if ($errno & (E_ALL ^ E_NOTICE)) {

    $types = array(1 => 'error', 2 => 'warning', 4 => 'parse error', 8 => 'notice', 16 => 'core error', 32 => 'core warning', 64 => 'compile error', 128 => 'compile warning', 256 => 'user error', 512 => 'user warning', 1024 => 'user notice', 2048 => 'strict warning', 4096 => 'recoverable fatal error');

    // For database errors, we want the line number/file name of the place that
    // the query was originally called, not _db_query().
    if (isset($context[DB_ERROR])) {
      $backtrace = array_reverse(debug_backtrace());

      // List of functions where SQL queries can originate.
      $query_functions = array('db_query', 'pager_query', 'db_query_range', 'db_query_temporary', 'update_sql');

      // Determine where query function was called, and adjust line/file
      // accordingly.
      foreach ($backtrace as $index => $function) {
        if (in_array($function['function'], $query_functions)) {
          $line = $backtrace[$index]['line'];
          $filename = $backtrace[$index]['file'];
          break;
        }
      }
    }

    // 'patterns_error' is not a real error and should be skipped
    if ($message != 'patterns_error') {
      watchdog('php', '%message in %file on line %line.', array('%error' => $types[$errno], '%message' => $message, '%file' => $filename, '%line' => $line), WATCHDOG_ERROR);
      patterns_error_set_last(array('message' => $message, 'type' => $errno, 'file' => $filename, 'line' => $line));
    }
  }
  return TRUE;
}

function patterns_error_set_last($error = NULL) {
  static $last_error = array();

  if (isset($error)) {
    $last_error = $error;
  }
  return $last_error;
}

/**
 * Check and report PHP errors during patterns execution
 *
 * @param $op
 *   operation within hook_patterns() during which error occured
 * @param $key
 *   number of the action currently proccessed
 * @param $description
 *   description of the current action
 * @param $pattern_title
 *   title of the pattern currently proccessed
 * @param $pattern_file
 *   path to pattern file currently proccessed
 * @return
 *   error message if new error encountered
 *   FALSE if there are no new errors
 *
 */
function patterns_error_get_last($op, $key, $description, $pattern_title, $pattern_file) {
  $error = patterns_error_set_last();

  if (!empty($error) && $error['message'] != 'patterns_error') {
    $types = array(1 => 'error', 2 => 'warning', 4 => 'parse error', 8 => 'notice', 16 => 'core error', 32 => 'core warning', 64 => 'compile error', 128 => 'compile warning', 256 => 'user error', 512 => 'user warning', 1024 => 'user notice', 2048 => 'strict warning', 4096 => 'recoverable fatal error');
    $php_error_message = $types[$error['type']] .': '. $error['message'] .' in '. $error['file'] .' on line '. $error['line'] .'.';
    $message = t('Pattern %title (%pattern_file)<br>Action #%key: %description (op "%op")<br>PHP error occured:<br>%error', array('%key' => $key, '%title' => $pattern_title, '%op' => $op, '%description' => $description, '%error' => $php_error_message, '%pattern_file' => $pattern_file));
    return $message;
  }
  return FALSE;
}

/**
 * Custom implementation of drupal_process_form()
 *
 * Enables validation to be performed for each executed form
 * by calling our custom _patterns_validate_form() function
 * see issue: http://drupal.org/node/260934
 */
function _patterns_process_form($form_id, &$form, &$form_state) {
  $form_state['values'] = array();

  $form = form_builder($form_id, $form, $form_state);

  // Only process the form if it is programmed or the form_id coming
  // from the POST data is set and matches the current form_id.
  if ((!empty($form['#programmed'])) || (!empty($form['#post']) && (isset($form['#post']['form_id']) && ($form['#post']['form_id'] == $form_id)))) {
    _patterns_validate_form($form_id, $form, $form_state);

    // form_clean_id() maintains a cache of element IDs it has seen,
    // so it can prevent duplicates. We want to be sure we reset that
    // cache when a form is processed, so scenerios that result in
    // the form being built behind the scenes and again for the
    // browser don't increment all the element IDs needlessly.
    form_clean_id(NULL, TRUE);

    if ((!empty($form_state['submitted'])) && !form_get_errors() && empty($form_state['rebuild'])) {
      $form_state['redirect'] = NULL;
      form_execute_handlers('submit', $form, $form_state);

      // We'll clear out the cached copies of the form and its stored data
      // here, as we've finished with them. The in-memory copies are still
      // here, though.
      if (variable_get('cache', CACHE_DISABLED) == CACHE_DISABLED && !empty($form_state['values']['form_build_id'])) {
        cache_clear_all('form_'. $form_state['values']['form_build_id'], 'cache_form');
        cache_clear_all('storage_'. $form_state['values']['form_build_id'], 'cache_form');
      }

      // If batches were set in the submit handlers, we process them now,
      // possibly ending execution. We make sure we do not react to the batch
      // that is already being processed (if a batch operation performs a
      // drupal_execute).
      if ($batch =& batch_get() && !isset($batch['current_set'])) {
        // The batch uses its own copies of $form and $form_state for
        // late execution of submit handers and post-batch redirection.
        $batch['form'] = $form;
        $batch['form_state'] = $form_state;
        $batch['progressive'] = !$form['#programmed'];
        batch_process();
        // Execution continues only for programmatic forms.
        // For 'regular' forms, we get redirected to the batch processing
        // page. Form redirection will be handled in _batch_finished(),
        // after the batch is processed.
      }

      // If no submit handlers have populated the $form_state['storage']
      // bundle, and the $form_state['rebuild'] flag has not been set,
      // we're finished and should redirect to a new destination page
      // if one has been set (and a fresh, unpopulated copy of the form
      // if one hasn't). If the form was called by drupal_execute(),
      // however, we'll skip this and let the calling function examine
      // the resulting $form_state bundle itself.
      if (!$form['#programmed'] && empty($form_state['rebuild']) && empty($form_state['storage'])) {
        drupal_redirect_form($form, $form_state['redirect']);
      }
    }
  }
}

/**
 * Custom implementation of drupal_validate_form()
 *
 * Removed static variable that prevented same form_id to be
 * validated more then once during a single page request
 */
function _patterns_validate_form($form_id, $form, &$form_state) {
  // If the session token was set by drupal_prepare_form(), ensure that it
  // matches the current user's session.
  if (isset($form['#token'])) {
    if (!drupal_valid_token($form_state['values']['form_token'], $form['#token'])) {
      // Setting this error will cause the form to fail validation.
      form_set_error('form_token', t('Validation error, please try again. If this error persists, please contact the site administrator.'));
    }
  }

  _form_validate($form, $form_state, $form_id);
}

/**
 * Custom implementation of Drupal's _taxonomy_term_select()
 *
 * Used to override static caching in taxonmy_get_tree()
 * which is preventing terms added during patterns execution
 * to be included in parents and relations dropdowns and
 * causes validation errors.
 * Hopefully, we can find better solution for this.
 */
function _patterns_taxonomy_term_select($title, $name, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) {
  $tree = _patterns_taxonomy_get_tree($vocabulary_id);

  $options = array();

  if ($blank) {
    $options[''] = $blank;
  }
  if ($tree) {
    foreach ($tree as $term) {
      if (!in_array($term->tid, $exclude)) {
        $choice = new stdClass();
        $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
        $options[] = $choice;
      }
    }
  }

  return array('#type' => 'select',
    '#title' => $title,
    '#default_value' => $value,
    '#options' => $options,
    '#description' => $description,
    '#multiple' => $multiple,
    '#size' => $multiple ? min(9, count($options)) : 0,
    '#weight' => -15,
    '#theme' => 'taxonomy_term_select',
  );
}

/**
 * Custom implementation of Drupal's taxonomy_get_tree()
 *
 * Removed static caching.
 * New terms may be created during patterns execution and
 * static caching prevents them from being returned in
 * all subsequent calls to taxonomy_get_tree() during
 * the current pattern execution (within current page request)
 */
function _patterns_taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {

  $depth++;

  $children[$vid] = array();

  $result = db_query(db_rewrite_sql('SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d ORDER BY weight, name', 't', 'tid'), $vid);
  while ($term = db_fetch_object($result)) {
    $children[$vid][$term->parent][] = $term->tid;
    $parents[$vid][$term->tid][] = $term->parent;
    $terms[$vid][$term->tid] = $term;
  }

  $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
  $tree = array();
  if (!empty($children[$vid][$parent])) {
    foreach ($children[$vid][$parent] as $child) {
      if ($max_depth > $depth) {
        $term = drupal_clone($terms[$vid][$child]);
        $term->depth = $depth;
        // The "parent" attribute is not useful, as it would show one parent only.
        unset($term->parent);
        $term->parents = $parents[$vid][$child];
        $tree[] = $term;

        if (!empty($children[$vid][$child])) {
          $tree = array_merge($tree, _patterns_taxonomy_get_tree($vid, $child, $depth, $max_depth));
        }
      }
    }
  }

  return $tree;
}

/**
 * Custom implementation of Drupal's taxonomy_form()
 *
 * Used to override static caching for taxonomy terms on node edit form.
 */
function _patterns_taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
  $vocabulary = taxonomy_vocabulary_load($vid);
  $help = ($help) ? $help : $vocabulary->help;

  if (!$vocabulary->multiple) {
    $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -');
  }
  else {
    $blank = ($vocabulary->required) ? 0 : t('- None -');
  }

  return _patterns_taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank);
}

/**
 * Custom implementation of Drupal's menu_parent_options()
 *
 * Used to override static caching in menu_tree_all_data()
 * which is preventing menu items created during patterns execution
 * to be included in 'parent item' dropdown and causes validation errors.
 * Hopefully, we can find better solution for this.
 */
function _patterns_menu_parent_options($menus, $item) {
  // The menu_links table can be practically any size and we need a way to
  // allow contrib modules to provide more scalable pattern choosers.
  // hook_form_alter is too late in itself because all the possible parents are
  // retrieved here, unless menu_override_parent_selector is set to TRUE.
  if (variable_get('menu_override_parent_selector', FALSE)) {
    return array();
  }
  // If the item has children, there is an added limit to the depth of valid parents.
  if (isset($item['parent_depth_limit'])) {
    $limit = $item['parent_depth_limit'];
  }
  else {
    $limit = _menu_parent_depth_limit($item);
  }

  foreach ($menus as $menu_name => $title) {
    $tree = _patterns_menu_tree_all_data($menu_name, NULL);
    $options[$menu_name .':0'] = '<'. $title .'>';
    _menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit);
  }
  return $options;
}

/**
 * Custom implementation of Drupal's menu_tree_all_data()
 *
 * Removed static caching.
 * New menu items may be created during pattern execution and
 * static caching prevents them from being returned in
 * all subsequent calls to menu_tree_all_data() during
 * the current pattern execution (within current page request)
 */
function _patterns_menu_tree_all_data($menu_name = 'navigation', $item = NULL) {
  $tree = array();

  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
  $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
  // Generate a cache ID (cid) specific for this $menu_name and $item.
  $cid = 'links:'. $menu_name .':all-cid:'. $mlid;

  // If the static variable doesn't have the data, check {cache_menu}.
  $cache = cache_get($cid, 'cache_menu');
  if ($cache && isset($cache->data)) {
    // If the cache entry exists, it will just be the cid for the actual data.
    // This avoids duplication of large amounts of data.
    $cache = cache_get($cache->data, 'cache_menu');
    if ($cache && isset($cache->data)) {
      $data = $cache->data;
    }
  }
  // If the tree data was not in the cache, $data will be NULL.
  if (!isset($data)) {
    // Build and run the query, and build the tree.
    if ($mlid) {
      // The tree is for a single item, so we need to match the values in its
      // p columns and 0 (the top level) with the plid values of other links.
      $args = array(0);
      for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
        $args[] = $item["p$i"];
      }
      $args = array_unique($args);
      $placeholders = implode(', ', array_fill(0, count($args), '%d'));
      $where = ' AND ml.plid IN ('. $placeholders .')';
      $parents = $args;
      $parents[] = $item['mlid'];
    }
    else {
      // Get all links in this menu.
      $where = '';
      $args = array();
      $parents = array();
    }
    array_unshift($args, $menu_name);
    // Select the links from the table, and recursively build the tree.  We
    // LEFT JOIN since there is no match in {menu_router} for an external
    // link.
    $data['tree'] = menu_tree_data(db_query("
      SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
      FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
      WHERE ml.menu_name = '%s'". $where ."
      ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC", $args), $parents);
    $data['node_links'] = array();
    menu_tree_collect_node_links($data['tree'], $data['node_links']);
    // Cache the data, if it is not already in the cache.
    $tree_cid = _menu_tree_cid($menu_name, $data);
    if (!cache_get($tree_cid, 'cache_menu')) {
      cache_set($tree_cid, $data, 'cache_menu');
    }
    // Cache the cid of the (shared) data using the menu and item-specific cid.
    cache_set($cid, $tree_cid, 'cache_menu');
  }
  // Check access for the current user to each item in the tree.
  menu_tree_check_access($data['tree'], $data['node_links']);
  $tree[$cid] = $data['tree'];

  return $tree[$cid];
}


function patterns_debug_batch($var = null) {
  $var = print_r($var, true);
  $var = str_replace(" ", '&nbsp;', $var);
  $var = str_replace("\n", "<br />", $var);
  print drupal_to_js(array('status' => 0, 'data' => $var));
  exit;
}