<?php

/**
 * @file
 * Allows administrators to attach custom fields to fieldable types.
 */

use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\ConfirmFormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;

require_once __DIR__ . '/includes/helpers.inc';

/**
 * Implements hook_theme_registry_alter().
 */
function field_group_theme_registry_alter(&$theme_registry) {

  // Inject field_group_build_entity_groups in all entity theming functions.
  $entity_info = Drupal::entityTypeManager()->getDefinitions();
  $entity_types = array();
  foreach ($entity_info as $entity_type_id => $entity_type) {
    if ($route_name = $entity_type->get('field_ui_base_route')) {
      $entity_types[] = $entity_type_id;
    }
  }

  foreach ($theme_registry as $theme_hook => $info) {
    if (in_array($theme_hook, $entity_types) || (!empty($info['base hook']) && in_array($info['base hook'], $entity_types))) {
      $theme_registry[$theme_hook]['preprocess functions'][] = 'field_group_build_entity_groups';
    }
  }

  // ECK does not use the eck as theme function.
  if (isset($theme_registry['eck_entity'])) {
    $theme_registry['eck_entity']['preprocess functions'][] = 'field_group_build_entity_groups';
  }

}

/**
 * Implements hook_theme().
 */
function field_group_theme() {
  return array(
    'horizontal_tabs' => array(
      'render element' => 'element',
      'template' => 'horizontal-tabs',
      'file' => 'templates/theme.inc',
    ),
    'field_group_accordion_item' => array(
      'render element' => 'element',
      'template' => 'field-group-accordion-item',
      'file' => 'templates/theme.inc',
    ),
    'field_group_accordion' => array(
      'render element' => 'element',
      'template' => 'field-group-accordion',
      'file' => 'templates/theme.inc',
    ),
    'field_group_html_element' => array(
      'render element' => 'element',
      'template' => 'field-group-html-element',
      'file' => 'templates/theme.inc',
    ),
  );
}

/**
 * Implements hook_theme_suggestions_alter().
 *
 * @param array $suggestions
 * @param array $variables
 * @param $hook
 */
function field_group_theme_suggestions_alter(array &$suggestions, array $variables, $hook) {
  switch ($hook) {
    case 'horizontal_tabs':
    case 'field_group_accordion_item':
    case 'field_group_accordion':
    case 'field_group_html_element':
      $element = $variables['element'];

      $name = $element['#group_name'];
      $entity_type = $element['#entity_type'];
      $bundle = $element['#bundle'];

      $wrapper = '';
      if (isset($element['#wrapper_element'])) {
        $wrapper = $element['#wrapper_element'];
        $suggestions[] = $hook . '__' . $wrapper;
      }

      $suggestions[] = $hook . '__' . $entity_type;
      $suggestions[] = $hook . '__' . $bundle;
      $suggestions[] = $hook . '__' . $name;

      if ($wrapper) {
        $suggestions[] = $hook . '__' . $entity_type . '__' . $wrapper;
      }

      $suggestions[] = $hook . '__' . $entity_type . '__' . $bundle;
      $suggestions[] = $hook . '__' . $entity_type . '__' . $name;

      if ($wrapper) {
        $suggestions[] = $hook . '__' . $entity_type . '__' . $bundle . '__' . $wrapper;
      }
      $suggestions[] = $hook . '__' . $entity_type . '__' . $bundle . '__' . $name;
      break;
  }

}

/**
 * Implements hook_form_FORM_ID_alter().
 * Using hook_form_field_ui_form_display_overview_form_alter.
 */
function field_group_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
  $form_state->loadInclude('field_group', 'inc', 'includes/field_ui');
  field_group_field_ui_display_form_alter($form, $form_state);
}

/**
 * Implements hook_form_FORM_ID_alter().
 * Using hook_form_field_ui_display_overview_form_alter.
 */
function field_group_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
  $form_state->loadInclude('field_group', 'inc', 'includes/field_ui');
  field_group_field_ui_display_form_alter($form, $form_state);
}

/**
 * Implements hook_field_info_max_weight().
 */
function field_group_field_info_max_weight($entity_type, $bundle, $context, $context_mode) {

  $groups = field_group_info_groups($entity_type, $bundle, $context, $context_mode);

  $weights = array();
  foreach ($groups as $group) {
    $weights[] = $group->weight;
  }
  return $weights ? max($weights) : NULL;
}

/**
 * Implements hook_form_alter().
 */
