<?php

namespace Drupal\DrupalExtension\Context\Environment\Reader;

use Behat\Behat\Context\Environment\UninitializedContextEnvironment;
use Behat\Behat\Context\Environment\ContextEnvironment;
use Behat\Behat\Context\Reader\ContextReader;
use Behat\Testwork\Call\Callee;
use Behat\Testwork\Environment\Environment;
use Behat\Testwork\Environment\Exception\EnvironmentReadException;
use Behat\Testwork\Environment\Reader\EnvironmentReader;

use Drupal\DrupalDriverManager;
use Drupal\Driver\SubDriverFinderInterface;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;

/**
 * Read in additional contexts provided by core and contrib.
 */
final class Reader implements EnvironmentReader {

  /**
   * @var ContextReader[]
   */
  private $contextReaders = array();

  /**
   * Drupal driver manager.
   *
   * @var \Drupal\DrupalDriverManager
   */
  private $drupal;

  /**
   * Configuration parameters for this suite.
   */
  private $parameters;

  /**
   * Statically cached lists of subcontexts by path.
   *
   * @var array
   */
  static protected $subContexts;

  /**
   * Register the Drupal driver manager.
   */
  public function __construct(DrupalDriverManager $drupal, array $parameters) {
    $this->drupal = $drupal;
    $this->parameters = $parameters;
  }

  /**
   * Registers context loader.
   *
   * @param ContextReader $contextReader
   */
  public function registerContextReader(ContextReader $contextReader) {
    $this->contextReaders[] = $contextReader;
  }

  /**
   * {@inheritdoc}
   */
  public function supportsEnvironment(Environment $environment) {
    return $environment instanceof ContextEnvironment;
  }

  /**
   * {@inheritdoc}
   */
  public function readEnvironmentCallees(Environment $environment) {

    if (!$environment instanceof ContextEnvironment) {
      throw new EnvironmentReadException(sprintf(
          'ContextEnvironmentReader does not support `%s` environment.',
          get_class($environment)
        ), $environment);
    }

    $callees = array();
    if (!$environment instanceof UninitializedContextEnvironment) {
      return $callees;
    }

    $contextClasses = $this->findSubContextClasses();

    foreach ($contextClasses as $contextClass) {
      // When executing test scenarios with an examples table the registering of
      // contexts is handled differently in newer version of Behat. Starting
      // with Behat 3.2.0 the contexts are already registered, and the callees
      // are returned by the default reader.
      // Work around this and provide compatibility with Behat 3.1.0 as well as
      // 3.2.0 and higher by checking if the class already exists before
      // registering it and returning the callees.
      // @see https://github.com/Behat/Behat/issues/758
      if (!$environment->hasContextClass($contextClass)) {
        $callees = array_merge(
          $callees,
          $this->readContextCallees($environment, $contextClass)
        );

        // Register context.
        $environment->registerContextClass($contextClass, array($this->drupal));
      }
    }

    return $callees;
  }

    /**
     * Reads callees from a specific suite's context.
     *
     * @param ContextEnvironment $environment
     * @param string             $contextClass
     *
     * @return Callee[]
     */
    private function readContextCallees(ContextEnvironment $environment, $contextClass)
    {
        $callees = array();
        foreach ($this->contextReaders as $loader) {
            $callees = array_merge(
                $callees,
                $loader->readContextCallees($environment, $contextClass)
            );
        }

        return $callees;
    }

  /**
   * Finds and loads available subcontext classes.
   */
  private function findSubContextClasses() {
    $class_names = array();

    // Initialize any available sub-contexts.
    if (isset($this->parameters['subcontexts'])) {
      $paths = array();
      // Drivers may specify paths to subcontexts.
      if ($this->parameters['subcontexts']['autoload']) {
        foreach ($this->drupal->getDrivers() as $name => $driver) {
          if ($driver instanceof SubDriverFinderInterface) {
            $paths += $driver->getSubDriverPaths();
          }
        }
      }

      // Additional subcontext locations may be specified manually in behat.yml.
      if (isset($this->parameters['subcontexts']['paths'])) {
        $paths = array_merge($paths, $this->parameters['subcontexts']['paths']);
      }

      // Load each class.
      foreach ($paths as $path) {
        if ($subcontexts = $this->findAvailableSubContexts($path)) {
          $this->loadSubContexts($subcontexts);
        }
      }

      // Find all subcontexts, excluding abstract base classes.
      $classes = get_declared_classes();
      foreach ($classes as $class) {
        $reflect = new \ReflectionClass($class);
        if (!$reflect->isAbstract() && $reflect->implementsInterface('Drupal\DrupalExtension\Context\DrupalSubContextInterface')) {
          $class_names[] = $class;
        }
      }

    }

    return $class_names;
  }

  /**
   * Find Sub-contexts matching a given pattern located at the passed path.
   *
   * @param string $path
   *   Absolute path to the directory to search for sub-contexts.
   * @param string $pattern
   *   File pattern to match. Defaults to `/^.+\.behat\.inc/i`.
   *
   * @return array
   *   An array of paths.
   */
  private function findAvailableSubContexts($path, $pattern = '/^.+\.behat\.inc/i') {

    if (isset(static::$subContexts[$pattern][$path])) {
      return static::$subContexts[$pattern][$path];
    }

    static::$subContexts[$pattern][$path] = array();

    $fileIterator = new RegexIterator(
      new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($path)
      ), $pattern,
      RegexIterator::MATCH
    );
    foreach ($fileIterator as $found) {
      static::$subContexts[$pattern][$path][$found->getRealPath()] = $found->getFileName();
    }

    return static::$subContexts[$pattern][$path];
  }

  /**
   * Load each subcontext file.
   *
   * @param array $subcontexts
   *   An array of files to include.
   */
  private function loadSubContexts($subcontexts) {
    foreach ($subcontexts as $path => $subcontext) {
      if (!file_exists($path)) {
        throw new \RuntimeException(sprintf('Subcontext path %s path does not exist.', $path));
      }

      // Load file.
      require_once $path;
    }
  }

}
