<?php

/**
 * @task customfield Custom Field Integration
 */
abstract class HeraldAdapter {

  const FIELD_TITLE                  = 'title';
  const FIELD_BODY                   = 'body';
  const FIELD_AUTHOR                 = 'author';
  const FIELD_ASSIGNEE               = 'assignee';
  const FIELD_REVIEWER               = 'reviewer';
  const FIELD_REVIEWERS              = 'reviewers';
  const FIELD_COMMITTER              = 'committer';
  const FIELD_CC                     = 'cc';
  const FIELD_TAGS                   = 'tags';
  const FIELD_DIFF_FILE              = 'diff-file';
  const FIELD_DIFF_CONTENT           = 'diff-content';
  const FIELD_DIFF_ADDED_CONTENT     = 'diff-added-content';
  const FIELD_DIFF_REMOVED_CONTENT   = 'diff-removed-content';
  const FIELD_DIFF_ENORMOUS          = 'diff-enormous';
  const FIELD_REPOSITORY             = 'repository';
  const FIELD_REPOSITORY_PROJECTS    = 'repository-projects';
  const FIELD_RULE                   = 'rule';
  const FIELD_AFFECTED_PACKAGE       = 'affected-package';
  const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner';
  const FIELD_CONTENT_SOURCE         = 'contentsource';
  const FIELD_ALWAYS                 = 'always';
  const FIELD_AUTHOR_PROJECTS        = 'authorprojects';
  const FIELD_PROJECTS               = 'projects';
  const FIELD_PUSHER                 = 'pusher';
  const FIELD_PUSHER_PROJECTS        = 'pusher-projects';
  const FIELD_DIFFERENTIAL_REVISION  = 'differential-revision';
  const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers';
  const FIELD_DIFFERENTIAL_CCS       = 'differential-ccs';
  const FIELD_DIFFERENTIAL_ACCEPTED  = 'differential-accepted';
  const FIELD_IS_MERGE_COMMIT        = 'is-merge-commit';
  const FIELD_BRANCHES               = 'branches';
  const FIELD_AUTHOR_RAW             = 'author-raw';
  const FIELD_COMMITTER_RAW          = 'committer-raw';
  const FIELD_IS_NEW_OBJECT          = 'new-object';
  const FIELD_TASK_PRIORITY          = 'taskpriority';
  const FIELD_ARCANIST_PROJECT       = 'arcanist-project';
  const FIELD_PUSHER_IS_COMMITTER    = 'pusher-is-committer';

  const CONDITION_CONTAINS        = 'contains';
  const CONDITION_NOT_CONTAINS    = '!contains';
  const CONDITION_IS              = 'is';
  const CONDITION_IS_NOT          = '!is';
  const CONDITION_IS_ANY          = 'isany';
  const CONDITION_IS_NOT_ANY      = '!isany';
  const CONDITION_INCLUDE_ALL     = 'all';
  const CONDITION_INCLUDE_ANY     = 'any';
  const CONDITION_INCLUDE_NONE    = 'none';
  const CONDITION_IS_ME           = 'me';
  const CONDITION_IS_NOT_ME       = '!me';
  const CONDITION_REGEXP          = 'regexp';
  const CONDITION_RULE            = 'conditions';
  const CONDITION_NOT_RULE        = '!conditions';
  const CONDITION_EXISTS          = 'exists';
  const CONDITION_NOT_EXISTS      = '!exists';
  const CONDITION_UNCONDITIONALLY = 'unconditionally';
  const CONDITION_NEVER           = 'never';
  const CONDITION_REGEXP_PAIR     = 'regexp-pair';
  const CONDITION_HAS_BIT         = 'bit';
  const CONDITION_NOT_BIT         = '!bit';
  const CONDITION_IS_TRUE         = 'true';
  const CONDITION_IS_FALSE        = 'false';

  const ACTION_ADD_CC       = 'addcc';
  const ACTION_REMOVE_CC    = 'remcc';
  const ACTION_EMAIL        = 'email';
  const ACTION_NOTHING      = 'nothing';
  const ACTION_AUDIT        = 'audit';
  const ACTION_FLAG         = 'flag';
  const ACTION_ASSIGN_TASK  = 'assigntask';
  const ACTION_ADD_PROJECTS = 'addprojects';
  const ACTION_ADD_REVIEWERS = 'addreviewers';
  const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers';
  const ACTION_APPLY_BUILD_PLANS = 'applybuildplans';
  const ACTION_BLOCK = 'block';

  const VALUE_TEXT            = 'text';
  const VALUE_NONE            = 'none';
  const VALUE_EMAIL           = 'email';
  const VALUE_USER            = 'user';
  const VALUE_TAG             = 'tag';
  const VALUE_RULE            = 'rule';
  const VALUE_REPOSITORY      = 'repository';
  const VALUE_OWNERS_PACKAGE  = 'package';
  const VALUE_PROJECT         = 'project';
  const VALUE_FLAG_COLOR      = 'flagcolor';
  const VALUE_CONTENT_SOURCE  = 'contentsource';
  const VALUE_USER_OR_PROJECT = 'userorproject';
  const VALUE_BUILD_PLAN      = 'buildplan';
  const VALUE_TASK_PRIORITY   = 'taskpriority';
  const VALUE_ARCANIST_PROJECT = 'arcanistprojects';

  private $contentSource;
  private $isNewObject;
  private $customFields = false;

  public function setContentSource(PhabricatorContentSource $content_source) {
    $this->contentSource = $content_source;
    return $this;
  }
  public function getContentSource() {
    return $this->contentSource;
  }

