<?php

namespace DrupalCodeGenerator\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;

/**
 * Implements generate command.
 */
class Navigation extends Command {

  /**
   * Menu tree.
   *
   * @var array
   */
  protected $menuTree;

  /**
   * Name of the generator to execute.
   *
   * @var string
   */
  protected $generatorName;

  /**
   * Command labels.
   *
   * @var array
   */
  protected $labels = [
    'd7' => 'Drupal 7',
    'd8' => 'Drupal 8',
  ];

  /**
   * Aliases for some sub-menus.
   *
   * @var array
   */
  protected $defaultAliases = [
    'service' => 'd8:service',
    'plugin' => 'd8:plugin',
    'theme' => 'd8:theme',
    'module' => 'd8:module',
    'form' => 'd8:form',
    'test' => 'd8:test',
    'yml' => 'd8:yml',
    'links' => 'd8:yml:links',
  ];

  /**
   * Constructs menu command.
   *
   * @param \DrupalCodeGenerator\Command\GeneratorInterface[] $commands
   *   List of registered commands.
   */
  public function __construct(array $commands) {
    parent::__construct();

    // Initialize the menu structure.
    $this->menuTree = [];
    $aliases = array_keys($this->defaultAliases);

    // Build aliases for the navigation based on command namespaces.
    foreach ($commands as $command) {
      $command_name = $command->getName();
      $sub_names = explode(':', $command_name);

      $this->arraySetNestedValue($this->menuTree, $sub_names, TRUE);

      // The last sub-name is actual command name so it cannot be used as an
      // alias for navigation command.
      $last_sub_name = array_pop($sub_names);

      // Collect command labels.
      if ($label = $command->getLabel()) {
        $this->labels[$last_sub_name] = $label;
      }

      // We cannot use $application->getNamespaces() here because the
      // application is not available at this point.
      $alias = '';
      foreach ($sub_names as $sub_name) {
        $alias = $alias ? $alias . ':' . $sub_name : $sub_name;
        $aliases[] = $alias;
      }
    }

    $this->setAliases(array_unique($aliases));
    $this->recursiveKsort($this->menuTree);
  }

  /**
   * {@inheritdoc}
   */
  public function getUsages() {
    return ['<generator>'];
  }

  /**
   * {@inheritdoc}
   */
  protected function configure() {
    $this
      ->setName('navigation')
      ->setDescription('Provides an interactive menu to select generator')
      ->setHelp('Run `dcg list` to check out all available generators.')
      ->setHidden(TRUE)
      ->addOption(
        'directory',
        '-d',
        InputOption::VALUE_OPTIONAL,
        'Working directory'
      )
      ->addOption(
        'answers',
        '-a',
        InputOption::VALUE_OPTIONAL,
        'Default JSON formatted answers'
      );
  }

  /**
   * {@inheritdoc}
   */
  protected function interact(InputInterface $input, OutputInterface $output) {
    $style = new OutputFormatterStyle('black', 'cyan', []);
    $output->getFormatter()->setStyle('title', $style);

    $command_name = $input->getFirstArgument();

    // Before version 3.3.6 of Symfony console getFistArgument returned default
    // command name.
    $command_name = $command_name == 'navigation' ? NULL : $command_name;

    if (isset($this->defaultAliases[$command_name])) {
      $command_name = $this->defaultAliases[$command_name];
    }

    $menu_trail = $command_name ? explode(':', $command_name) : [];

    $this->generatorName = $this->selectGenerator($input, $output, $menu_trail);
  }

  /**
   * {@inheritdoc}
   */
  protected function execute(InputInterface $input, OutputInterface $output) {

    if (!$this->generatorName) {
      return 0;
    }

    // Run the generator.
    return $this->getApplication()
      ->find($this->generatorName)
      ->run($input, $output);
  }

  /**
   * Returns a generator selected by the user from a multilevel console menu.
   *
   * @param \Symfony\Component\Console\Input\InputInterface $input
   *   Input instance.
   * @param \Symfony\Component\Console\Output\OutputInterface $output
   *   Output instance.
   * @param array $menu_trail
   *   Menu trail.
   *
   * @return string|null
   *   Generator name or null if user decided to exit the navigation.
   */
  protected function selectGenerator(InputInterface $input, OutputInterface $output, array $menu_trail) {

    // Narrow down menu tree.
    $active_menu_tree = $this->menuTree;
    foreach ($menu_trail as $active_menu_item) {
      $active_menu_tree = $active_menu_tree[$active_menu_item];
    }

    // The $active_menu_tree can be either an array of menu items or TRUE if the
    // user reached the final menu point.
    if ($active_menu_tree === TRUE) {
      return implode(':', $menu_trail);
    }

    $sub_menu_labels = $command_labels = [];
    foreach ($active_menu_tree as $menu_item => $subtree) {
      if (is_array($subtree)) {
        $sub_menu_labels[$menu_item] = $this->createMenuItemLabel($menu_item, TRUE);
      }
      else {
        $command_labels[$menu_item] = $this->createMenuItemLabel($menu_item, FALSE);
      }
    }
    asort($sub_menu_labels);
    asort($command_labels);

    // Generally the choices array consists of the following parts:
    // - Reference to the parent menu level.
    // - Sorted list of nested menu levels.
    // - Sorted list of commands.
    $choices = ['..' => '..'] + $sub_menu_labels + $command_labels;
    $question = new ChoiceQuestion('<title> Select generator: </title>', array_values($choices));
    $question->setPrompt(count($choices) <= 10 ? '  ➤➤➤ ' : '  ➤➤➤➤ ');

    $answer_label = $this->getHelper('question')->ask($input, $output, $question);
    $answer = array_search($answer_label, $choices);

    if ($answer == '..') {
      // Exit the application if the user selected zero on the top menu level.
      if (count($menu_trail) == 0) {
        return NULL;
      }
      // Decrease menu level.
      array_pop($menu_trail);
    }
    else {
      // Increase menu level.
      $menu_trail[] = $answer;
    }

    return $this->selectGenerator($input, $output, $menu_trail);
  }

  /**
   * Creates a human readable label for a given menu item.
   *
   * @param string $menu_item
   *   Machine name of the menu item.
   * @param bool $comment
   *   A boolean indicating that the label should be wrapped with comment tag.
   *
   * @return string
   *   The menu label.
   */
  protected function createMenuItemLabel($menu_item, $comment) {
    $label = isset($this->labels[$menu_item]) ?
      $this->labels[$menu_item] : str_replace(['-', '_'], ' ', ucfirst($menu_item));
    return $comment ? "<comment>$label</comment>" : $label;
  }

  /**
   * Sort multi-dimensional array by keys.
   *
   * @param array $array
   *   An array being sorted.
   *
   * @return array
   *   Sorted array.
   */
  protected function recursiveKsort(array &$array) {
    foreach ($array as &$value) {
      if (is_array($value)) {
        $this->recursiveKsort($value);
      }
    }
    return ksort($array);
  }

  /**
   * Sets a value in a nested array with variable depth.
   *
   * @param array $array
   *   A reference to the array to modify.
   * @param array $parents
   *   An array of parent keys, starting with the outermost key.
   * @param mixed $value
   *   The value to set.
   *
   * @see https://api.drupal.org/api/drupal/includes!common.inc/function/drupal_array_set_nested_value/7
   */
  protected function arraySetNestedValue(array &$array, array $parents, $value) {
    $ref = &$array;
    foreach ($parents as $parent) {
      if (isset($ref) && !is_array($ref)) {
        $ref = [];
      }
      $ref = &$ref[$parent];
    }
    if (!isset($ref)) {
      $ref = $value;
    }
  }

}