function field_group_form_alter(array &$form, FormStateInterface $form_state) {

  if ($form_state->getFormObject() instanceof ContentEntityFormInterface && !$form_state->getFormObject() instanceof ConfirmFormInterface) {

    /**
     * @var EntityFormDisplayInterface $form_display
     */
    if ($form_display = $form_state->getStorage()['form_display']) {
      $entity = $form_state->getFormObject()->getEntity();

      $context = array(
        'entity_type' => $entity->getEntityTypeId(),
        'bundle' => $entity->bundle(),
        'entity' => $entity,
        'context' => 'form',
        'display_context' => 'form',
        'mode' => $form_display->getMode(),
      );

      field_group_attach_groups($form, $context);
      $form['#pre_render'][] = 'field_group_form_pre_render';
    }
  }

}

/**
 * Implements hook_inline_entity_form_entity_form_alter().
 */
function field_group_inline_entity_form_entity_form_alter(&$entity_form, FormStateInterface $form_state) {

  // Attach the fieldgroups to current entity form.
  $context = [
    'entity_type' => $entity_form['#entity']->getEntityTypeId(),
    'bundle' => $entity_form['#entity']->bundle(),
    'entity' => $entity_form['#entity'],
    'display_context' => 'form',
    'mode' => 'default',
  ];

  field_group_attach_groups($entity_form, $context);
  $entity_form['#pre_render'][] = 'field_group_form_pre_render';
}

/**
 * Implements hook_entity_view_alter().
 */
function field_group_entity_view_alter(&$build, EntityInterface $entity, EntityDisplayInterface $display) {

  $context = array(
    'entity_type' => $display->getTargetEntityTypeId(),
    'bundle' => $entity->bundle(),
    'entity' => $entity,
    'display_context' => 'view',
    'mode' => $display->getMode(),
  );

  field_group_attach_groups($build, $context);

  // If no theme hook, we have no theme hook to preprocess.
  // Add a prerender.
  if (empty($build['#theme'])) {

    $ds_enabled = false;
    if (Drupal::moduleHandler()->moduleExists('ds')) {
      // Check if DS is enabled for this display.
      if ($display->getThirdPartySetting('ds', 'layout') && !Drupal\ds\Ds::isDisabled()) {
        $ds_enabled = true;
      }
    }

    // If DS is enabled, no pre render is needed (DS adds fieldgroup preprocessing).
    if (!$ds_enabled) {
      $build['#pre_render'][] = 'field_group_entity_view_pre_render';
    }
  }
}

/**
 * Pre render callback for rendering groups.
 * @see field_group_field_attach_form
 * @param $element Form that is being rendered.
 */
function field_group_form_pre_render($element) {
  if (empty($element['#field_group_form_pre_render'])) {
    $element['#field_group_form_pre_render'] = TRUE;
    field_group_build_entity_groups($element, 'form');
  }
  return $element;
}

/**
 * Pre render callback for rendering groups on entities without theme hook.
 * @param $element
 *   Entity being rendered.
 */
function field_group_entity_view_pre_render($element) {
  field_group_build_entity_groups($element, 'view');
  return $element;
}

/**
 * Implements hook_field_group_pre_render().
 *
 * @param Array $element
 *   Group beïng rendered.
 * @param Object $group
 *   The Field group info.
 * @param $rendering_object
 *   The entity / form beïng rendered
 */
function field_group_field_group_pre_render(&$element, &$group, &$rendering_object) {

  // Add all field_group format types to the js settings.
  $element['#attached']['drupalSettings']['field_group'] = array(
    $group->format_type => [
      'mode' => $group->mode,
      'context' => $group->context,
      'settings' => $group->format_settings,
    ],
  );

  $element['#weight'] = $group->weight;

  // Call the pre render function for the format type.
  $manager = Drupal::service('plugin.manager.field_group.formatters');
  $plugin = $manager->getInstance(array(
    'format_type' => $group->format_type,
    'configuration' => array('label' => $group->label, 'settings' => $group->format_settings),
    'group' => $group,
  ));
  $plugin->preRender($element, $rendering_object);

}

/**
 * Implements hook_field_group_build_pre_render_alter().
 * @param Array $elements by address.
 */
function field_group_field_group_build_pre_render_alter(& $element) {

  // Someone is doing a node view, in a node view. Reset content.
  if (isset($element['#node']->content) && count($element['#node']->content) > 0) {
    $element['#node']->content = array();
  }

  $display = isset($element['#view_mode']);
  $groups = array_keys($element['#fieldgroups']);

  // Dish the fieldgroups with no fields for non-forms.
  if ($display) {
    field_group_remove_empty_display_groups($element, $groups);
  }
  else {
    // Fix the problem on forms with additional settings.
    field_group_remove_empty_form_groups('form', $element, $groups, $element['#fieldgroups'], $element['#entity_type']);
  }

}