  public function getIsNewObject() {
    if (is_bool($this->isNewObject)) {
      return $this->isNewObject;
    }

    throw new Exception(pht('You must setIsNewObject to a boolean first!'));
  }
  public function setIsNewObject($new) {
    $this->isNewObject = (bool) $new;
    return $this;
  }

  abstract public function getPHID();
  abstract public function getHeraldName();

  public function getHeraldField($field_name) {
    switch ($field_name) {
      case self::FIELD_RULE:
        return null;
      case self::FIELD_CONTENT_SOURCE:
        return $this->getContentSource()->getSource();
      case self::FIELD_ALWAYS:
        return true;
      case self::FIELD_IS_NEW_OBJECT:
        return $this->getIsNewObject();
      default:
        if ($this->isHeraldCustomKey($field_name)) {
          return $this->getCustomFieldValue($field_name);
        }

        throw new Exception(
          "Unknown field '{$field_name}'!");
    }
  }

  abstract public function applyHeraldEffects(array $effects);

  public function isAvailableToUser(PhabricatorUser $viewer) {
    $applications = id(new PhabricatorApplicationQuery())
      ->setViewer($viewer)
      ->withInstalled(true)
      ->withClasses(array($this->getAdapterApplicationClass()))
      ->execute();

    return !empty($applications);
  }


  /**
   * NOTE: You generally should not override this; it exists to support legacy
   * adapters which had hard-coded content types.
   */
  public function getAdapterContentType() {
    return get_class($this);
  }

  abstract public function getAdapterContentName();
  abstract public function getAdapterContentDescription();
  abstract public function getAdapterApplicationClass();
  abstract public function getObject();

  public function supportsRuleType($rule_type) {
    return false;
  }

  public function canTriggerOnObject($object) {
    return false;
  }

  public function explainValidTriggerObjects() {
    return pht('This adapter can not trigger on objects.');
  }

  public function getTriggerObjectPHIDs() {
    return array($this->getPHID());
  }

  public function getAdapterSortKey() {
    return sprintf(
      '%08d%s',
      $this->getAdapterSortOrder(),
      $this->getAdapterContentName());
  }

  public function getAdapterSortOrder() {
    return 1000;
  }


/* -(  Fields  )------------------------------------------------------------- */


  public function getFields() {
    $fields = array();

    $fields[] = self::FIELD_ALWAYS;
    $fields[] = self::FIELD_RULE;

    $custom_fields = $this->getCustomFields();
    if ($custom_fields) {
      foreach ($custom_fields->getFields() as $custom_field) {
        $key = $custom_field->getFieldKey();
        $fields[] = $this->getHeraldKeyFromCustomKey($key);
      }
    }

    return $fields;
  }

  public function getFieldNameMap() {
    return array(
      self::FIELD_TITLE => pht('Title'),
      self::FIELD_BODY => pht('Body'),
      self::FIELD_AUTHOR => pht('Author'),
      self::FIELD_ASSIGNEE => pht('Assignee'),
      self::FIELD_COMMITTER => pht('Committer'),
      self::FIELD_REVIEWER => pht('Reviewer'),
      self::FIELD_REVIEWERS => pht('Reviewers'),
      self::FIELD_CC => pht('CCs'),
      self::FIELD_TAGS => pht('Tags'),
      self::FIELD_DIFF_FILE => pht('Any changed filename'),
      self::FIELD_DIFF_CONTENT => pht('Any changed file content'),
      self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'),
      self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'),
      self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'),
      self::FIELD_REPOSITORY => pht('Repository'),
      self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'),
      self::FIELD_RULE => pht('Another Herald rule'),
      self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'),
      self::FIELD_AFFECTED_PACKAGE_OWNER =>
        pht("Any affected package's owner"),
      self::FIELD_CONTENT_SOURCE => pht('Content Source'),
      self::FIELD_ALWAYS => pht('Always'),
      self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"),
      self::FIELD_PROJECTS => pht("Projects"),
      self::FIELD_PUSHER => pht('Pusher'),
      self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"),
      self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'),
      self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'),
      self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'),
      self::FIELD_DIFFERENTIAL_ACCEPTED
        => pht('Accepted Differential revision'),
      self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'),
      self::FIELD_BRANCHES => pht('Commit\'s branches'),
      self::FIELD_AUTHOR_RAW => pht('Raw author name'),
      self::FIELD_COMMITTER_RAW => pht('Raw committer name'),
      self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'),
      self::FIELD_TASK_PRIORITY => pht('Task priority'),
      self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'),
      self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'),
    ) + $this->getCustomFieldNameMap();
  }


/* -(  Conditions  )--------------------------------------------------------- */


  public function getConditionNameMap() {
    return array(
      self::CONDITION_CONTAINS        => pht('contains'),
      self::CONDITION_NOT_CONTAINS    => pht('does not contain'),
      self::CONDITION_IS              => pht('is'),
      self::CONDITION_IS_NOT          => pht('is not'),
      self::CONDITION_IS_ANY          => pht('is any of'),
      self::CONDITION_IS_TRUE         => pht('is true'),
      self::CONDITION_IS_FALSE        => pht('is false'),
      self::CONDITION_IS_NOT_ANY      => pht('is not any of'),
      self::CONDITION_INCLUDE_ALL     => pht('include all of'),
      self::CONDITION_INCLUDE_ANY     => pht('include any of'),
      self::CONDITION_INCLUDE_NONE    => pht('do not include'),
      self::CONDITION_IS_ME           => pht('is myself'),
      self::CONDITION_IS_NOT_ME       => pht('is not myself'),
      self::CONDITION_REGEXP          => pht('matches regexp'),
      self::CONDITION_RULE            => pht('matches:'),
      self::CONDITION_NOT_RULE        => pht('does not match:'),
      self::CONDITION_EXISTS          => pht('exists'),
      self::CONDITION_NOT_EXISTS      => pht('does not exist'),
      self::CONDITION_UNCONDITIONALLY => '',  // don't show anything!
      self::CONDITION_NEVER           => '',  // don't show anything!
      self::CONDITION_REGEXP_PAIR     => pht('matches regexp pair'),
      self::CONDITION_HAS_BIT         => pht('has bit'),
      self::CONDITION_NOT_BIT         => pht('lacks bit'),
    );
  }

