<?php

namespace Drupal\config_update_ui\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Diff\DiffFormatter;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\config_update\ConfigDiffInterface;
use Drupal\config_update\ConfigListByProviderInterface;
use Drupal\config_update\ConfigRevertInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Returns responses for Configuration Revert module operations.
 */
class ConfigUpdateController extends ControllerBase {

  /**
   * The config differ.
   *
   * @var \Drupal\config_update\ConfigDiffInterface
   */
  protected $configDiff;

  /**
   * The config lister.
   *
   * @var \Drupal\config_update\ConfigListByProviderInterface
   */
  protected $configList;

  /**
   * The config reverter.
   *
   * @var \Drupal\config_update\ConfigRevertInterface
   */
  protected $configRevert;

  /**
   * The diff formatter.
   *
   * @var \Drupal\Core\Diff\DiffFormatter
   */
  protected $diffFormatter;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The theme handler.
   *
   * @var \Drupal\Core\Extension\ThemeHandlerInterface
   */
  protected $themeHandler;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Constructs a ConfigUpdateController object.
   *
   * @param \Drupal\config_update\ConfigDiffInterface $config_diff
   *   The config differ.
   * @param \Drupal\config_update\ConfigListByProviderInterface $config_list
   *   The config lister.
   * @param \Drupal\config_update\ConfigRevertInterface $config_update
   *   The config reverter.
   * @param \Drupal\Core\Diff\DiffFormatter $diff_formatter
   *   The diff formatter to use.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
   *   The theme handler.
   * @param \drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(ConfigDiffInterface $config_diff, ConfigListByProviderInterface $config_list, ConfigRevertInterface $config_update, DiffFormatter $diff_formatter, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, ConfigFactoryInterface $config_factory) {
    $this->configDiff = $config_diff;
    $this->configList = $config_list;
    $this->configRevert = $config_update;
    $this->diffFormatter = $diff_formatter;
    $this->diffFormatter->show_header = FALSE;
    $this->moduleHandler = $module_handler;
    $this->themeHandler = $theme_handler;
    $this->configFactory = $config_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config_update.config_diff'),
      $container->get('config_update.config_list'),
      $container->get('config_update.config_update'),
      $container->get('diff.formatter'),
      $container->get('module_handler'),
      $container->get('theme_handler'),
      $container->get('config.factory')
    );
  }

  /**
   * Imports configuration from a module, theme, or profile.
   *
   * Configuration is assumed not to currently exist.
   *
   * @param string $config_type
   *   The type of configuration.
   * @param string $config_name
   *   The name of the config item, without the prefix.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   Redirects to the updates report.
   */
  public function import($config_type, $config_name) {
    $this->configRevert->import($config_type, $config_name);

    drupal_set_message($this->t('The configuration was imported.'));
    return $this->redirect('config_update_ui.report');
  }

  /**
   * Shows the diff between active and provided configuration.
   *
   * @param string $config_type
   *   The type of configuration.
   * @param string $config_name
   *   The name of the config item, without the prefix.
   *
   * @return array
   *   Render array for page showing differences between them.
   */
  public function diff($config_type, $config_name) {
    $diff = $this->configDiff->diff(
      $this->configRevert->getFromExtension($config_type, $config_name),
      $this->configRevert->getFromActive($config_type, $config_name)
    );

    $build = [];
    $definition = $this->configList->getType($config_type);
    $config_type_label = ($definition) ? $definition->getLabel() : $this->t('Simple configuration');
    $build['#title'] = $this->t('Config difference for @type @name', ['@type' => $config_type_label, '@name' => $config_name]);
    $build['#attached']['library'][] = 'system/diff';

    $build['diff'] = [
      '#type' => 'table',
      '#header' => [
        ['data' => $this->t('Source config'), 'colspan' => '2'],
        ['data' => $this->t('Site config'), 'colspan' => '2'],
      ],
      '#rows' => $this->diffFormatter->format($diff),
      '#attributes' => ['class' => ['diff']],
    ];

    $url = new Url('config_update_ui.report');

    $build['back'] = [
      '#type' => 'link',
      '#attributes' => [
        'class' => [
          'dialog-cancel',
        ],
      ],
      '#title' => $this->t("Back to 'Updates report' page."),
      '#url' => $url,
    ];

    return $build;
  }

  /**
   * Generates the config updates report.
   *
   * @param string $report_type
   *   (optional) Type of report to run:
   *   - type: Configuration entity type.
   *   - module: Module.
   *   - theme: Theme.
   *   - profile: Install profile.
   * @param string $name
   *   (optional) Name of specific item to run report for (config entity type
   *   ID, module machine name, etc.). Ignored for profile.
   *
   * @return array
   *   Render array for report, with section at the top for selecting another
   *   report to run. If either $report_type or $name is missing, the report
   *   itself is not generated.
   */
  public function report($report_type = NULL, $name = NULL) {
    $links = $this->generateReportLinks();

    $report = $this->generateReport($report_type, $name);
    if (!$report) {
      return $links;
    }

    // If there is a report, extract the title, put table of links in a
    // details element, and add report to build.
    $build = [];
    $build['#title'] = $report['#title'];
    unset($report['#title']);

    $build['links_wrapper'] = [
      '#type' => 'details',
      '#title' => $this->t('Generate new report'),
      '#children' => $links,
    ];

    $build['report'] = $report;

    $build['#attached']['library'][] = 'config_update/report_css';

    return $build;
  }

  /**
   * Generates the operations links for running individual reports.
   *
   * @return array
   *   Render array for the operations links for running reports.
   */
  protected function generateReportLinks() {

    // These links are put into an 'operations' render array element. They do
    // not look good outside of tables. Also note that the array index in
    // operations links is used as a class on the LI element. Some classes are
    // special in the Seven CSS, such as "contextual", so avoid hitting these
    // accidentally by prefixing.
    $build = [];

    $build['links'] = [
      '#type' => 'table',
      '#header' => [
        $this->t('Report type'),
        $this->t('Report on'),
      ],
      '#rows' => [],
    ];

    // Full report of all configuration.
    $links['report_full'] = [
      'title' => $this->t('Everything'),
      'url' => Url::fromRoute('config_update_ui.report', ['report_type' => 'type', 'name' => 'system.all']),
    ];
    $build['links']['#rows'][] = [
      $this->t('Full report'),
      [
        'data' => [
          '#type' => 'operations',
          '#links' => $links,
        ],
      ],
    ];

    // Reports by configuration type.
    $definitions = $this->configList->listTypes();
    $links = [];
    foreach ($definitions as $entity_type => $definition) {
      $links['report_type_' . $entity_type] = [
        'title' => $definition->getLabel(),
        'url' => Url::fromRoute('config_update_ui.report', ['report_type' => 'type', 'name' => $entity_type]),
      ];
    }

    uasort($links, [$this, 'sortLinks']);

    $links = [
      'report_type_system.simple' => [
        'title' => $this->t('Simple configuration'),
        'url' => Url::fromRoute('config_update_ui.report', ['report_type' => 'type', 'name' => 'system.simple']),
      ],
    ] + $links;

    $build['links']['#rows'][] = [
      $this->t('Single configuration type'),
      [
        'data' => [
          '#type' => 'operations',
          '#links' => $links,
        ],
      ],
    ];

    // Make a list of installed modules.
    $profile = $this->getProfileName();
    $modules = $this->moduleHandler->getModuleList();
    $links = [];
    foreach ($modules as $machine_name => $module) {
      if ($machine_name != $profile && $this->configList->providerHasConfig('module', $machine_name)) {
        $links['report_module_' . $machine_name] = [
          'title' => $this->moduleHandler->getName($machine_name),
          'url' => Url::fromRoute('config_update_ui.report', ['report_type' => 'module', 'name' => $machine_name]),
        ];
      }
    }
    uasort($links, [$this, 'sortLinks']);

    $build['links']['#rows'][] = [
      $this->t('Single module'),
      [
        'data' => [
          '#type' => 'operations',
          '#links' => $links,
        ],
      ],
    ];

    // Make a list of installed themes.
    $themes = $this->themeHandler->listInfo();
    $links = [];
    foreach ($themes as $machine_name => $theme) {
      if ($this->configList->providerHasConfig('theme', $machine_name)) {
        $links['report_theme_' . $machine_name] = [
          'title' => $this->themeHandler->getName($machine_name),
          'url' => Url::fromRoute('config_update_ui.report', ['report_type' => 'theme', 'name' => $machine_name]),
        ];
      }
    }
    uasort($links, [$this, 'sortLinks']);

    $build['links']['#rows'][] = [
      $this->t('Single theme'),
      [
        'data' => [
          '#type' => 'operations',
          '#links' => $links,
        ],
      ],
    ];

    $links = [];

    // Profile is just one option.
    if ($this->configList->providerHasConfig('profile', $profile)) {
      $links['report_profile_' . $profile] = [
        'title' => $this->moduleHandler->getName($profile),
        'url' => Url::fromRoute('config_update_ui.report', ['report_type' => 'profile']),
      ];
      $build['links']['#rows'][] = [
        $this->t('Installation profile'),
        [
          'data' => [
            '#type' => 'operations',
            '#links' => $links,
          ],
        ],
      ];
    }

    return $build;
  }

  /**
   * Generates a report about config updates.
   *
   * @param string $report_type
   *   Type of report to generate: 'type', 'module', 'theme', or 'profile'.
   * @param string $value
   *   Machine name of a configuration type, module, or theme to generate the
   *   report for. Ignored for profile, since that uses the active profile.
   *
   * @return array
   *   Render array for the updates report. Empty if invalid or missing
   *   report type or value.
   */
  protected function generateReport($report_type, $value) {
    // Figure out what to name the report, and incidentally, validate that
    // $value exists for this type of report.
    switch ($report_type) {
      case 'type':
        if ($value == 'system.all') {
          $label = $this->t('All configuration');
        }
        elseif ($value == 'system.simple') {
          $label = $this->t('Simple configuration');
        }
        else {
          $definition = $this->configList->getType($value);
          if (!$definition) {
            return NULL;
          }

          $label = $this->t('@name configuration', ['@name' => $definition->getLabel()]);
        }

        break;

      case 'module':
        $list = $this->moduleHandler->getModuleList();
        if (!isset($list[$value])) {
          return NULL;
        }

        $label = $this->t('@name module', ['@name' => $this->moduleHandler->getName($value)]);
        break;

      case 'theme':
        $list = $this->themeHandler->listInfo();
        if (!isset($list[$value])) {
          return NULL;
        }

        $label = $this->t('@name theme', ['@name' => $this->themeHandler->getName($value)]);
        break;

      case 'profile':
        $profile = $this->getProfileName();
        $label = $this->t('@name profile', ['@name' => $this->moduleHandler->getName($profile)]);
        break;

      default:
        return NULL;
    }

    // List the active and extension-provided config.
    list($active_list, $install_list, $optional_list) = $this->configList->listConfig($report_type, $value);

    // Build the report.
    $build = [];

    $build['#title'] = $this->t('Configuration updates report for @label', ['@label' => $label]);
    $build['report_header'] = ['#markup' => '<h3>' . $this->t('Updates report') . '</h3>'];

    // List items missing from site.
    $removed = array_diff($install_list, $active_list);
    $build['removed'] = [
      '#caption' => $this->t('Missing configuration items'),
      '#empty' => $this->t('None: all provided configuration items are in your active configuration.'),
    ] + $this->makeReportTable($removed, 'extension', ['import']);

    // List optional items that are not installed.
    $inactive = array_diff($optional_list, $active_list);
    $build['inactive'] = [
      '#caption' => $this->t('Inactive optional items'),
      '#empty' => $this->t('None: all optional configuration items are in your active configuration.'),
    ] + $this->makeReportTable($inactive, 'extension', ['import']);

    // List items added to site, which only makes sense in the report for a
    // config type.
    $added = array_diff($active_list, $install_list, $optional_list);
    if ($report_type == 'type') {
      $build['added'] = [
        '#caption' => $this->t('Added configuration items'),
        '#empty' => $this->t('None: all active configuration items of this type were provided by modules, themes, or install profile.'),
      ] + $this->makeReportTable($added, 'active', ['export', 'delete']);
    }

    // For differences, we need to go through the array of config in both
    // and see if each config item is the same or not.
    $both = array_diff($active_list, $added);
    $different = [];
    foreach ($both as $name) {
      if (!$this->configDiff->same(
        $this->configRevert->getFromExtension('', $name),
        $this->configRevert->getFromActive('', $name)
      )) {
        $different[] = $name;
      }
    }
    $build['different'] = [
      '#caption' => $this->t('Changed configuration items'),
      '#empty' => $this->t('None: no active configuration items differ from their current provided versions.'),
    ] + $this->makeReportTable($different, 'active',
      ['diff', 'export', 'revert']);

    return $build;
  }

  /**
   * Builds a table for the report.
   *
   * @param string[] $names
   *   List of machine names of config items for the table.
   * @param string $storage
   *   Config storage the items can be loaded from, either 'active' or
   *   'extension'.
   * @param string[] $actions
   *   Action links to include, one or more of:
   *   - diff
   *   - revert
   *   - export
   *   - import
   *   - delete.
   *
   * @return array
   *   Render array for the table, not including the #empty and #prefix
   *   properties.
   */
  protected function makeReportTable(array $names, $storage, array $actions) {
    $build = [];

    $build['#type'] = 'table';

    $build['#attributes'] = ['class' => ['config-update-report']];

    $build['#header'] = [
      'name' => [
        'data' => $this->t('Machine name'),
      ],
      'label' => [
        'data' => $this->t('Label (if any)'),
        'class' => [RESPONSIVE_PRIORITY_LOW],
      ],
      'type' => [
        'data' => $this->t('Type'),
        'class' => [RESPONSIVE_PRIORITY_MEDIUM],
      ],
      'provider' => [
        'data' => $this->t('Provider'),
        'class' => [RESPONSIVE_PRIORITY_LOW],
      ],
      'operations' => [
        'data' => $this->t('Operations'),
      ],
    ];

    $build['#rows'] = [];

    foreach ($names as $name) {
      $row = [];
      if ($storage == 'active') {
        $config = $this->configRevert->getFromActive('', $name);
      }
      else {
        $config = $this->configRevert->getFromExtension('', $name);
      }

      // Figure out what type of config it is, and get the ID.
      $entity_type = $this->configList->getTypeNameByConfigName($name);

      if (!$entity_type) {
        // This is simple config.
        $id = $name;
        $label = '';
        $type_label = $this->t('Simple configuration');
        $entity_type = 'system.simple';
      }
      else {
        $definition = $this->configList->getType($entity_type);
        $id_key = $definition->getKey('id');
        $id = $config[$id_key];
        // The label key is not required.
        if ($label_key = $definition->getKey('label')) {
          $label = $config[$label_key];
        }
        else {
          $label = '';
        }

        $type_label = $definition->getLabel();
      }

      $row[] = $name;
      $row[] = $label;
      $row[] = $type_label;
      $provider = $this->configList->getConfigProvider($name);
      $provider_name = '';
      if (!empty($provider)) {
        switch ($provider[0]) {
          case 'profile':
            $provider_name = $this->moduleHandler->getName($provider[1]);
            if ($provider_name) {
              $provider_name = $this->t('@name profile', ['@name' => $provider_name]);
            }
            else {
              $provider_name = '';
            }
            break;

          case 'module':
            $provider_name = $this->moduleHandler->getName($provider[1]);
            if ($provider_name) {
              $provider_name = $this->t('@name module', ['@name' => $provider_name]);
            }
            else {
              $provider_name = '';
            }
            break;

          case 'theme':
            $provider_name = $this->themeHandler->getName($provider[1]);
            if ($provider_name) {
              $provider_name = $this->t('@name theme', ['@name' => $provider_name]);
            }
            else {
              $provider_name = '';
            }
            break;
        }
      }
      $row[] = $provider_name;

      $links = [];
      $routes = [
        'export' => 'config.export_single',
        'import' => 'config_update_ui.import',
        'diff' => 'config_update_ui.diff',
        'revert' => 'config_update_ui.revert',
        'delete' => 'config_update_ui.delete',
      ];
      $titles = [
        'export' => $this->t('Export'),
        'import' => $this->t('Import from source'),
        'diff' => $this->t('Show differences'),
        'revert' => $this->t('Revert to source'),
        'delete' => $this->t('Delete'),
      ];

      foreach ($actions as $action) {
        $links[$action] = [
          'url' => Url::fromRoute($routes[$action], ['config_type' => $entity_type, 'config_name' => $id]),
          'title' => $titles[$action],
        ];
      }

      $row[] = [
        'data' => [
          '#type' => 'operations',
          '#links' => $links,
        ],
      ];

      $build['#rows'][] = $row;
    }

    return $build;
  }

  /**
   * Compares links for uasort(), to sort by displayed link title.
   */
  protected static function sortLinks($link1, $link2) {
    $title1 = $link1['title'];
    $title2 = $link2['title'];
    if ($title1 == $title2) {
      return 0;
    }
    return ($title1 < $title2) ? -1 : 1;
  }

  /**
   * Returns the name of the install profile.
   *
   * For backwards compatibility with pre/post 8.3.x, tries to get it from
   * either configuration or settings.
   *
   * @return string
   *   The name of the install profile.
   */
  protected function getProfileName() {
    // Code adapted from DrupalKernel::getInstalProfile() in Core.
    // In Core 8.3.x or later, read from config.
    $profile = $this->configFactory->get('core.extension')->get('profile');
    if (!empty($profile)) {
      return $profile;
    }
    else {
      // If system_update_8300() has not yet run, use settings.
      return Settings::get('install_profile');
    }
  }

}