/**
 * Attach groups to the (form) build.
 *
 * @param Array $element
 *   The part of the form.
 *  @param Array $context
 *   The contextual information.
 */
function field_group_attach_groups(&$element, $context) {

  $entity_type = $context['entity_type'];
  $bundle = $context['bundle'];
  $mode = $context['mode'];
  $display_context = $context['display_context'];

  $element['#fieldgroups'] = field_group_info_groups($entity_type, $bundle, $display_context, $mode);

  // Create a lookup array.
  $group_children = array();
  foreach ($element['#fieldgroups'] as $group_name => $group) {
    foreach ($group->children as $child) {
      $group_children[$child] = $group_name;
    }
  }
  $element['#group_children'] = $group_children;
  $element['#entity_type'] = $entity_type;

}

/**
 * Preprocess/ Pre-render callback.
 *
 * @see field_group_form_pre_render()
 * @see field_group_theme_registry_alter
 * @see field_group_fields_nest()
 * @param $vars preprocess vars or form element
 * @param $context The display context (form or view)
 * @return $element Array with re-arranged fields in groups.
 */
function field_group_build_entity_groups(&$vars, $context = 'view') {

  if ($context == 'form') {
    $element = &$vars;
    $nest_vars = NULL;
  }
  else {
    if (isset($vars['elements'])) {
      $element = &$vars['elements'];
    }
    elseif (isset($vars['content'])) {
      $element = &$vars['content'];
    }
    else {

      if ($context === 'eck_entity') {
        $element = &$vars['entity'];
      }
      else {
        $element = &$vars;
      }
    }

    $nest_vars = &$vars;
  }

  // No groups on the entity.
  if (empty($element['#fieldgroups'])) {
    return $element;
  }

  // Nest the fields in the corresponding field groups.
  field_group_fields_nest($element, $nest_vars);

  // Allow others to alter the pre_rendered build.
  Drupal::moduleHandler()->alter('field_group_build_pre_render', $element);

  // Return the element on forms.
  if ($context == 'form') {
    return $element;
  }

  // No groups on the entity. Prerender removed empty field groups.
  if (empty($element['#fieldgroups'])) {
    return $element;
  }

  // Put groups inside content if we are rendering an entity_view.
  foreach ($element['#fieldgroups'] as $group) {
    if (!empty($element[$group->group_name])) {
      if (isset($vars['content'])) {
        $vars['content'][$group->group_name] = $element[$group->group_name];
      }
      elseif (isset($vars['user_profile'])) {
        $vars['user_profile'][$group->group_name] = $element[$group->group_name];
      }
    }
  }

}

/**
 * Recursive function to nest fields in the field groups.
 *
 * This function will take out all the elements in the form and
 * place them in the correct container element, a fieldgroup.
 * The current group element in the loop is passed recursively so we can
 * stash fields and groups in it while we go deeper in the array.
 * @param Array $element
 *   The current element to analyse for grouping.
 * @param Array $vars
 *   Rendering vars from the entity being viewed.
 */
function field_group_fields_nest(&$element, &$vars = NULL) {

  // Create all groups and keep a flat list of references to these groups.
  $group_references = array();
  foreach ($element['#fieldgroups'] as $group_name => $group) {
    // Construct own weight, as some fields (for example preprocess fields) don't have weight set.
    $element[$group_name] = array();
    $group_references[$group_name] = &$element[$group_name];
  }

  // Loop through all form children looking for those that are supposed to be
  // in groups, and insert placeholder element for the new group field in the
  // correct location within the form structure.
  $element_clone = array();
  foreach (Element::children($element) as $child_name) {
    $element_clone[$child_name] = $element[$child_name];
    // If this element is in a group, create the placeholder element.
    if (isset($element['#group_children'][$child_name])) {
      $element_clone[$element['#group_children'][$child_name]] = array();
    }
  }
  $element = array_merge($element_clone, $element);

  // Move all children to their parents. Use the flat list of references for
  // direct access as we don't know where in the root_element hierarchy the
  // parent currently is situated.
  foreach ($element['#group_children'] as $child_name => $parent_name) {

    // Entity being viewed
    if ($vars) {
      // If not a group, check vars['content'] for empty field.
      if (!isset($element['#fieldgroups'][$child_name]) && isset($vars['content'][$child_name])) {
        $group_references[$parent_name][$child_name] = $vars['content'][$child_name];
        unset($vars['content'][$child_name]);
      }
      elseif (!isset($element['#fieldgroups'][$child_name]) && isset($vars['user_profile'][$child_name])) {
        $group_references[$parent_name][$child_name] = $vars['user_profile'][$child_name];
        unset($vars['user_profile'][$child_name]);
      }
      // If this is a group, we have to use a reference to keep the reference
      // list intact (but if it is a field we don't mind).
      else {
        $group_references[$parent_name][$child_name] = &$element[$child_name];
        unset($element[$child_name]);
      }
    }
    // Form being viewed
    else {
      // Block denied fields (#access) before they are put in groups.
      // Fields (not groups) that don't have children (like field_permissions) are removed
      // in field_group_field_group_build_pre_render_alter.
      if (isset($element[$child_name]) && (!isset($element[$child_name]['#access']) || $element[$child_name]['#access'])) {
        // If this is a group, we have to use a reference to keep the reference
        // list intact (but if it is a field we don't mind).
        $group_references[$parent_name][$child_name] = &$element[$child_name];
        $group_references[$parent_name]['#weight'] = $element['#fieldgroups'][$parent_name]->weight;
      }

      // The child has been copied to its parent: remove it from the root element.
      unset($element[$child_name]);
    }

  }

  // Bring extra element wrappers to achieve a grouping of fields.
  // This will mainly be prefix and suffix altering.
  foreach ($element['#fieldgroups'] as $group_name => $group) {
    field_group_pre_render($group_references[$group_name], $group, $element);
  }

}