  public function getConditionsForField($field) {
    switch ($field) {
      case self::FIELD_TITLE:
      case self::FIELD_BODY:
      case self::FIELD_COMMITTER_RAW:
      case self::FIELD_AUTHOR_RAW:
        return array(
          self::CONDITION_CONTAINS,
          self::CONDITION_NOT_CONTAINS,
          self::CONDITION_IS,
          self::CONDITION_IS_NOT,
          self::CONDITION_REGEXP,
        );
      case self::FIELD_REVIEWER:
      case self::FIELD_PUSHER:
      case self::FIELD_TASK_PRIORITY:
      case self::FIELD_ARCANIST_PROJECT:
        return array(
          self::CONDITION_IS_ANY,
          self::CONDITION_IS_NOT_ANY,
        );
      case self::FIELD_REPOSITORY:
      case self::FIELD_ASSIGNEE:
      case self::FIELD_AUTHOR:
      case self::FIELD_COMMITTER:
        return array(
          self::CONDITION_IS_ANY,
          self::CONDITION_IS_NOT_ANY,
          self::CONDITION_EXISTS,
          self::CONDITION_NOT_EXISTS,
        );
      case self::FIELD_TAGS:
      case self::FIELD_REVIEWERS:
      case self::FIELD_CC:
      case self::FIELD_AUTHOR_PROJECTS:
      case self::FIELD_PROJECTS:
      case self::FIELD_AFFECTED_PACKAGE:
      case self::FIELD_AFFECTED_PACKAGE_OWNER:
      case self::FIELD_PUSHER_PROJECTS:
      case self::FIELD_REPOSITORY_PROJECTS:
        return array(
          self::CONDITION_INCLUDE_ALL,
          self::CONDITION_INCLUDE_ANY,
          self::CONDITION_INCLUDE_NONE,
          self::CONDITION_EXISTS,
          self::CONDITION_NOT_EXISTS,
        );
      case self::FIELD_DIFF_FILE:
      case self::FIELD_BRANCHES:
        return array(
          self::CONDITION_CONTAINS,
          self::CONDITION_REGEXP,
        );
      case self::FIELD_DIFF_CONTENT:
      case self::FIELD_DIFF_ADDED_CONTENT:
      case self::FIELD_DIFF_REMOVED_CONTENT:
        return array(
          self::CONDITION_CONTAINS,
          self::CONDITION_REGEXP,
          self::CONDITION_REGEXP_PAIR,
        );
      case self::FIELD_RULE:
        return array(
          self::CONDITION_RULE,
          self::CONDITION_NOT_RULE,
        );
      case self::FIELD_CONTENT_SOURCE:
        return array(
          self::CONDITION_IS,
          self::CONDITION_IS_NOT,
        );
      case self::FIELD_ALWAYS:
        return array(
          self::CONDITION_UNCONDITIONALLY,
        );
      case self::FIELD_DIFFERENTIAL_REVIEWERS:
        return array(
          self::CONDITION_EXISTS,
          self::CONDITION_NOT_EXISTS,
          self::CONDITION_INCLUDE_ALL,
          self::CONDITION_INCLUDE_ANY,
          self::CONDITION_INCLUDE_NONE,
        );
      case self::FIELD_DIFFERENTIAL_CCS:
        return array(
          self::CONDITION_INCLUDE_ALL,
          self::CONDITION_INCLUDE_ANY,
          self::CONDITION_INCLUDE_NONE,
        );
      case self::FIELD_DIFFERENTIAL_REVISION:
      case self::FIELD_DIFFERENTIAL_ACCEPTED:
        return array(
          self::CONDITION_EXISTS,
          self::CONDITION_NOT_EXISTS,
        );
      case self::FIELD_IS_MERGE_COMMIT:
      case self::FIELD_DIFF_ENORMOUS:
      case self::FIELD_IS_NEW_OBJECT:
      case self::FIELD_PUSHER_IS_COMMITTER:
        return array(
          self::CONDITION_IS_TRUE,
          self::CONDITION_IS_FALSE,
        );
      default:
        if ($this->isHeraldCustomKey($field)) {
          return $this->getCustomFieldConditions($field);
        }
        throw new Exception(
          "This adapter does not define conditions for field '{$field}'!");
    }
  }

