<?php

/**
 * @file
 * The inline entity form module.
 *
 * Provides a widget for inline management (creation, modification, removal) of
 * referenced entities. The primary use case is the parent -> children one
 * (for example, order -> line items), where the child entities are never
 * managed outside the parent form.
 */

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\inline_entity_form\ElementSubmit;
use Drupal\inline_entity_form\WidgetSubmit;
use Drupal\inline_entity_form\Form\EntityInlineForm;
use Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormComplex;
use Drupal\inline_entity_form\MigrationHelper;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;

/**
 * Implements hook_entity_type_build().
 */
function inline_entity_form_entity_type_build(array &$entity_types) {
  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
  if (isset($entity_types['node']) && !$entity_types['node']->getHandlerClass('inline_form')) {
    $entity_types['node']->setHandlerClass('inline_form', '\Drupal\inline_entity_form\Form\NodeInlineForm');
  }

  foreach ($entity_types as &$entity_type) {
    if (!$entity_type->hasHandlerClass('inline_form')) {
      $entity_type->setHandlerClass('inline_form', '\Drupal\inline_entity_form\Form\EntityInlineForm');
    }
  }
}

/**
 * Implements hook_form_alter().
 */
function inline_entity_form_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Attach the IEF handlers only if the current form has an IEF widget.
  $widget_state = $form_state->get('inline_entity_form');
  if (!is_null($widget_state)) {
    ElementSubmit::attach($form, $form_state);
    WidgetSubmit::attach($form, $form_state);
  }
}

/**
 * Implements hook_theme().
 */
function inline_entity_form_theme() {
  return [
    'inline_entity_form_entity_table' => [
      'render element' => 'form',
    ],
  ];
}

/**
 * Provides the form for adding existing entities through an autocomplete field.
 *
 * @param array $reference_form
 *   The form array that will receive the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 *
 * @return array
 *   The form array containing the embedded form.
 */
function inline_entity_form_reference_form(array $reference_form, FormStateInterface &$form_state) {
  $labels = $reference_form['#ief_labels'];
  $ief_id = $reference_form['#ief_id'];
  /** @var \Drupal\field\Entity\FieldConfig $instance */
  $instance = $form_state->get(['inline_entity_form', $ief_id, 'instance']);
  $selection_settings = [
    'match_operator' => $reference_form['#match_operator'],
  ] + $instance->getSetting('handler_settings');

  $reference_form['#title'] = t('Add existing @type_singular', ['@type_singular' => $labels['singular']]);

  $reference_form['entity_id'] = [
    '#type' => 'entity_autocomplete',
    // @todo Use bundle defined singular/plural labels as soon as
    //   https://www.drupal.org/node/2765065 is committed.
    // @see https://www.drupal.org/node/2765065
    '#title' => t('@label', ['@label' => ucfirst($labels['singular'])]),
    '#target_type' => $instance->getSetting('target_type'),
    '#selection_handler' => $instance->getSetting('handler'),
    '#selection_settings' => $selection_settings,
    '#required' => TRUE,
    '#maxlength' => 255,
  ];
  // Add the actions.
  $reference_form['actions'] = [
    '#type' => 'container',
    '#weight' => 100,
  ];
  $reference_form['actions']['ief_reference_save'] = [
    '#type' => 'submit',
    '#value' => t('Add @type_singular', ['@type_singular' => $labels['singular']]),
    '#name' => 'ief-reference-submit-' . $reference_form['#ief_id'],
    '#limit_validation_errors' => [$reference_form['#parents']],
    '#attributes' => ['class' => ['ief-entity-submit']],
    '#ajax' => [
      'callback' => 'inline_entity_form_get_element',
      'wrapper' => 'inline-entity-form-' . $reference_form['#ief_id'],
    ],
  ];
  InlineEntityFormComplex::addSubmitCallbacks($reference_form['actions']['ief_reference_save']);
  $reference_form['actions']['ief_reference_cancel'] = [
    '#type' => 'submit',
    '#value' => t('Cancel'),
    '#name' => 'ief-reference-cancel-' . $reference_form['#ief_id'],
    '#limit_validation_errors' => [],
    '#ajax' => [
      'callback' => 'inline_entity_form_get_element',
      'wrapper' => 'inline-entity-form-' . $reference_form['#ief_id'],
    ],
    '#submit' => [
      [
        '\Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormComplex',
        'closeForm',
      ],
    ],
  ];

  $reference_form['#element_validate'][] = 'inline_entity_form_reference_form_validate';
  $reference_form['#ief_element_submit'][] = 'inline_entity_form_reference_form_submit';

  // Allow other modules and themes to alter the form.
  \Drupal::moduleHandler()->alter('inline_entity_form_reference_form', $reference_form, $form_state);
  \Drupal::theme()->alter('inline_entity_form_reference_form', $reference_form, $form_state);

  return $reference_form;
}

