<?php

/**
 * @file
 * Provides Features integration for Workflow using the CRUD API.
 *
 * As you will notice this file will only handle the <export> of Worflows,
 * including states and transitions. The <import> is handeled magically,
 * and all modifications are done in function Workflow::save().
 */

define('WORKFLOW_FEATURES_AUTHOR_NAME', 'workflow_features_author_name');

// Even if workflow Node is not enabled, Features may use Node API's type_maps.
require_once dirname(__FILE__) . '/workflow.node.type_map.inc';

/**
 * Default controller handling features integration.
 */
class WorkflowFeaturesController extends EntityDefaultFeaturesController {

  /**
   * Generates the result for hook_features_export().
   */
  public function export($data, &$export, $module_name = '') {
    $pipe = parent::export($data, $export, $module_name);

    foreach ($data as $workflow_name) {
      if ($workflow = workflow_load_by_name($workflow_name)) {
        // Add dependency on workflow_node.
        if (count($workflow->getTypeMap())) {
          $export['dependencies']['workflownode'] = 'workflownode';
        }
        // Add dependency on workflow_field.
        $export['features']['Workflow'][$workflow_name] = $workflow_name;
      }
    }

    return $pipe;
  }

  /**
   * Generates the result for hook_features_export_render().
   *
   * This is a copy of the parent, adding 'system_roles'.
   * The workflow is imported in the target system with Workflow::save().
   */
  public function export_render($module, $data, $export = NULL) {
    $translatables = $code = array();
    $code[] = '  $workflows = array();';
    $code[] = '';

    foreach ($data as $identifier) {
      // Clone workflow to make sure changes are not propagated to original.
      if ($workflow = entity_load_single($this->type, $identifier)) {
        $this->export_render_workflow($workflow, $identifier, $code);
      }
    }

    $code[] = '  return $workflows;';
    $code = implode("\n", $code);

    $hook = isset($this->info['export']['default hook']) ? $this->info['export']['default hook'] : 'default_' . $this->type;

    return array($hook => $code);
  }

  /**
   * Renders the provided workflow into export code.
   *
   * @param Workflow $workflow
   *   The workflow to export.
   * @param string $identifier
   *   The unique machine name for the workflow in the export.
   * @param array $code
   *   A reference to the export code array that will receive the output.
   */
  protected function export_render_workflow(Workflow $workflow, $identifier, array &$code) {
    // Make sure data is not copied to the database.
    $workflow = clone $workflow;

    $this->sanitize_workflow_for_export($workflow);

    // Make sure to escape the characters \ and '.
    // The following method has the advantage, that you can export with
    // features,
    // and later import without enabling Features in the target system.
    $workflow_export = addcslashes(entity_export($this->type, $workflow, '  '), '\\\'');
    $workflow_identifier = features_var_export($identifier);

    $code[] = "  // Exported workflow: {$workflow_identifier}";
    $code[] = "  \$workflows[{$workflow_identifier}] = entity_import('{$this->type}', '" . $workflow_export . "');";
    $code[] = ''; // Blank line
  }

  /**
   * Prepares the provided workflow for export.
   *
   * Removes serial IDs and replaces them with machine names.
   *
   * @param Workflow $workflow
   *   The workflow to sanitize. The contents of this object are modified directly.
   */
  protected function sanitize_workflow_for_export(Workflow $workflow) {
    // Eliminate serial IDs in exports to prevent "Overridden" status.
    // We use machine names instead.
    unset($workflow->wid);

    // Add roles to translate role IDs on target system.
    $permission = NULL;

    $workflow->system_roles = workflow_get_roles($permission);

    $sid_to_name_map = $this->pack_states($workflow);
    $this->pack_transitions($workflow, $sid_to_name_map);
  }

  /**
   * "Packs" the states in the provided workflow into an export-friendly format.
   *
   * @param Workflow $workflow
   *   The workflow to pack. The contents of this object are modified directly.
   *
   * @return array
   *   A map of the old state IDs to their new machine names.
   */
  protected function pack_states(Workflow $workflow) {
    $named_states    = array();
    $sid_to_name_map = array();

    foreach ($workflow->states as $state) {
      /* @var WorkflowState $state */
      $name = $state->getName();

      $sid_to_name_map[$state->sid] = $name;

      // Eliminate serial IDs in exports to prevent "Overridden" status.
      // We use machine names instead.
      unset($state->sid);
      unset($state->wid);

      $named_states[$name] = $state;
    }
    ksort($named_states);

    // Identify states by machine name.
    $workflow->states = $named_states;

    return $sid_to_name_map;
  }

  /**
   * "Packs" the transitions in the provided workflow into an export-friendly format.
   *
   * @param Workflow $workflow
   *   The workflow to pack. The contents of this object are modified directly.
   *
   * @param array $sid_to_name_map
   *   The map of numeric state IDs to their machine names, for remapping sid
   *   references.
   */
  protected function pack_transitions(Workflow $workflow, array $sid_to_name_map) {
    $named_transitions = array();

    foreach ($workflow->transitions as $transition) {
      /* @var WorkflowTransition $transition */
      $start_name = $sid_to_name_map[$transition->sid];
      $end_name   = $sid_to_name_map[$transition->target_sid];
      $new_name = WorkflowConfigTransition::machineName($start_name, $end_name);

      $transition->name        = $new_name;
      $transition->start_state = $start_name;
      $transition->end_state   = $end_name;

      // Eliminate serial IDs in exports to prevent "Overridden" status.
      // We use machine names instead.
      unset($transition->wid);
      unset($transition->tid);
      unset($transition->sid);
      unset($transition->target_sid);

      $named_transitions[$new_name] = $transition;
    }
    ksort($named_transitions);

    // Identify transitions by new machine name.
    $workflow->transitions = $named_transitions;
  }

  /**
   * Revert this workflow, either creating the workflow new (if one with the
   * same machine name is not present), or updating the existing workflow.
   *
   * @param string $module
   *   The name of the feature module whose components should be reverted.
   */
  function revert($module = NULL) {
    // Loads defaults from feature code.
    $defaults = workflow_get_defaults($module);

    foreach ($defaults as $machine_name => $entity) {
      workflow_revert($defaults, $machine_name);
    }
  }
}

/**
 * Implements hook_features_COMPONENT_alter().
 *
 * Adds the corresponding Workflow to the WorkflowField.
 */
function workflow_features_pipe_field_base_alter(&$pipe, $data, $export) {
  if (!empty($data)) {
    foreach ($data as $field_name) {
      // $info = field_info_field($field_name);
      $field = _workflow_info_field($field_name);

      if ($field['type'] == 'workflow') {
        // $field['settings']['wid'] can be numeric or named.
        $workflow = workflow_load_single($field['settings']['wid']);

        // Fields might reference missing workflows.
        if (!empty($workflow)) {
          $pipe['Workflow'][] = $workflow->name;
        }
      }
    }
  }
}

/**
 * Implements hook_features_api_alter().
 *
 * Ensures Workflow always fires last during rebuild, to ensure that roles
 * referenced by workflows to be loaded-in when features contain roles.
 */
function workflow_features_api_alter(array &$components) {
  // FIXME: Why is Workflow the only features provider with an uppercase component name?
  $component_name = 'Workflow';

  if (isset($components[$component_name])) {
    $setting = $components[$component_name];
    unset($components[$component_name]);
    $components[$component_name] = $setting;
  }
}