  public function doesConditionMatch(
    HeraldEngine $engine,
    HeraldRule $rule,
    HeraldCondition $condition,
    $field_value) {

    $condition_type = $condition->getFieldCondition();
    $condition_value = $condition->getValue();

    switch ($condition_type) {
      case self::CONDITION_CONTAINS:
        // "Contains" can take an array of strings, as in "Any changed
        // filename" for diffs.
        foreach ((array)$field_value as $value) {
          if (stripos($value, $condition_value) !== false) {
            return true;
          }
        }
        return false;
      case self::CONDITION_NOT_CONTAINS:
        return (stripos($field_value, $condition_value) === false);
      case self::CONDITION_IS:
        return ($field_value == $condition_value);
      case self::CONDITION_IS_NOT:
        return ($field_value != $condition_value);
      case self::CONDITION_IS_ME:
        return ($field_value == $rule->getAuthorPHID());
      case self::CONDITION_IS_NOT_ME:
        return ($field_value != $rule->getAuthorPHID());
      case self::CONDITION_IS_ANY:
        if (!is_array($condition_value)) {
          throw new HeraldInvalidConditionException(
            "Expected condition value to be an array.");
        }
        $condition_value = array_fuse($condition_value);
        return isset($condition_value[$field_value]);
      case self::CONDITION_IS_NOT_ANY:
        if (!is_array($condition_value)) {
          throw new HeraldInvalidConditionException(
            "Expected condition value to be an array.");
        }
        $condition_value = array_fuse($condition_value);
        return !isset($condition_value[$field_value]);
      case self::CONDITION_INCLUDE_ALL:
        if (!is_array($field_value)) {
          throw new HeraldInvalidConditionException(
            "Object produced non-array value!");
        }
        if (!is_array($condition_value)) {
          throw new HeraldInvalidConditionException(
            "Expected condition value to be an array.");
        }

        $have = array_select_keys(array_fuse($field_value), $condition_value);
        return (count($have) == count($condition_value));
      case self::CONDITION_INCLUDE_ANY:
        return (bool)array_select_keys(
          array_fuse($field_value),
          $condition_value);
      case self::CONDITION_INCLUDE_NONE:
        return !array_select_keys(
          array_fuse($field_value),
          $condition_value);
      case self::CONDITION_EXISTS:
      case self::CONDITION_IS_TRUE:
        return (bool)$field_value;
      case self::CONDITION_NOT_EXISTS:
      case self::CONDITION_IS_FALSE:
        return !$field_value;
      case self::CONDITION_UNCONDITIONALLY:
        return (bool)$field_value;
      case self::CONDITION_NEVER:
        return false;
      case self::CONDITION_REGEXP:
        foreach ((array)$field_value as $value) {
          // We add the 'S' flag because we use the regexp multiple times.
          // It shouldn't cause any troubles if the flag is already there
          // - /.*/S is evaluated same as /.*/SS.
          $result = @preg_match($condition_value . 'S', $value);
          if ($result === false) {
            throw new HeraldInvalidConditionException(
              "Regular expression is not valid!");
          }
          if ($result) {
            return true;
          }
        }
        return false;
      case self::CONDITION_REGEXP_PAIR:
        // Match a JSON-encoded pair of regular expressions against a
        // dictionary. The first regexp must match the dictionary key, and the
        // second regexp must match the dictionary value. If any key/value pair
        // in the dictionary matches both regexps, the condition is satisfied.
        $regexp_pair = json_decode($condition_value, true);
        if (!is_array($regexp_pair)) {
          throw new HeraldInvalidConditionException(
            "Regular expression pair is not valid JSON!");
        }
        if (count($regexp_pair) != 2) {
          throw new HeraldInvalidConditionException(
            "Regular expression pair is not a pair!");
        }

        $key_regexp   = array_shift($regexp_pair);
        $value_regexp = array_shift($regexp_pair);

        foreach ((array)$field_value as $key => $value) {
          $key_matches = @preg_match($key_regexp, $key);
          if ($key_matches === false) {
            throw new HeraldInvalidConditionException(
              "First regular expression is invalid!");
          }
          if ($key_matches) {
            $value_matches = @preg_match($value_regexp, $value);
            if ($value_matches === false) {
              throw new HeraldInvalidConditionException(
                "Second regular expression is invalid!");
            }
            if ($value_matches) {
              return true;
            }
          }
        }
        return false;
      case self::CONDITION_RULE:
      case self::CONDITION_NOT_RULE:
        $rule = $engine->getRule($condition_value);
        if (!$rule) {
          throw new HeraldInvalidConditionException(
            "Condition references a rule which does not exist!");
        }

        $is_not = ($condition_type == self::CONDITION_NOT_RULE);
        $result = $engine->doesRuleMatch($rule, $this);
        if ($is_not) {
          $result = !$result;
        }
        return $result;
      case self::CONDITION_HAS_BIT:
        return (($condition_value & $field_value) === (int) $condition_value);
      case self::CONDITION_NOT_BIT:
        return (($condition_value & $field_value) !== (int) $condition_value);
      default:
        throw new HeraldInvalidConditionException(
          "Unknown condition '{$condition_type}'.");
    }
  }

