<?php

abstract class PhabricatorAuthProvider {

  private $providerConfig;

  public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
    $this->providerConfig = $config;
    return $this;
  }

  public function hasProviderConfig() {
    return (bool)$this->providerConfig;
  }

  public function getProviderConfig() {
    if ($this->providerConfig === null) {
      throw new Exception(
        "Call attachProviderConfig() before getProviderConfig()!");
    }
    return $this->providerConfig;
  }

  public function getConfigurationHelp() {
    return null;
  }

  public function getDefaultProviderConfig() {
    return id(new PhabricatorAuthProviderConfig())
      ->setProviderClass(get_class($this))
      ->setIsEnabled(1)
      ->setShouldAllowLogin(1)
      ->setShouldAllowRegistration(1)
      ->setShouldAllowLink(1)
      ->setShouldAllowUnlink(1);
  }

  public function getNameForCreate() {
    return $this->getProviderName();
  }

  public function getDescriptionForCreate() {
    return null;
  }

  public function getProviderKey() {
    return $this->getAdapter()->getAdapterKey();
  }

  public function getProviderType() {
    return $this->getAdapter()->getAdapterType();
  }

  public function getProviderDomain() {
    return $this->getAdapter()->getAdapterDomain();
  }

  public static function getAllBaseProviders() {
    static $providers;

    if ($providers === null) {
      $objects = id(new PhutilSymbolLoader())
        ->setAncestorClass(__CLASS__)
        ->loadObjects();
      $providers = $objects;
    }

    return $providers;
  }

  public static function getAllProviders() {
    static $providers;

    if ($providers === null) {
      $objects = self::getAllBaseProviders();

      $configs = id(new PhabricatorAuthProviderConfigQuery())
        ->setViewer(PhabricatorUser::getOmnipotentUser())
        ->execute();

      $providers = array();
      foreach ($configs as $config) {
        if (!isset($objects[$config->getProviderClass()])) {
          // This configuration is for a provider which is not installed.
          continue;
        }

        $object = clone $objects[$config->getProviderClass()];
        $object->attachProviderConfig($config);

        $key = $object->getProviderKey();
        if (isset($providers[$key])) {
          throw new Exception(
            pht(
              "Two authentication providers use the same provider key ".
              "('%s'). Each provider must be identified by a unique ".
              "key.",
              $key));
        }
        $providers[$key] = $object;
      }
    }

    return $providers;
  }

  public static function getAllEnabledProviders() {
    $providers = self::getAllProviders();
    foreach ($providers as $key => $provider) {
      if (!$provider->isEnabled()) {
        unset($providers[$key]);
      }
    }
    return $providers;
  }

  public static function getEnabledProviderByKey($provider_key) {
    return idx(self::getAllEnabledProviders(), $provider_key);
  }

  abstract public function getProviderName();
  abstract public function getAdapter();

  public function isEnabled() {
    return $this->getProviderConfig()->getIsEnabled();
  }

  public function shouldAllowLogin() {
    return $this->getProviderConfig()->getShouldAllowLogin();
  }

  public function shouldAllowRegistration() {
    return $this->getProviderConfig()->getShouldAllowRegistration();
  }

  public function shouldAllowAccountLink() {
    return $this->getProviderConfig()->getShouldAllowLink();
  }

  public function shouldAllowAccountUnlink() {
    return $this->getProviderConfig()->getShouldAllowUnlink();
  }

  public function buildLoginForm(
    PhabricatorAuthStartController $controller) {
    return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
  }

  abstract public function processLoginRequest(
    PhabricatorAuthLoginController $controller);

  public function buildLinkForm(
    PhabricatorAuthLinkController $controller) {
    return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
  }

  public function shouldAllowAccountRefresh() {
    return true;
  }

  public function buildRefreshForm(
    PhabricatorAuthLinkController $controller) {
    return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
  }

  protected function renderLoginForm(
    AphrontRequest $request,
    $mode) {
    throw new Exception("Not implemented!");
  }

  public function createProviders() {
    return array($this);
  }

  protected function willSaveAccount(PhabricatorExternalAccount $account) {
    return;
  }

  public function willRegisterAccount(PhabricatorExternalAccount $account) {
    return;
  }

  protected function loadOrCreateAccount($account_id) {
    if (!strlen($account_id)) {
      throw new Exception(
        "loadOrCreateAccount(...): empty account ID!");
    }

    $adapter = $this->getAdapter();
    $adapter_class = get_class($adapter);

    if (!strlen($adapter->getAdapterType())) {
      throw new Exception(
        "AuthAdapter (of class '{$adapter_class}') has an invalid ".
        "implementation: no adapter type.");
    }

    if (!strlen($adapter->getAdapterDomain())) {
      throw new Exception(
        "AuthAdapter (of class '{$adapter_class}') has an invalid ".
        "implementation: no adapter domain.");
    }

    $account = id(new PhabricatorExternalAccount())->loadOneWhere(
      'accountType = %s AND accountDomain = %s AND accountID = %s',
      $adapter->getAdapterType(),
      $adapter->getAdapterDomain(),
      $account_id);
    if (!$account) {
      $account = id(new PhabricatorExternalAccount())
        ->setAccountType($adapter->getAdapterType())
        ->setAccountDomain($adapter->getAdapterDomain())
        ->setAccountID($account_id);
    }

    $account->setUsername($adapter->getAccountName());
    $account->setRealName($adapter->getAccountRealName());
    $account->setEmail($adapter->getAccountEmail());
    $account->setAccountURI($adapter->getAccountURI());

    $account->setProfileImagePHID(null);
    $image_uri = $adapter->getAccountImageURI();
    if ($image_uri) {
      try {
        $name = PhabricatorSlug::normalize($this->getProviderName());
        $name = $name.'-profile.jpg';

        // TODO: If the image has not changed, we do not need to make a new
        // file entry for it, but there's no convenient way to do this with
        // PhabricatorFile right now. The storage will get shared, so the impact
        // here is negligible.
        $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
          $image_file = PhabricatorFile::newFromFileDownload(
            $image_uri,
            array(
              'name' => $name,
            ));
        unset($unguarded);

        if ($image_file) {
          $account->setProfileImagePHID($image_file->getPHID());
        }
      } catch (Exception $ex) {
        // Log this but proceed, it's not especially important that we
        // be able to pull profile images.
        phlog($ex);
      }
    }

    $this->willSaveAccount($account);

    $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
      $account->save();
    unset($unguarded);

    return $account;
  }

  public function getLoginURI() {
    $app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth');
    $uri = $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
    return PhabricatorEnv::getURI($uri);
  }

  public function getSettingsURI() {
    return '/settings/panel/external/';
  }

  public function getStartURI() {
    $app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth');
    $uri = $app->getApplicationURI('/start/');
    return $uri;
  }

  public function isDefaultRegistrationProvider() {
    return false;
  }

  public function shouldRequireRegistrationPassword() {
    return false;
  }

  public function getDefaultExternalAccount() {
    throw new Exception("Not implemented!");
  }

  public function getLoginOrder() {
    return '500-'.$this->getProviderName();
  }

  protected function getLoginIcon() {
    return 'Generic';
  }

  public function isLoginFormAButton() {
    return false;
  }

  public function renderConfigPropertyTransactionTitle(
    PhabricatorAuthProviderConfigTransaction $xaction) {

    return null;
  }

  public function readFormValuesFromProvider() {
    return array();
  }

  public function readFormValuesFromRequest(AphrontRequest $request) {
    return array();
  }

  public function processEditForm(
    AphrontRequest $request,
    array $values) {

    $errors = array();
    $issues = array();

    return array($errors, $issues, $values);
  }

  public function extendEditForm(
    AphrontRequest $request,
    AphrontFormView $form,
    array $values,
    array $issues) {
    return;
  }

  public function willRenderLinkedAccount(
    PhabricatorUser $viewer,
    PHUIObjectItemView $item,
    PhabricatorExternalAccount $account) {

    $account_view = id(new PhabricatorAuthAccountView())
      ->setExternalAccount($account)
      ->setAuthProvider($this);

    $item->appendChild(
      phutil_tag(
        'div',
        array(
          'class' => 'mmr mml mst mmb',
        ),
        $account_view));
  }


  /**
   * Return true to use a two-step configuration (setup, configure) instead of
   * the default single-step configuration. In practice, this means that
   * creating a new provider instance will redirect back to the edit page
   * instead of the provider list.
   *
   * @return bool True if this provider uses two-step configuration.
   */
  public function hasSetupStep() {
    return false;
  }


  /**
   * Render a standard login/register button element.
   *
   * The `$attributes` parameter takes these keys:
   *
   *   - `uri`: URI the button should take the user to when clicked.
   *   - `method`: Optional HTTP method the button should use, defaults to GET.
   *
   * @param   AphrontRequest  HTTP request.
   * @param   string          Request mode string.
   * @param   map             Additional parameters, see above.
   * @return  wild            Login button.
   */
  protected function renderStandardLoginButton(
    AphrontRequest $request,
    $mode,
    array $attributes = array()) {

    PhutilTypeSpec::checkMap(
      $attributes,
      array(
        'method' => 'optional string',
        'uri' => 'string',
        'sigil' => 'optional string',
      ));

    $viewer = $request->getUser();
    $adapter = $this->getAdapter();

    if ($mode == 'link') {
      $button_text = pht('Link External Account');
    } else if ($mode == 'refresh') {
      $button_text = pht('Refresh Account Link');
    } else if ($this->shouldAllowRegistration()) {
      $button_text = pht('Login or Register');
    } else {
      $button_text = pht('Login');
    }

    $icon = id(new PHUIIconView())
      ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
      ->setSpriteIcon($this->getLoginIcon());

    $button = id(new PHUIButtonView())
      ->setSize(PHUIButtonView::BIG)
      ->setColor(PHUIButtonView::GREY)
      ->setIcon($icon)
      ->setText($button_text)
      ->setSubtext($this->getProviderName());

    $uri = $attributes['uri'];
    $uri = new PhutilURI($uri);
    $params = $uri->getQueryParams();
    $uri->setQueryParams(array());

    $content = array($button);

    foreach ($params as $key => $value) {
      $content[] = phutil_tag(
        'input',
        array(
          'type' => 'hidden',
          'name' => $key,
          'value' => $value,
        ));
    }

    return phabricator_form(
      $viewer,
      array(
        'method' => idx($attributes, 'method', 'GET'),
        'action' => (string)$uri,
        'sigil'  => idx($attributes, 'sigil'),
      ),
      $content);
  }

}
