<?php

namespace Drupal\commerce_product\Form;

use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\commerce\EntityHelper;
use Drupal\commerce\InlineFormManager;
use Drupal\commerce_product\ProductAttributeFieldManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ProductAttributeForm extends BundleEntityFormBase {

  /**
   * The attribute field manager.
   *
   * @var \Drupal\commerce_product\ProductAttributeFieldManagerInterface
   */
  protected $attributeFieldManager;

  /**
   * The inline form manager.
   *
   * @var \Drupal\commerce\InlineFormManager
   */
  protected $inlineFormManager;

  /**
   * Constructs a new ProductAttributeForm object.
   *
   * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager
   *   The attribute field manager.
   * @param \Drupal\commerce\InlineFormManager $inline_form_manager
   *   The inline form manager.
   */
  public function __construct(ProductAttributeFieldManagerInterface $attribute_field_manager, InlineFormManager $inline_form_manager) {
    $this->attributeFieldManager = $attribute_field_manager;
    $this->inlineFormManager = $inline_form_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('commerce_product.attribute_field_manager'),
      $container->get('plugin.manager.commerce_inline_form')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);
    /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */
    $attribute = $this->entity;

    $form['label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name'),
      '#maxlength' => 255,
      '#default_value' => $attribute->label(),
      '#required' => TRUE,
    ];
    $form['id'] = [
      '#type' => 'machine_name',
      '#default_value' => $attribute->id(),
      '#machine_name' => [
        'exists' => '\Drupal\commerce_product\Entity\ProductAttribute::load',
      ],
      // Attribute field names are constructed as 'attribute_' + id, and must
      // not be longer than 32 characters. Account for that prefix length here.
      '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH - 10,
      '#disabled' => !$attribute->isNew(),
    ];
    $form['elementType'] = [
      '#type' => 'select',
      '#title' => $this->t('Element type'),
      '#description' => $this->t('Controls how the attribute is displayed on the add to cart form.'),
      '#options' => [
        'radios' => $this->t('Radio buttons'),
        'select' => $this->t('Select list'),
        'commerce_product_rendered_attribute' => $this->t('Rendered attribute'),
      ],
      '#default_value' => $attribute->getElementType(),
    ];

    $attribute_field_map = $this->attributeFieldManager->getFieldMap();
    $variation_type_storage = $this->entityTypeManager->getStorage('commerce_product_variation_type');
    $variation_types = $variation_type_storage->loadMultiple();
    // Allow the attribute to be assigned to a product variation type.
    $form['original_variation_types'] = [
      '#type' => 'value',
      '#value' => [],
    ];
    $form['variation_types'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Product variation types'),
      '#options' => EntityHelper::extractLabels($variation_types),
      '#access' => count($variation_types) > 0,
    ];
    $disabled_variation_types = [];
    foreach ($variation_types as $variation_type_id => $variation_type) {
      if (!$attribute->isNew() && isset($attribute_field_map[$variation_type_id])) {
        $used_attributes = array_column($attribute_field_map[$variation_type_id], 'attribute_id');
        if (in_array($attribute->id(), $used_attributes)) {
          $form['original_variation_types']['#value'][$variation_type_id] = $variation_type_id;
          $form['variation_types']['#default_value'][$variation_type_id] = $variation_type_id;
          if (!$this->attributeFieldManager->canDeleteField($attribute, $variation_type_id)) {
            $form['variation_types'][$variation_type_id] = [
              '#disabled' => TRUE,
            ];
            $disabled_variation_types[] = $variation_type_id;
          }
        }
      }
    }
    $form['disabled_variation_types'] = [
      '#type' => 'value',
      '#value' => $disabled_variation_types,
    ];

    if ($this->moduleHandler->moduleExists('content_translation')) {
      $enabled = TRUE;
      if (!$attribute->isNew()) {
        $translation_manager = \Drupal::service('content_translation.manager');
        $enabled = $translation_manager->isEnabled('commerce_product_attribute_value', $attribute->id());
      }
      $form['enable_value_translation'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Enable attribute value translation'),
        '#default_value' => $enabled,
      ];
    }
    // The attribute acts as a bundle for attribute values, so the values can't
    // be created until the attribute is saved.
    if (!$attribute->isNew()) {
      $form = $this->buildValuesForm($form, $form_state);
    }

    return $form;
  }

  /**
   * Builds the attribute values form.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The attribute values form.
   */
  public function buildValuesForm(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */
    $attribute = $this->entity;
    $values = $attribute->getValues();
    $user_input = $form_state->getUserInput();
    // Reorder the values by name, if requested.
    if ($form_state->get('reset_alphabetical')) {
      $value_names = EntityHelper::extractLabels($values);
      asort($value_names);
      foreach (array_keys($value_names) as $weight => $id) {
        $values[$id]->setWeight($weight);
      }
    }
    // The value map allows new values to be added and removed before saving.
    // An array in the $index => $id format. $id is '_new' for unsaved values.
    $value_map = (array) $form_state->get('value_map');
    if (empty($value_map)) {
      $value_map = $values ? array_keys($values) : ['_new'];
      $form_state->set('value_map', $value_map);
    }

    $wrapper_id = Html::getUniqueId('product-attribute-values-ajax-wrapper');
    $form['values'] = [
      '#type' => 'table',
      '#header' => [
        ['data' => $this->t('Value'), 'colspan' => 2],
        $this->t('Weight'),
        $this->t('Operations'),
      ],
      '#tabledrag' => [
        [
          'action' => 'order',
          'relationship' => 'sibling',
          'group' => 'product-attribute-value-order-weight',
        ],
      ],
      '#weight' => 5,
      '#prefix' => '<div id="' . $wrapper_id . '">',
      '#suffix' => '</div>',
      // #input defaults to TRUE, which breaks file fields on the value form.
      // This table is used for visual grouping only, the element itself
      // doesn't have any values of its own that need processing.
      '#input' => FALSE,
    ];
    // Make the weight list always reflect the current number of values.
    // Taken from WidgetBase::formMultipleElements().
    $max_weight = count($value_map);

    foreach ($value_map as $index => $id) {
      $value_form = &$form['values'][$index];
      // The tabledrag element is always added to the first cell in the row,
      // so we add an empty cell to guide it there, for better styling.
      $value_form['#attributes']['class'][] = 'draggable';
      $value_form['tabledrag'] = [
        '#markup' => '',
      ];
      if ($id == '_new') {
        $value = $this->entityTypeManager->getStorage('commerce_product_attribute_value')->create([
          'attribute' => $attribute->id(),
          'langcode' => $attribute->get('langcode'),
        ]);
        $default_weight = $max_weight;
        $remove_access = TRUE;
      }
      else {
        $value = $values[$id];
        $default_weight = $value->getWeight();
        $remove_access = $value->access('delete');
      }
      $inline_form = $this->inlineFormManager->createInstance('content_entity', [
        'skip_save' => TRUE,
      ], $value);

      $value_form['entity'] = [
        '#parents' => ['values', $index, 'entity'],
        '#inline_form' => $inline_form,
      ];
      $value_form['entity'] = $inline_form->buildInlineForm($value_form['entity'], $form_state);

      $value_form['weight'] = [
        '#type' => 'weight',
        '#title' => $this->t('Weight'),
        '#title_display' => 'invisible',
        '#delta' => $max_weight,
        '#default_value' => $default_weight,
        '#attributes' => [
          'class' => ['product-attribute-value-order-weight'],
        ],
      ];
      // Used by SortArray::sortByWeightProperty to sort the rows.
      if (isset($user_input['values'][$index])) {
        $input_weight = $user_input['values'][$index]['weight'];
        // If the weights were just reset, reflect it in the user input.
        if ($form_state->get('reset_alphabetical')) {
          $input_weight = $default_weight;
        }
        // Make sure the weight is not out of bounds due to removals.
        if ($user_input['values'][$index]['weight'] > $max_weight) {
          $input_weight = $max_weight;
        }
        // Reflect the updated user input on the element.
        $value_form['weight']['#value'] = $input_weight;

        $value_form['#weight'] = $input_weight;
      }
      else {
        $value_form['#weight'] = $default_weight;
      }

      $value_form['remove'] = [
        '#type' => 'submit',
        '#name' => 'remove_value' . $index,
        '#value' => $this->t('Remove'),
        '#limit_validation_errors' => [],
        '#submit' => ['::removeValueSubmit'],
        '#value_index' => $index,
        '#ajax' => [
          'callback' => '::valuesAjax',
          'wrapper' => $wrapper_id,
        ],
        '#access' => $remove_access,
      ];
    }

    // Sort the values by weight. Ensures weight is preserved on ajax refresh.
    uasort($form['values'], ['\Drupal\Component\Utility\SortArray', 'sortByWeightProperty']);

    $access_handler = $this->entityTypeManager->getAccessControlHandler('commerce_product_attribute_value');
    if ($access_handler->createAccess($attribute->id())) {
      $form['values']['_add_new'] = [
        '#tree' => FALSE,
      ];
      $form['values']['_add_new']['entity'] = [
        '#type' => 'container',
        '#wrapper_attributes' => ['colspan' => 2],
      ];
      $form['values']['_add_new']['entity']['add_value'] = [
        '#type' => 'submit',
        '#value' => $this->t('Add value'),
        '#submit' => ['::addValueSubmit'],
        '#limit_validation_errors' => [],
        '#ajax' => [
          'callback' => '::valuesAjax',
          'wrapper' => $wrapper_id,
        ],
      ];
      $form['values']['_add_new']['entity']['reset_alphabetical'] = [
        '#type' => 'submit',
        '#value' => $this->t('Reset to alphabetical'),
        '#submit' => ['::resetAlphabeticalSubmit'],
        '#limit_validation_errors' => [],
        '#ajax' => [
          'callback' => '::valuesAjax',
          'wrapper' => $wrapper_id,
        ],
      ];
      $form['values']['_add_new']['operations'] = [
        'data' => [],
      ];
    }

    return $form;
  }

  /**
   * Ajax callback for value operations.
   */
  public function valuesAjax(array $form, FormStateInterface $form_state) {
    return $form['values'];
  }

  /**
   * Submit callback for adding a new value.
   */
  public function addValueSubmit(array $form, FormStateInterface $form_state) {
    $value_map = (array) $form_state->get('value_map');
    $value_map[] = '_new';
    $form_state->set('value_map', $value_map);
    $form_state->setRebuild();
  }

  /**
   * Submit callback for resetting attribute value ordering to alphabetical.
   */
  public function resetAlphabeticalSubmit(array $form, FormStateInterface $form_state) {
    $form_state->set('reset_alphabetical', TRUE);
    $form_state->setRebuild();
  }

  /**
   * Submit callback for removing a value.
   */
  public function removeValueSubmit(array $form, FormStateInterface $form_state) {
    $value_index = $form_state->getTriggeringElement()['#value_index'];
    $value_map = (array) $form_state->get('value_map');
    $value_id = $value_map[$value_index];
    unset($value_map[$value_index]);
    $form_state->set('value_map', $value_map);
    // Non-new values also need to be deleted from storage.
    if ($value_id != '_new') {
      $delete_queue = (array) $form_state->get('delete_queue');
      $delete_queue[] = $value_id;
      $form_state->set('delete_queue', $delete_queue);
    }
    $form_state->setRebuild();
  }

  /**
   * Saves the attribute values.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  protected function saveValues(array $form, FormStateInterface $form_state) {
    $delete_queue = $form_state->get('delete_queue');
    if (!empty($delete_queue)) {
      $value_storage = $this->entityTypeManager->getStorage('commerce_product_attribute_value');
      $values = $value_storage->loadMultiple($delete_queue);
      $value_storage->delete($values);
    }

    foreach ($form_state->getValue(['values']) as $index => $value_data) {
      /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $inline_form */
      $inline_form = $form['values'][$index]['entity']['#inline_form'];
      /** @var \Drupal\commerce_product\Entity\ProductAttributeValueInterface $value */
      $value = $inline_form->getEntity();
      $value->setWeight($value_data['weight']);
      $value->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    $status = $this->entity->save();

    $original_variation_types = $form_state->getValue('original_variation_types', []);
    $variation_types = array_filter($form_state->getValue('variation_types', []));
    $disabled_variation_types = $form_state->getValue('disabled_variation_types', []);
    $variation_types = array_unique(array_merge($disabled_variation_types, $variation_types));
    $selected_variation_types = array_diff($variation_types, $original_variation_types);
    $unselected_variation_types = array_diff($original_variation_types, $variation_types);
    if ($selected_variation_types) {
      foreach ($selected_variation_types as $selected_variation_type) {
        $this->attributeFieldManager->createField($this->entity, $selected_variation_type);
      }
    }
    if ($unselected_variation_types) {
      foreach ($unselected_variation_types as $unselected_variation_type) {
        $this->attributeFieldManager->deleteField($this->entity, $unselected_variation_type);
      }
    }

    if ($this->moduleHandler->moduleExists('content_translation')) {
      $translation_manager = \Drupal::service('content_translation.manager');
      // Logic from content_translation_language_configuration_element_submit().
      $enabled = $form_state->getValue('enable_value_translation');
      if ($translation_manager->isEnabled('commerce_product_attribute_value', $this->entity->id()) != $enabled) {
        $translation_manager->setEnabled('commerce_product_attribute_value', $this->entity->id(), $enabled);
        $this->entityTypeManager->clearCachedDefinitions();
        \Drupal::service('router.builder')->setRebuildNeeded();
      }
    }

    if ($status == SAVED_NEW) {
      $this->messenger()->addMessage($this->t('Created the %label product attribute.', ['%label' => $this->entity->label()]));
      // Send the user to the edit form to create the attribute values.
      $form_state->setRedirectUrl($this->entity->toUrl('edit-form'));
    }
    else {
      $this->saveValues($form, $form_state);
      $this->messenger()->addMessage($this->t('Updated the %label product attribute.', ['%label' => $this->entity->label()]));
      $form_state->setRedirectUrl($this->entity->toUrl('collection'));
    }

    return $status;
  }

}