  public function willSaveCondition(HeraldCondition $condition) {
    $condition_type = $condition->getFieldCondition();
    $condition_value = $condition->getValue();

    switch ($condition_type) {
      case self::CONDITION_REGEXP:
        $ok = @preg_match($condition_value, '');
        if ($ok === false) {
          throw new HeraldInvalidConditionException(
            pht(
              'The regular expression "%s" is not valid. Regular expressions '.
              'must have enclosing characters (e.g. "@/path/to/file@", not '.
              '"/path/to/file") and be syntactically correct.',
              $condition_value));
        }
        break;
      case self::CONDITION_REGEXP_PAIR:
        $json = json_decode($condition_value, true);
        if (!is_array($json)) {
          throw new HeraldInvalidConditionException(
            pht(
              'The regular expression pair "%s" is not valid JSON. Enter a '.
              'valid JSON array with two elements.',
              $condition_value));
        }

        if (count($json) != 2) {
          throw new HeraldInvalidConditionException(
            pht(
              'The regular expression pair "%s" must have exactly two '.
              'elements.',
              $condition_value));
        }

        $key_regexp = array_shift($json);
        $val_regexp = array_shift($json);

        $key_ok = @preg_match($key_regexp, '');
        if ($key_ok === false) {
          throw new HeraldInvalidConditionException(
            pht(
              'The first regexp in the regexp pair, "%s", is not a valid '.
              'regexp.',
              $key_regexp));
        }

        $val_ok = @preg_match($val_regexp, '');
        if ($val_ok === false) {
          throw new HeraldInvalidConditionException(
            pht(
              'The second regexp in the regexp pair, "%s", is not a valid '.
              'regexp.',
              $val_regexp));
        }
        break;
      case self::CONDITION_CONTAINS:
      case self::CONDITION_NOT_CONTAINS:
      case self::CONDITION_IS:
      case self::CONDITION_IS_NOT:
      case self::CONDITION_IS_ANY:
      case self::CONDITION_IS_NOT_ANY:
      case self::CONDITION_INCLUDE_ALL:
      case self::CONDITION_INCLUDE_ANY:
      case self::CONDITION_INCLUDE_NONE:
      case self::CONDITION_IS_ME:
      case self::CONDITION_IS_NOT_ME:
      case self::CONDITION_RULE:
      case self::CONDITION_NOT_RULE:
      case self::CONDITION_EXISTS:
      case self::CONDITION_NOT_EXISTS:
      case self::CONDITION_UNCONDITIONALLY:
      case self::CONDITION_NEVER:
      case self::CONDITION_HAS_BIT:
      case self::CONDITION_NOT_BIT:
      case self::CONDITION_IS_TRUE:
      case self::CONDITION_IS_FALSE:
        // No explicit validation for these types, although there probably
        // should be in some cases.
        break;
      default:
        throw new HeraldInvalidConditionException(
          pht(
            'Unknown condition "%s"!',
            $condition_type));
    }
  }


/* -(  Actions  )------------------------------------------------------------ */

  abstract public function getActions($rule_type);

  public function getActionNameMap($rule_type) {
    switch ($rule_type) {
      case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL:
      case HeraldRuleTypeConfig::RULE_TYPE_OBJECT:
        return array(
          self::ACTION_NOTHING      => pht('Do nothing'),
          self::ACTION_ADD_CC       => pht('Add emails to CC'),
          self::ACTION_REMOVE_CC    => pht('Remove emails from CC'),
          self::ACTION_EMAIL        => pht('Send an email to'),
          self::ACTION_AUDIT        => pht('Trigger an Audit by'),
          self::ACTION_FLAG         => pht('Mark with flag'),
          self::ACTION_ASSIGN_TASK  => pht('Assign task to'),
          self::ACTION_ADD_PROJECTS => pht('Add projects'),
          self::ACTION_ADD_REVIEWERS => pht('Add reviewers'),
          self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'),
          self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'),
          self::ACTION_BLOCK => pht('Block change with message'),
        );
      case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL:
        return array(
          self::ACTION_NOTHING      => pht('Do nothing'),
          self::ACTION_ADD_CC       => pht('Add me to CC'),
          self::ACTION_REMOVE_CC    => pht('Remove me from CC'),
          self::ACTION_EMAIL        => pht('Send me an email'),
          self::ACTION_AUDIT        => pht('Trigger an Audit by me'),
          self::ACTION_FLAG         => pht('Mark with flag'),
          self::ACTION_ASSIGN_TASK  => pht('Assign task to me'),
          self::ACTION_ADD_PROJECTS => pht('Add projects'),
          self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'),
          self::ACTION_ADD_BLOCKING_REVIEWERS =>
            pht('Add me as a blocking reviewer'),
        );
      default:
        throw new Exception("Unknown rule type '{$rule_type}'!");
    }
  }

  public function willSaveAction(
    HeraldRule $rule,
    HeraldAction $action) {

    $target = $action->getTarget();
    if (is_array($target)) {
      $target = array_keys($target);
    }

    $author_phid = $rule->getAuthorPHID();

    $rule_type = $rule->getRuleType();
    if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) {
      switch ($action->getAction()) {
        case self::ACTION_EMAIL:
        case self::ACTION_ADD_CC:
        case self::ACTION_REMOVE_CC:
        case self::ACTION_AUDIT:
        case self::ACTION_ASSIGN_TASK:
        case self::ACTION_ADD_REVIEWERS:
        case self::ACTION_ADD_BLOCKING_REVIEWERS:
          // For personal rules, force these actions to target the rule owner.
          $target = array($author_phid);
          break;
        case self::ACTION_FLAG:
          // Make sure flag color is valid; set to blue if not.
          $color_map = PhabricatorFlagColor::getColorNameMap();
          if (empty($color_map[$target])) {
            $target = PhabricatorFlagColor::COLOR_BLUE;
          }
          break;
        case self::ACTION_BLOCK:
        case self::ACTION_NOTHING:
          break;
        default:
          throw new HeraldInvalidActionException(
            pht(
              'Unrecognized action type "%s"!',
              $action->getAction()));
      }
    }

    $action->setTarget($target);
  }