/**
 * Validates the form for adding existing entities.
 *
 * @param array $reference_form
 *   The reference entity form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_reference_form_validate(array &$reference_form, FormStateInterface $form_state) {
  $form_values = NestedArray::getValue($form_state->getValues(), $reference_form['#parents']);
  if (empty($form_values['entity_id'])) {
    // The entity_id element is required, the value is empty only if
    // the form was cancelled.
    return;
  }
  $ief_id = $reference_form['#ief_id'];
  $labels = $reference_form['#ief_labels'];
  $storage = \Drupal::entityTypeManager()->getStorage($reference_form['#entity_type']);
  $entity = $storage->load($form_values['entity_id']);

  // Check if the entity is already referenced by the field.
  if (!empty($entity)) {
    foreach ($form_state->get(['inline_entity_form', $ief_id, 'entities']) as $value) {
      if ($value['entity'] && $value['entity']->id() == $entity->id()) {
        $form_state->setError($reference_form['entity_id'], t('The selected @label has already been added.', ['@label' => $labels['singular']]));
        break;
      }
    }
  }
}

/**
 * Submits the form for adding existing entities.
 *
 * Adds the specified entity to the IEF form state.
 *
 * @param array $reference_form
 *   The reference entity form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_reference_form_submit(array $reference_form, FormStateInterface $form_state) {
  $ief_id = $reference_form['#ief_id'];
  $form_values = NestedArray::getValue($form_state->getValues(), $reference_form['#parents']);
  $storage = \Drupal::entityTypeManager()->getStorage($reference_form['#entity_type']);
  $entity = $storage->load($form_values['entity_id']);
  $entities = &$form_state->get(['inline_entity_form', $ief_id, 'entities']);
  // Determine the correct weight of the new element.
  $weight = 0;
  if ($entities) {
    $weight = max(array_keys($entities)) + 1;
  }

  $entities[] = [
    'entity' => $entity,
    'weight' => $weight,
    'form' => NULL,
    'needs_save' => FALSE,
  ];
  $form_state->set(['inline_entity_form', $ief_id, 'entities'], $entities);
}

/**
 * Button #submit callback: Opens a form in the IEF widget.
 *
 * The form is shown below the entity table, at the bottom of the widget.
 *
 * @param array $form
 *   The complete parent form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_open_form(array $form, FormStateInterface $form_state) {
  $element = inline_entity_form_get_element($form, $form_state);
  $ief_id = $element['#ief_id'];
  $form_state->setRebuild();

  // Get the current form values.
  $parents = array_merge($element['#field_parents'], [$element['#field_name']]);
  $form_values = NestedArray::getValue($form_state->getUserInput(), $parents);

  $triggering_element = $form_state->getTriggeringElement();
  $form_state->set(['inline_entity_form', $ief_id, 'form'], $triggering_element['#ief_form']);
  if (!empty($form_values['actions']['bundle'])) {
    $form_state->set(['inline_entity_form', $ief_id, 'form settings'], [
      'bundle' => $form_values['actions']['bundle'],
    ]);
  }
}

/**
 * Button #submit callback: Cleans up form state for a closed entity form.
 *
 * @param array $form
 *   The complete parent form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_cleanup_form_state(array $form, FormStateInterface $form_state) {
  $element = inline_entity_form_get_element($form, $form_state);
  EntityInlineForm::submitCleanFormState($element['form']['inline_entity_form'], $form_state);
}

/**
 * Button #submit callback: Opens a row form in the IEF widget.
 *
 * The row is identified by #ief_row_delta stored on the triggering
 * element.
 *
 * @param array $form
 *   The complete parent form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_open_row_form(array $form, FormStateInterface $form_state) {
  $element = inline_entity_form_get_element($form, $form_state);
  $ief_id = $element['#ief_id'];
  $delta = $form_state->getTriggeringElement()['#ief_row_delta'];

  $form_state->setRebuild();
  $form_state->set(['inline_entity_form', $ief_id, 'entities', $delta, 'form'], $form_state->getTriggeringElement()['#ief_row_form']);
}

/**
 * Closes all open IEF forms.
 *
 * Recurses and closes open forms in nested IEF widgets as well.
 *
 * @param array $elements
 *   An array of form elements containing entity forms.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_close_all_forms(array $elements, FormStateInterface $form_state) {
  // Recurse through all children.
  foreach (Element::children($elements) as $key) {
    if (!empty($elements[$key])) {
      inline_entity_form_close_all_forms($elements[$key], $form_state);
    }
  }

  if (!empty($elements['#ief_id'])) {
    $ief_id = $elements['#ief_id'];
    // Close the main form.
    $form_state->set(['inline_entity_form', $ief_id, 'form'], NULL);
    // Close the row forms.
    $entities = $form_state->get(['inline_entity_form', $ief_id, 'entities']);
    foreach ($entities as $key => $value) {
      $entities[$key]['form'] = NULL;
    }
    $form_state->set(['inline_entity_form', $ief_id, 'entities'], $entities);
  }
}

/**
 * Button #submit callback: Cleans up form state for a closed entity row form.
 *
 * @param array $form
 *   The complete parent form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The form state of the parent form.
 */