/**
 * Function to pre render the field group element.
 *
 * @see field_group_fields_nest()
 *
 * @param $element
 *   Render array of group element that needs to be created.
 * @param $group
 *   Object with the group information.
 * @param $rendering_object
 *   The entity / form beïng rendered.
 */
function field_group_pre_render(& $element, $group, & $rendering_object) {

  // Only run the pre_render function if the group has elements.
  // $group->group_name
  if ($element == array()) {
    return;
  }

  // Let modules define their wrapping element.
  // Note that the group element has no properties, only elements.
  foreach (Drupal::moduleHandler()->getImplementations('field_group_pre_render') as $module) {
    // The intention here is to have the opportunity to alter the
    // elements, as defined in hook_field_group_formatter_info.
    // Note, implement $element by reference!
    $function = $module . '_field_group_pre_render';
    $function($element, $group, $rendering_object);
  }

  // Allow others to alter the pre_render.
  Drupal::moduleHandler()->alter('field_group_pre_render', $element, $group, $rendering_object);

}

/**
 * Saves a group definition.
 * This function is called by ctools export when calls are made
 * through ctools_export_crud_save().
 *
 * @param $group
 *   A group definition.
 * @param $display
 *   The display to update if known.
 * @return EntityDisplayInterface || NULL
 */
function field_group_group_save($group, $display = NULL) {
  if ($display === NULL) {
    if ($group->context == 'form') {
      $display = EntityFormDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
    }
    elseif ($group->context == 'view') {
      $display = EntityViewDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
    }
  }

  // If no display was found. It doesn't exist yet, create it.
  if (!isset($display)) {

    if ($group->context == 'form') {
      $display = EntityFormDisplay::create(array(
        'targetEntityType' => $group->entity_type,
        'bundle' => $group->bundle,
        'mode' => $group->mode,
      ))->setStatus(TRUE);
    }
    elseif ($group->context == 'view') {
      $display = EntityViewDisplay::create(array(
        'targetEntityType' => $group->entity_type,
        'bundle' => $group->bundle,
        'mode' => $group->mode,
      ))->setStatus(TRUE);
    }

  }

  /**
   * @var $display \Drupal\Core\Entity\Display\EntityDisplayInterface
   */
  if (isset($display)) {
    $data = (array) $group;
    unset($data['group_name'], $data['entity_type'], $data['bundle'], $data['mode'], $data['form'], $data['context']);
    $display->setThirdPartySetting('field_group', $group->group_name, $data);
    $display->save();
  }

  return $display;
}

/**
 * Delete a field group.
 *
 * @param $group
 *   A group definition.
 */
function field_group_group_delete($group) {
  if ($group->context == 'form') {
    $display = EntityFormDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
  }
  elseif ($group->context == 'view') {
    $display = EntityViewDisplay::load($group->entity_type . '.' . $group->bundle . '.' . $group->mode);
  }

  /**
   * @var $display \Drupal\Core\Entity\Display\EntityDisplayInterface
   */
  if (isset($display)) {
    $display->unsetThirdPartySetting('field_group', $group->group_name);
    $display->save();
  }

  Drupal::moduleHandler()->invokeAll('field_group_delete_field_group', array($group));
}