/* -(  Values  )------------------------------------------------------------- */


  public function getValueTypeForFieldAndCondition($field, $condition) {

    if ($this->isHeraldCustomKey($field)) {
      $value_type = $this->getCustomFieldValueTypeForFieldAndCondition(
        $field,
        $condition);
      if ($value_type !== null) {
        return $value_type;
      }
    }

    switch ($condition) {
      case self::CONDITION_CONTAINS:
      case self::CONDITION_NOT_CONTAINS:
      case self::CONDITION_REGEXP:
      case self::CONDITION_REGEXP_PAIR:
        return self::VALUE_TEXT;
      case self::CONDITION_IS:
      case self::CONDITION_IS_NOT:
        switch ($field) {
          case self::FIELD_CONTENT_SOURCE:
            return self::VALUE_CONTENT_SOURCE;
          default:
            return self::VALUE_TEXT;
        }
        break;
      case self::CONDITION_IS_ANY:
      case self::CONDITION_IS_NOT_ANY:
        switch ($field) {
          case self::FIELD_REPOSITORY:
            return self::VALUE_REPOSITORY;
          case self::FIELD_TASK_PRIORITY:
            return self::VALUE_TASK_PRIORITY;
          case self::FIELD_ARCANIST_PROJECT:
            return self::VALUE_ARCANIST_PROJECT;
          default:
            return self::VALUE_USER;
        }
        break;
      case self::CONDITION_INCLUDE_ALL:
      case self::CONDITION_INCLUDE_ANY:
      case self::CONDITION_INCLUDE_NONE:
        switch ($field) {
          case self::FIELD_REPOSITORY:
            return self::VALUE_REPOSITORY;
          case self::FIELD_CC:
            return self::VALUE_EMAIL;
          case self::FIELD_TAGS:
            return self::VALUE_TAG;
          case self::FIELD_AFFECTED_PACKAGE:
            return self::VALUE_OWNERS_PACKAGE;
          case self::FIELD_AUTHOR_PROJECTS:
          case self::FIELD_PUSHER_PROJECTS:
          case self::FIELD_PROJECTS:
          case self::FIELD_REPOSITORY_PROJECTS:
            return self::VALUE_PROJECT;
          case self::FIELD_REVIEWERS:
            return self::VALUE_USER_OR_PROJECT;
          default:
            return self::VALUE_USER;
        }
        break;
      case self::CONDITION_IS_ME:
      case self::CONDITION_IS_NOT_ME:
      case self::CONDITION_EXISTS:
      case self::CONDITION_NOT_EXISTS:
      case self::CONDITION_UNCONDITIONALLY:
      case self::CONDITION_NEVER:
      case self::CONDITION_IS_TRUE:
      case self::CONDITION_IS_FALSE:
        return self::VALUE_NONE;
      case self::CONDITION_RULE:
      case self::CONDITION_NOT_RULE:
        return self::VALUE_RULE;
      default:
        throw new Exception("Unknown condition '{$condition}'.");
    }
  }

  public static function getValueTypeForAction($action, $rule_type) {
    $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL);

    if ($is_personal) {
      switch ($action) {
        case self::ACTION_ADD_CC:
        case self::ACTION_REMOVE_CC:
        case self::ACTION_EMAIL:
        case self::ACTION_NOTHING:
        case self::ACTION_AUDIT:
        case self::ACTION_ASSIGN_TASK:
        case self::ACTION_ADD_REVIEWERS:
        case self::ACTION_ADD_BLOCKING_REVIEWERS:
          return self::VALUE_NONE;
        case self::ACTION_FLAG:
          return self::VALUE_FLAG_COLOR;
        case self::ACTION_ADD_PROJECTS:
          return self::VALUE_PROJECT;
        default:
          throw new Exception("Unknown or invalid action '{$action}'.");
      }
    } else {
      switch ($action) {
        case self::ACTION_ADD_CC:
        case self::ACTION_REMOVE_CC:
        case self::ACTION_EMAIL:
          return self::VALUE_EMAIL;
        case self::ACTION_NOTHING:
          return self::VALUE_NONE;
        case self::ACTION_ADD_PROJECTS:
          return self::VALUE_PROJECT;
        case self::ACTION_FLAG:
          return self::VALUE_FLAG_COLOR;
        case self::ACTION_ASSIGN_TASK:
          return self::VALUE_USER;
        case self::ACTION_AUDIT:
        case self::ACTION_ADD_REVIEWERS:
        case self::ACTION_ADD_BLOCKING_REVIEWERS:
          return self::VALUE_USER_OR_PROJECT;
        case self::ACTION_APPLY_BUILD_PLANS:
          return self::VALUE_BUILD_PLAN;
        case self::ACTION_BLOCK:
          return self::VALUE_TEXT;
        default:
          throw new Exception("Unknown or invalid action '{$action}'.");
      }
    }
  }


