<?php

final class PhabricatorProjectTransactionEditor
  extends PhabricatorApplicationTransactionEditor {

  public function getTransactionTypes() {
    $types = parent::getTransactionTypes();

    $types[] = PhabricatorTransactions::TYPE_EDGE;
    $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
    $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
    $types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;

    $types[] = PhabricatorProjectTransaction::TYPE_NAME;
    $types[] = PhabricatorProjectTransaction::TYPE_SLUGS;
    $types[] = PhabricatorProjectTransaction::TYPE_STATUS;
    $types[] = PhabricatorProjectTransaction::TYPE_IMAGE;

    return $types;
  }

  protected function getCustomTransactionOldValue(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    switch ($xaction->getTransactionType()) {
      case PhabricatorProjectTransaction::TYPE_NAME:
        return $object->getName();
      case PhabricatorProjectTransaction::TYPE_SLUGS:
        $slugs = $object->getSlugs();
        $slugs = mpull($slugs, 'getSlug', 'getSlug');
        unset($slugs[$object->getPrimarySlug()]);
        return $slugs;
      case PhabricatorProjectTransaction::TYPE_STATUS:
        return $object->getStatus();
      case PhabricatorProjectTransaction::TYPE_IMAGE:
        return $object->getProfileImagePHID();
    }

    return parent::getCustomTransactionOldValue($object, $xaction);
  }

  protected function getCustomTransactionNewValue(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    switch ($xaction->getTransactionType()) {
      case PhabricatorProjectTransaction::TYPE_NAME:
      case PhabricatorProjectTransaction::TYPE_SLUGS:
      case PhabricatorProjectTransaction::TYPE_STATUS:
      case PhabricatorProjectTransaction::TYPE_IMAGE:
        return $xaction->getNewValue();
    }

    return parent::getCustomTransactionNewValue($object, $xaction);
  }

  protected function applyCustomInternalTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    switch ($xaction->getTransactionType()) {
      case PhabricatorProjectTransaction::TYPE_NAME:
        $object->setName($xaction->getNewValue());
        $object->setPhrictionSlug($xaction->getNewValue());
        return;
      case PhabricatorProjectTransaction::TYPE_SLUGS:
        return;
      case PhabricatorProjectTransaction::TYPE_STATUS:
        $object->setStatus($xaction->getNewValue());
        return;
      case PhabricatorProjectTransaction::TYPE_IMAGE:
        $object->setProfileImagePHID($xaction->getNewValue());
        return;
      case PhabricatorTransactions::TYPE_EDGE:
        return;
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
        $object->setViewPolicy($xaction->getNewValue());
        return;
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
        $object->setEditPolicy($xaction->getNewValue());
        return;
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
        $object->setJoinPolicy($xaction->getNewValue());
        return;
    }

    return parent::applyCustomInternalTransaction($object, $xaction);
  }

  protected function applyCustomExternalTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    switch ($xaction->getTransactionType()) {
      case PhabricatorProjectTransaction::TYPE_NAME:
        $new_slug = id(new PhabricatorProjectSlug())
          ->setSlug($object->getPrimarySlug())
          ->setProjectPHID($object->getPHID())
          ->save();

        if ($xaction->getOldValue() !== null) {
          $clone_object = clone $object;
          $clone_object->setPhrictionSlug($xaction->getOldValue());
          $old_slug = $clone_object->getPrimarySlug();
          $old_slug = id(new PhabricatorProjectSlug())
            ->loadOneWhere('slug = %s', $old_slug);
          if ($old_slug) {
            $old_slug->delete();
          }
        }

        // TODO -- delete all of the below once we sever automagical project
        // to phriction stuff
        if ($xaction->getOldValue() === null) {
          // Project was just created, we don't need to move anything.
          return;
        }

        $clone_object = clone $object;
        $clone_object->setPhrictionSlug($xaction->getOldValue());
        $old_slug = $clone_object->getFullPhrictionSlug();

        $old_document = id(new PhrictionDocument())
          ->loadOneWhere('slug = %s', $old_slug);
        if ($old_document && $old_document->getStatus() ==
            PhrictionDocumentStatus::STATUS_EXISTS) {
          $content = id(new PhrictionContent())
            ->load($old_document->getContentID());
          $from_editor = id(PhrictionDocumentEditor::newForSlug($old_slug))
            ->setActor($this->getActor())
            ->setTitle($content->getTitle())
            ->setContent($content->getContent())
            ->setDescription($content->getDescription());

          $target_editor = id(PhrictionDocumentEditor::newForSlug(
            $object->getFullPhrictionSlug()))
            ->setActor($this->getActor())
            ->setTitle($content->getTitle())
            ->setContent($content->getContent())
            ->setDescription($content->getDescription())
            ->moveHere($old_document->getID(), $old_document->getPHID());

          $target_document = $target_editor->getDocument();
          $from_editor->moveAway($target_document->getID());
        }
        return;
      case PhabricatorProjectTransaction::TYPE_SLUGS:
        $old = $xaction->getOldValue();
        $new = $xaction->getNewValue();
        $add = array_diff($new, $old);
        $rem = array_diff($old, $new);

        if ($add) {
          $add_slug_template = id(new PhabricatorProjectSlug())
            ->setProjectPHID($object->getPHID());
          foreach ($add as $add_slug_str) {
            $add_slug = id(clone $add_slug_template)
              ->setSlug($add_slug_str)
              ->save();
          }
        }
        if ($rem) {
          $rem_slugs = id(new PhabricatorProjectSlug())
            ->loadAllWhere('slug IN (%Ls)', $rem);
          foreach ($rem_slugs as $rem_slug) {
            $rem_slug->delete();
          }
        }
        return;
      case PhabricatorTransactions::TYPE_VIEW_POLICY:
      case PhabricatorTransactions::TYPE_EDIT_POLICY:
      case PhabricatorTransactions::TYPE_JOIN_POLICY:
      case PhabricatorProjectTransaction::TYPE_STATUS:
      case PhabricatorProjectTransaction::TYPE_IMAGE:
        return;
      case PhabricatorTransactions::TYPE_EDGE:
        $edge_type = $xaction->getMetadataValue('edge:type');
        switch ($edge_type) {
          case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
          case PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER:
            $old = $xaction->getOldValue();
            $new = $xaction->getNewValue();

            // When adding members or watchers, we add subscriptions.
            $add = array_keys(array_diff_key($new, $old));

            // When removing members, we remove their subscription too.
            // When unwatching, we leave subscriptions, since it's fine to be
            // subscribed to a project but not be a member of it.
            if ($edge_type == PhabricatorEdgeConfig::TYPE_PROJ_MEMBER) {
              $rem = array_keys(array_diff_key($old, $new));
            } else {
              $rem = array();
            }

            // NOTE: The subscribe is "explicit" because there's no implicit
            // unsubscribe, so Join -> Leave -> Join doesn't resubscribe you
            // if we use an implicit subscribe, even though you never willfully
            // unsubscribed. Not sure if adding implicit unsubscribe (which
            // would not write the unsubscribe row) is justified to deal with
            // this, which is a fairly weird edge case and pretty arguable both
            // ways.

            // Subscriptions caused by watches should also clearly be explicit,
            // and that case is unambiguous.

            id(new PhabricatorSubscriptionsEditor())
              ->setActor($this->requireActor())
              ->setObject($object)
              ->subscribeExplicit($add)
              ->unsubscribe($rem)
              ->save();

            if ($rem) {
              // When removing members, also remove any watches on the project.
              $edge_editor = id(new PhabricatorEdgeEditor())
                ->setSuppressEvents(true);
              foreach ($rem as $rem_phid) {
                $edge_editor->removeEdge(
                  $object->getPHID(),
                  PhabricatorEdgeConfig::TYPE_OBJECT_HAS_WATCHER,
                  $rem_phid);
              }
              $edge_editor->save();
            }
            break;
        }
        return;
    }

    return parent::applyCustomExternalTransaction($object, $xaction);
  }

  protected function validateTransaction(
    PhabricatorLiskDAO $object,
    $type,
    array $xactions) {

    $errors = parent::validateTransaction($object, $type, $xactions);

    switch ($type) {
      case PhabricatorProjectTransaction::TYPE_NAME:
        $missing = $this->validateIsEmptyTextField(
          $object->getName(),
          $xactions);

        if ($missing) {
          $error = new PhabricatorApplicationTransactionValidationError(
            $type,
            pht('Required'),
            pht('Project name is required.'),
            nonempty(last($xactions), null));

          $error->setIsMissingFieldError(true);
          $errors[] = $error;
        }

        if (!$xactions) {
          break;
        }

        $name = last($xactions)->getNewValue();
        $name_used_already = id(new PhabricatorProjectQuery())
          ->setViewer($this->getActor())
          ->withNames(array($name))
          ->executeOne();
        if ($name_used_already &&
           ($name_used_already->getPHID() != $object->getPHID())) {
          $error = new PhabricatorApplicationTransactionValidationError(
            $type,
            pht('Duplicate'),
            pht('Project name is already used.'),
            nonempty(last($xactions), null));
          $errors[] = $error;
        }

        $slug_builder = clone $object;
        $slug_builder->setPhrictionSlug($name);
        $slug = $slug_builder->getPrimarySlug();
        $slug_used_already = id(new PhabricatorProjectSlug())
          ->loadOneWhere('slug = %s', $slug);
        if ($slug_used_already &&
            $slug_used_already->getProjectPHID() != $object->getPHID()) {
          $error = new PhabricatorApplicationTransactionValidationError(
            $type,
            pht('Duplicate'),
            pht('Project name can not be used due to hashtag collision.'),
            nonempty(last($xactions), null));
          $errors[] = $error;
        }
        break;
      case PhabricatorProjectTransaction::TYPE_SLUGS:
        if (!$xactions) {
          break;
        }

        $slug_xaction = last($xactions);
        $new = $slug_xaction->getNewValue();
        $slugs_used_already = id(new PhabricatorProjectSlug())
          ->loadAllWhere('slug IN (%Ls)', $new);
        $slugs_used_already = mgroup($slugs_used_already, 'getProjectPHID');
        foreach ($slugs_used_already as $project_phid => $used_slugs) {
          $used_slug_strs = mpull($used_slugs, 'getSlug');
          if ($project_phid == $object->getPHID()) {
            if (in_array($object->getPrimarySlug(), $used_slug_strs)) {
              $error = new PhabricatorApplicationTransactionValidationError(
                $type,
                pht('Invalid'),
                pht(
                  'Project hashtag %s is already the primary hashtag.',
                  $object->getPrimarySlug()),
                $slug_xaction);
              $errors[] = $error;
            }
            continue;
          }

          $error = new PhabricatorApplicationTransactionValidationError(
            $type,
            pht('Invalid'),
            pht(
              '%d project hashtag(s) are already used: %s',
              count($used_slug_strs),
              implode(', ', $used_slug_strs)),
            $slug_xaction);
          $errors[] = $error;
        }

        break;
    }

    return $errors;
  }


  protected function requireCapabilities(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    switch ($xaction->getTransactionType()) {
      case PhabricatorProjectTransaction::TYPE_NAME:
      case PhabricatorProjectTransaction::TYPE_STATUS:
      case PhabricatorProjectTransaction::TYPE_IMAGE:
        PhabricatorPolicyFilter::requireCapability(
          $this->requireActor(),
          $object,
          PhabricatorPolicyCapability::CAN_EDIT);
        return;
      case PhabricatorTransactions::TYPE_EDGE:
        switch ($xaction->getMetadataValue('edge:type')) {
          case PhabricatorEdgeConfig::TYPE_PROJ_MEMBER:
            $old = $xaction->getOldValue();
            $new = $xaction->getNewValue();

            $add = array_keys(array_diff_key($new, $old));
            $rem = array_keys(array_diff_key($old, $new));

            $actor_phid = $this->requireActor()->getPHID();

            $is_join = (($add === array($actor_phid)) && !$rem);
            $is_leave = (($rem === array($actor_phid)) && !$add);

            if ($is_join) {
              // You need CAN_JOIN to join a project.
              PhabricatorPolicyFilter::requireCapability(
                $this->requireActor(),
                $object,
                PhabricatorPolicyCapability::CAN_JOIN);
            } else if ($is_leave) {
              // You don't need any capabilities to leave a project.
            } else {
              // You need CAN_EDIT to change members other than yourself.
              PhabricatorPolicyFilter::requireCapability(
                $this->requireActor(),
                $object,
                PhabricatorPolicyCapability::CAN_EDIT);
            }
            return;
        }
        break;
    }

    return parent::requireCapabilities($object, $xaction);
  }

  protected function supportsSearch() {
    return true;
  }

  protected function extractFilePHIDsFromCustomTransaction(
    PhabricatorLiskDAO $object,
    PhabricatorApplicationTransaction $xaction) {

    switch ($xaction->getTransactionType()) {
      case PhabricatorProjectTransaction::TYPE_IMAGE:
        $new = $xaction->getNewValue();
        if ($new) {
          return array($new);
        }
        break;
    }

    return parent::extractFilePHIDsFromCustomTransaction($object, $xaction);
  }

}