function inline_entity_form_cleanup_row_form_state(array $form, FormStateInterface $form_state) {
  $element = inline_entity_form_get_element($form, $form_state);
  $delta = $form_state->getTriggeringElement()['#ief_row_delta'];
  $entity_form = $element['entities'][$delta]['form']['inline_entity_form'];
  EntityInlineForm::submitCleanFormState($entity_form, $form_state);
}

/**
 * Returns an IEF widget nearest to the triggering element.
 */
function inline_entity_form_get_element($form, FormStateInterface $form_state) {
  $element = [];
  $triggering_element = $form_state->getTriggeringElement();

  // Remove the action and the actions container.
  $array_parents = array_slice($triggering_element['#array_parents'], 0, -2);

  while (!isset($element['#ief_root'])) {
    $element = NestedArray::getValue($form, $array_parents);
    array_pop($array_parents);
  }

  return $element;
}

/**
 * Prepares variables for inline_entity_form_entity_table form templates.
 *
 * Default template: inline-entity-form-entity-table.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - form: A render element representing the form.
 */
function template_preprocess_inline_entity_form_entity_table(array &$variables) {
  $form = $variables['form'];
  $entity_type = $form['#entity_type'];

  $fields = $form['#table_fields'];
  $has_tabledrag = \Drupal::entityTypeManager()->getHandler($entity_type, 'inline_form')->isTableDragEnabled($form);

  // Sort the fields by weight.
  uasort($fields, '\Drupal\Component\Utility\SortArray::sortByWeightElement');

  $header = [];
  if ($has_tabledrag) {
    $header[] = ['data' => '', 'class' => ['ief-tabledrag-header']];
    $header[] = [
      'data' => t('Sort order'),
      'class' => ['ief-sort-order-header'],
    ];
  }
  // Add header columns for each field.
  $first = TRUE;
  foreach ($fields as $field_name => $field) {
    $column = [
      'data' => $field['label'],
      'class' => ['inline-entity-form-' . $entity_type . '-' . $field_name],
    ];
    // The first column gets a special class.
    if ($first) {
      $column['class'][] = 'ief-first-column-header';
      $first = FALSE;
    }
    $header[] = $column;
  }
  $header[] = t('Operations');

  // Build an array of entity rows for the table.
  $rows = [];
  foreach (Element::children($form) as $key) {
    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
    $entity = $form[$key]['#entity'];
    $row_classes = ['ief-row-entity'];
    $cells = [];
    if ($has_tabledrag) {
      $cells[] = [
        'data' => ['#plain_text' => ''],
        '#wrapper_attributes' => ['class' => ['ief-tabledrag-handle']],
      ];
      $cells[] = ['data' => $form[$key]['delta']];
      $row_classes[] = 'draggable';
    }
    // Add a special class to rows that have a form underneath, to allow
    // for additional styling.
    if (!empty($form[$key]['form'])) {
      $row_classes[] = 'ief-row-entity-form';
    }

    foreach ($fields as $field_name => $field) {
      if ($field['type'] == 'label') {
        $data = ['#markup' => $variables['form'][$key]['#label']];
      }
      elseif ($field['type'] == 'field' && $entity->hasField($field_name)) {
        $display_options = ['label' => 'hidden'];
        if (isset($field['display_options'])) {
          $display_options += $field['display_options'];
        }
        $data = $entity->get($field_name)->view($display_options);
      }
      elseif ($field['type'] == 'callback') {
        $arguments = [
          'entity' => $entity,
          'variables' => $variables,
        ];
        if (isset($field['callback_arguments'])) {
          $arguments = array_merge($arguments, $field['callback_arguments']);
        }

        $data = call_user_func_array($field['callback'], array_values($arguments));

        // Backward compatibility for callbacks that just provide a string not
        // an array.
        if (!is_array($data)) {
          $data = ['#markup' => $data];
        }
      }
      else {
        $data = ['#markup' => t('N/A')];
      }

      $cells[$field_name] = array_merge($data, ['#wrapper_attributes' => ['class' => ['inline-entity-form-' . $entity_type . '-' . $field_name]]]);
    }

    // Add the buttons belonging to the "Operations" column, when entity is not
    // being displayed as a form.
    if (empty($form[$key]['form'])) {
      $cells['actions'] = $form[$key]['actions'];
    }
    // Create the row.
    $rows[] = $cells + ['#attributes' => ['class' => $row_classes]];
    // If the current entity array specifies a form, output it in the next row.
    if (!empty($form[$key]['form'])) {
      $row = [];
      $row[] = $form[$key]['form'] + ['#wrapper_attributes' => ['colspan' => count($fields) + 1]];
      $rows[] = $row + [
        '#attributes' =>
        [
          'class' => ['ief-row-form'],
          'no_striping' => TRUE,
        ],
      ];
    }
  }

  if (!empty($rows)) {
    $tabledrag = [];
    if ($has_tabledrag) {
      $tabledrag = [
        [
          'action' => 'order',
          'relationship' => 'sibling',
          'group' => 'ief-entity-delta',
        ],
      ];
    }

    $variables['table'] = [
      '#type' => 'table',
      '#header' => $header,
      '#attributes' => [
        'id' => 'ief-entity-table-' . $form['#id'],
        'class' => ['ief-entity-table'],
      ],
      '#tabledrag' => $tabledrag,
    ] + $rows;
  }
}

/**
 * Implements hook_migrate_prepare_row().
 */
function inline_entity_form_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
  \Drupal::classResolver(MigrationHelper::class)
    ->alterRow($row, $source, $migration);
}

/**
 * Implements hook_migration_plugins_alter().
 */
function inline_entity_form_migration_plugins_alter(array &$migrations) {
  \Drupal::classResolver(MigrationHelper::class)
    ->alterPlugins($migrations);
}