/* -(  Repetition  )--------------------------------------------------------- */


  public function getRepetitionOptions() {
    return array(
      HeraldRepetitionPolicyConfig::EVERY,
    );
  }


  public static function applyFlagEffect(HeraldEffect $effect, $phid) {
    $color = $effect->getTarget();

    // TODO: Silly that we need to load this again here.
    $rule = id(new HeraldRule())->load($effect->getRuleID());
    $user = id(new PhabricatorUser())->loadOneWhere(
      'phid = %s',
      $rule->getAuthorPHID());

    $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid);
    if ($flag) {
      return new HeraldApplyTranscript(
        $effect,
        false,
        pht('Object already flagged.'));
    }

    $handle = id(new PhabricatorHandleQuery())
      ->setViewer($user)
      ->withPHIDs(array($phid))
      ->executeOne();

    $flag = new PhabricatorFlag();
    $flag->setOwnerPHID($user->getPHID());
    $flag->setType($handle->getType());
    $flag->setObjectPHID($handle->getPHID());

    // TOOD: Should really be transcript PHID, but it doesn't exist yet.
    $flag->setReasonPHID($user->getPHID());

    $flag->setColor($color);
    $flag->setNote(
      pht('Flagged by Herald Rule "%s".', $rule->getName()));
    $flag->save();

    return new HeraldApplyTranscript(
      $effect,
      true,
      pht('Added flag.'));
  }

  public static function getAllAdapters() {
    static $adapters;
    if (!$adapters) {
      $adapters = id(new PhutilSymbolLoader())
        ->setAncestorClass(__CLASS__)
        ->loadObjects();
      $adapters = msort($adapters, 'getAdapterSortKey');
    }
    return $adapters;
  }

  public static function getAdapterForContentType($content_type) {
    $adapters = self::getAllAdapters();

    foreach ($adapters as $adapter) {
      if ($adapter->getAdapterContentType() == $content_type) {
        return $adapter;
      }
    }

    throw new Exception(
      pht(
        'No adapter exists for Herald content type "%s".',
        $content_type));
  }

  public static function getEnabledAdapterMap(PhabricatorUser $viewer) {
    $map = array();

    $adapters = HeraldAdapter::getAllAdapters();
    foreach ($adapters as $adapter) {
      if (!$adapter->isAvailableToUser($viewer)) {
        continue;
      }
      $type = $adapter->getAdapterContentType();
      $name = $adapter->getAdapterContentName();
      $map[$type] = $name;
    }

    return $map;
  }

  public function renderRuleAsText(HeraldRule $rule, array $handles) {
    assert_instances_of($handles, 'PhabricatorObjectHandle');

    require_celerity_resource('herald-css');

    $icon = id(new PHUIIconView())
      ->setIconFont('fa-chevron-circle-right lightgreytext')
      ->addClass('herald-list-icon');

    if ($rule->getMustMatchAll()) {
      $match_text = pht('When all of these conditions are met:');
    } else {
      $match_text = pht('When any of these conditions are met:');
    }

    $match_title = phutil_tag(
      'p',
      array(
        'class' => 'herald-list-description'
      ),
      $match_text);

    $match_list = array();
    foreach ($rule->getConditions() as $condition) {
      $match_list[] = phutil_tag(
        'div',
        array(
          'class' => 'herald-list-item'
        ),
        array(
          $icon,
          $this->renderConditionAsText($condition, $handles)));
    }

    $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt(
      HeraldRepetitionPolicyConfig::EVERY);

    if ($rule->getRepetitionPolicy() == $integer_code_for_every) {
      $action_text =
        pht('Take these actions every time this rule matches:');
    } else {
      $action_text =
        pht('Take these actions the first time this rule matches:');
    }

    $action_title = phutil_tag(
      'p',
      array(
        'class' => 'herald-list-description'
      ),
      $action_text);

    $action_list = array();
    foreach ($rule->getActions() as $action) {
      $action_list[] = phutil_tag(
        'div',
        array(
          'class' => 'herald-list-item'
        ),
        array(
          $icon,
          $this->renderActionAsText($action, $handles)));    }

    return array(
      $match_title,
      $match_list,
      $action_title,
      $action_list);
  }

  private function renderConditionAsText(
    HeraldCondition $condition,
    array $handles) {

    $field_type = $condition->getFieldName();

    $default = $this->isHeraldCustomKey($field_type)
      ? pht('(Unknown Custom Field "%s")', $field_type)
      : pht('(Unknown Field "%s")', $field_type);

    $field_name = idx($this->getFieldNameMap(), $field_type, $default);

    $condition_type = $condition->getFieldCondition();
    $condition_name = idx($this->getConditionNameMap(), $condition_type);

    $value = $this->renderConditionValueAsText($condition, $handles);

    return hsprintf('    %s %s %s', $field_name, $condition_name, $value);
  }

  private function renderActionAsText(
    HeraldAction $action,
    array $handles) {
    $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL;

    $action_type = $action->getAction();
    $action_name = idx($this->getActionNameMap($rule_global), $action_type);

    $target = $this->renderActionTargetAsText($action, $handles);

    return hsprintf('    %s %s', $action_name, $target);
  }

  private function renderConditionValueAsText(
    HeraldCondition $condition,
    array $handles) {

    $value = $condition->getValue();
    if (!is_array($value)) {
      $value = array($value);
    }
    switch ($condition->getFieldName()) {
      case self::FIELD_TASK_PRIORITY:
        $priority_map = ManiphestTaskPriority::getTaskPriorityMap();
        foreach ($value as $index => $val) {
          $name = idx($priority_map, $val);
          if ($name) {
            $value[$index] = $name;
          }
        }
        break;
      case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE:
        $change_map =
          PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions();
        foreach ($value as $index => $val) {
          $name = idx($change_map, $val);
          if ($name) {
            $value[$index] = $name;
          }
        }
        break;
      default:
        foreach ($value as $index => $val) {
          $handle = idx($handles, $val);
          if ($handle) {
            $value[$index] = $handle->renderLink();
          }
        }
        break;
    }
    $value = phutil_implode_html(', ', $value);
    return $value;
  }

  private function renderActionTargetAsText(
    HeraldAction $action,
    array $handles) {

    $target = $action->getTarget();
    if (!is_array($target)) {
      $target = array($target);
    }
    foreach ($target as $index => $val) {
      switch ($action->getAction()) {
        case self::ACTION_FLAG:
          $target[$index] = PhabricatorFlagColor::getColorName($val);
          break;
        default:
          $handle = idx($handles, $val);
          if ($handle) {
            $target[$index] = $handle->renderLink();
          }
          break;
      }
    }
    $target = phutil_implode_html(', ', $target);
    return $target;
  }

  /**
   * Given a @{class:HeraldRule}, this function extracts all the phids that
   * we'll want to load as handles later.
   *
   * This function performs a somewhat hacky approach to figuring out what
   * is and is not a phid - try to get the phid type and if the type is
   * *not* unknown assume its a valid phid.
   *
   * Don't try this at home. Use more strongly typed data at home.
   *
   * Think of the children.
   */
  public static function getHandlePHIDs(HeraldRule $rule) {
    $phids = array($rule->getAuthorPHID());
    foreach ($rule->getConditions() as $condition) {
      $value = $condition->getValue();
      if (!is_array($value)) {
        $value = array($value);
      }
      foreach ($value as $val) {
        if (phid_get_type($val) !=
            PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
          $phids[] = $val;
        }
      }
    }

    foreach ($rule->getActions() as $action) {
      $target = $action->getTarget();
      if (!is_array($target)) {
        $target = array($target);
      }
      foreach ($target as $val) {
        if (phid_get_type($val) !=
            PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) {
          $phids[] = $val;
        }
      }
    }

    if ($rule->isObjectRule()) {
      $phids[] = $rule->getTriggerObjectPHID();
    }

    return $phids;
  }

