<?php

namespace Drupal\Core\Form;

use Drupal\Component\Utility\NestedArray;

/**
 * Stores information about the state of a subform.
 */
class SubformState extends FormStateDecoratorBase implements SubformStateInterface {

  use FormStateValuesTrait;

  /**
   * The parent form.
   *
   * @var mixed[]
   */
  protected $parentForm;

  /**
   * The subform.
   *
   * @var mixed[]
   */
  protected $subform;

  /**
   * Constructs a new instance.
   *
   * @param mixed[] $subform
   *   The subform for which to create a form state.
   * @param mixed[] $parent_form
   *   The subform's parent form.
   * @param \Drupal\Core\Form\FormStateInterface $parent_form_state
   *   The parent form state.
   */
  protected function __construct(array &$subform, array &$parent_form, FormStateInterface $parent_form_state) {
    $this->decoratedFormState = $parent_form_state;
    $this->parentForm = $parent_form;
    $this->subform = $subform;
  }

  /**
   * Creates a new instance for a subform.
   *
   * @param mixed[] $subform
   *   The subform for which to create a form state.
   * @param mixed[] $parent_form
   *   The subform's parent form.
   * @param \Drupal\Core\Form\FormStateInterface $parent_form_state
   *   The parent form state.
   *
   * @return static
   */
  public static function createForSubform(array &$subform, array &$parent_form, FormStateInterface $parent_form_state) {
    return new static($subform, $parent_form, $parent_form_state);
  }

  /**
   * Gets the subform's parents relative to its parent form.
   *
   * @param string $property
   *   The property name (#parents or #array_parents).
   *
   * @return mixed
   *
   * @throws \InvalidArgumentException
   *   Thrown when the requested property does not exist.
   * @throws \UnexpectedValueException
   *   Thrown when the subform is not contained by the given parent form.
   */
  protected function getParents($property) {
    foreach ([$this->subform, $this->parentForm] as $form) {
      if (!isset($form[$property]) || !is_array($form[$property])) {
        throw new \RuntimeException(sprintf('The subform and parent form must contain the %s property, which must be an array. Try calling this method from a #process callback instead.', $property));
      }
    }

    $relative_subform_parents = $this->subform[$property];
    // Remove all of the subform's parents that are also the parent form's
    // parents, so we are left with the parents relative to the parent form.
    foreach ($this->parentForm[$property] as $parent_form_parent) {
      if ($parent_form_parent !== $relative_subform_parents[0]) {
        // The parent form's parents are the subform's parents as well. If we
        // find no match, that means the given subform is not contained by the
        // given parent form.
        throw new \UnexpectedValueException('The subform is not contained by the given parent form.');
      }
      array_shift($relative_subform_parents);
    }

    return $relative_subform_parents;
  }

  /**
   * {@inheritdoc}
   */
  public function &getValues() {
    $exists = NULL;
    $values = &NestedArray::getValue(parent::getValues(), $this->getParents('#parents'), $exists);
    if (!$exists) {
      $values = [];
    }
    elseif (!is_array($values)) {
      throw new \UnexpectedValueException('The form state values do not belong to the subform.');
    }

    return $values;
  }

  /**
   * {@inheritdoc}
   */
  public function getCompleteFormState() {
    return $this->decoratedFormState instanceof SubformStateInterface ? $this->decoratedFormState->getCompleteFormState() : $this->decoratedFormState;
  }

  /**
   * {@inheritdoc}
   */
  public function setLimitValidationErrors($limit_validation_errors) {
    if (is_array($limit_validation_errors)) {
      $limit_validation_errors = array_merge($this->getParents('#parents'), $limit_validation_errors);
    }

    return parent::setLimitValidationErrors($limit_validation_errors);
  }

  /**
   * {@inheritdoc}
   */
  public function getLimitValidationErrors() {
    $limit_validation_errors = parent::getLimitValidationErrors();
    if (is_array($limit_validation_errors)) {
      return array_slice($limit_validation_errors, count($this->getParents('#parents')));

    }
    return $limit_validation_errors;
  }

  /**
   * {@inheritdoc}
   */
  public function setErrorByName($name, $message = '') {
    $parents = $this->subform['#array_parents'];
    $parents[] = $name;
    $name = implode('][', $parents);
    parent::setErrorByName($name, $message);

    return $this;
  }

}