/**
 * Get all groups.
 *
 * @param $entity_type
 *   The name of the entity.
 * @param $bundle
 *   The name of the bundle.
 * @param $context
 *   The context of the view mode (form or view)
 * @param $mode
 *   The view mode.
 */
function field_group_info_groups($entity_type, $bundle, $context, $mode) {
  if ($context == 'form') {
    $display = EntityFormDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
    if (!$display) {
      return array();
    }
    $data = $display->getThirdPartySettings('field_group');
  }
  if ($context == 'view') {
    $display = EntityViewDisplay::load($entity_type . '.' . $bundle . '.' . $mode);
    if (!$display) {
      return array();
    }
    $data = $display->getThirdPartySettings('field_group');
  }
  $groups = array();
  if (isset($data)) {
    foreach ($data as $group_name => $definition) {
      $definition += array(
        'group_name' => $group_name,
        'entity_type' => $entity_type,
        'bundle' => $bundle,
        'context' => $context,
        'mode' => $mode,
      );
      $groups[$group_name] = (object) $definition;
    }
  }
  return $groups;
}

/**
 * Loads a group definition.
 *
 * @param $group_name
 *   The name of the group.
 * @param $entity_type
 *   The name of the entity.
 * @param $bundle
 *   The name of the bundle.
 * @param $context
 *   The context of the view mode (form or view)
 * @param $mode
 *   The view mode to load.
 */
function field_group_load_field_group($group_name, $entity_type, $bundle, $context, $mode) {
  $groups = field_group_info_groups($entity_type, $bundle, $context, $mode);
  if (isset($groups[$group_name])) {
    return $groups[$group_name];
  }
}

/**
 * Checks if a field_group exists in required context.
 *
 * @param String $group_name
 *   The name of the group.
 * @param String $entity_type
 *   The name of the entity.
 * @param String $bundle
 *   The bundle for the entity.
 * @param $context
 *   The context of the view mode (form or view)
 * @param String $mode
 *   The view mode context the group will be rendered.
 */
function field_group_exists($group_name, $entity_type, $bundle, $context, $mode) {
  return (bool) field_group_load_field_group($group_name, $entity_type, $bundle, $context, $mode);
}

/**
 * Remove empty groups on forms.
 *
 * @param String $parent_name
 *   The name of the element.
 * @param array $element
 *   The element to check the empty state.
 * @param array $groups
 *   Array of group objects.
 */
function field_group_remove_empty_form_groups($name, & $element, $groups, &$form_groups, $entity) {

  $exceptions = array('user__account', 'comment__author');

  $children = Element::children($element);

  $hasChildren = FALSE;
  if (count($children)) {
    foreach ($children as $childname) {

      if (in_array($childname, $groups)) {
        field_group_remove_empty_form_groups($childname, $element[$childname], $groups, $form_groups, $entity);
      }
      $exception = $entity . '__' . $childname;
      $hasChildren = $hasChildren ? TRUE : (isset($element[$childname]['#type']) || isset($element[$childname]['#markup']) || in_array($exception, $exceptions));

    }
  }

  if (!$hasChildren) {

    // Remove empty elements from the #fieldgroups.
    if (empty($element) && isset($form_groups[$name]) && !is_array($form_groups[$name])) {
      foreach ($form_groups as $group_name => $group) {
        if (isset($group->children)) {
          $group_children = array_flip($group->children);
          if (isset($group_children[$name])) {
            unset($form_groups[$group_name]->children[$group_children[$name]]);
          }
        }
      }
    }

    $element['#access'] = FALSE;

  }

}

/**
 * Remove empty groups on entity display.
 *
 * @param array $element
 *   The element to check the empty state.
 * @param array $groups
 *   Array of group objects.
 */
function field_group_remove_empty_display_groups(& $element, $groups) {

  $empty_child = TRUE;
  $empty_group = TRUE;

  // Loop through the visible children for current element.
  foreach (Element::getVisibleChildren($element) as $name) {

    // Descend if the child is a group.
    if (in_array($name, $groups)) {
      $empty_child = field_group_remove_empty_display_groups($element[$name], $groups);
      if (!$empty_child) {
        $empty_group = FALSE;
      }
    }
    // Child is a field or a renderable array and the element is not empty.
    elseif (!empty($element[$name])) {
      $clone_element = $element[$name];
      // Weight parameter can make empty element seen as not empty.
      unset($clone_element['#weight']);
      if (!Element::isEmpty($clone_element)) {
        $empty_group = FALSE;
      }
    }

  }

  // Reset an empty group.
  if ($empty_group) {
    $element = [];
  }

  return $empty_group;

}