/* -(  Custom Field Integration  )------------------------------------------- */


  /**
   * Return an object which custom fields can be generated from while editing
   * rules. Adapters must return an object from this method to enable custom
   * field rules.
   *
   * Normally, you'll return an empty version of the adapted object, assuming
   * it implements @{interface:PhabricatorCustomFieldInterface}:
   *
   *   return new ApplicationObject();
   *
   * This is normally the only adapter method you need to override to enable
   * Herald rules to run against custom fields.
   *
   * @return null|PhabricatorCustomFieldInterface Template object.
   * @task customfield
   */
  protected function getCustomFieldTemplateObject() {
    return null;
  }


  /**
   * Returns the prefix used to namespace Herald fields which are based on
   * custom fields.
   *
   * @return string Key prefix.
   * @task customfield
   */
  private function getCustomKeyPrefix() {
    return 'herald.custom/';
  }


  /**
   * Determine if a field key is based on a custom field or a regular internal
   * field.
   *
   * @param string Field key.
   * @return bool True if the field key is based on a custom field.
   * @task customfield
   */
  private function isHeraldCustomKey($key) {
    $prefix = $this->getCustomKeyPrefix();
    return (strncmp($key, $prefix, strlen($prefix)) == 0);
  }


  /**
   * Convert a custom field key into a Herald field key.
   *
   * @param string Custom field key.
   * @return string Herald field key.
   * @task customfield
   */
  private function getHeraldKeyFromCustomKey($key) {
    return $this->getCustomKeyPrefix().$key;
  }


  /**
   * Get custom fields for this adapter, if appliable. This will either return
   * a field list or `null` if the adapted object does not implement custom
   * fields or the adapter does not support them.
   *
   * @return PhabricatorCustomFieldList|null List of fields, or `null`.
   * @task customfield
   */
  private function getCustomFields() {
    if ($this->customFields === false) {
      $this->customFields = null;


      $template_object = $this->getCustomFieldTemplateObject();
      if ($template_object) {
        $object = $this->getObject();
        if (!$object) {
          $object = $template_object;
        }

        $fields = PhabricatorCustomField::getObjectFields(
          $object,
          PhabricatorCustomField::ROLE_HERALD);
        $fields->setViewer(PhabricatorUser::getOmnipotentUser());
        $fields->readFieldsFromStorage($object);

        $this->customFields = $fields;
      }
    }

    return $this->customFields;
  }


  /**
   * Get a custom field by Herald field key, or `null` if it does not exist
   * or custom fields are not supported.
   *
   * @param string Herald field key.
   * @return PhabricatorCustomField|null Matching field, if it exists.
   * @task customfield
   */
  private function getCustomField($herald_field_key) {
    $fields = $this->getCustomFields();
    if (!$fields) {
      return null;
    }

    foreach ($fields->getFields() as $custom_field) {
      $key = $custom_field->getFieldKey();
      if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) {
        return $custom_field;
      }
    }

    return null;
  }


  /**
   * Get the field map for custom fields.
   *
   * @return map<string, string> Map of Herald field keys to field names.
   * @task customfield
   */
  private function getCustomFieldNameMap() {
    $fields = $this->getCustomFields();
    if (!$fields) {
      return array();
    }

    $map = array();
    foreach ($fields->getFields() as $field) {
      $key = $field->getFieldKey();
      $name = $field->getHeraldFieldName();
      $map[$this->getHeraldKeyFromCustomKey($key)] = $name;
    }

    return $map;
  }


  /**
   * Get the value for a custom field.
   *
   * @param string Herald field key.
   * @return wild Custom field value.
   * @task customfield
   */
  private function getCustomFieldValue($field_key) {
    $field = $this->getCustomField($field_key);
    if (!$field) {
      return null;
    }

    return $field->getHeraldFieldValue();
  }


  /**
   * Get the Herald conditions for a custom field.
   *
   * @param string Herald field key.
   * @return list<const> List of Herald conditions.
   * @task customfield
   */
  private function getCustomFieldConditions($field_key) {
    $field = $this->getCustomField($field_key);
    if (!$field) {
      return array(
        self::CONDITION_NEVER,
      );
    }

    return $field->getHeraldFieldConditions();
  }


  /**
   * Get the Herald value type for a custom field and condition.
   *
   * @param string Herald field key.
   * @param const Herald condition constant.
   * @return const|null Herald value type constant, or null to use the default.
   * @task customfield
   */
  private function getCustomFieldValueTypeForFieldAndCondition(
    $field_key,
    $condition) {

    $field = $this->getCustomField($field_key);
    if (!$field) {
      return self::VALUE_NONE;
    }

    return $field->getHeraldFieldValueType($condition);
  }


}
