<?php

/**
 * Class CRM_Afform_AfformScanner
 *
 * The AfformScanner searches the `ang` directory of extensions and `civicrm.files` for files
 * named `*.aff.*`. Each item is interpreted as a form instance.
 *
 * To reduce file-scanning, we keep a cache of file paths.
 */
class CRM_Afform_AfformScanner {

  const METADATA_JSON = 'aff.json';

  const METADATA_PHP = 'aff.php';

  const LAYOUT_FILE = 'aff.html';

  const FILE_REGEXP = '/\.aff\.(json|html|php)$/';

  const DEFAULT_REQUIRES = 'afCore';

  /**
   * @var CRM_Utils_Cache_Interface
   */
  protected $cache;

  /**
   * CRM_Afform_AfformScanner constructor.
   */
  public function __construct() {
    $this->cache = Civi::cache('long');
  }

  /**
   * Get a list of all forms and their file paths.
   *
   * @return array
   *   Ex: ['afformViewIndividual' => ['/var/www/foo/ang/afformViewIndividual']]
   */
  public function findFilePaths(): array {
    if ($this->isUseCachedPaths()) {
      $paths = $this->cache->get('afformAllPaths');
      if ($paths !== NULL) {
        return $paths;
      }
    }

    $paths = [];

    $mapper = CRM_Extension_System::singleton()->getMapper();
    foreach ($mapper->getModules() as $module) {
      try {
        if ($module->is_active) {
          $this->appendFilePaths($paths, dirname($mapper->keyToPath($module->name)) . DIRECTORY_SEPARATOR . 'ang', $module->name);
        }
      }
      catch (CRM_Extension_Exception_MissingException $e) {
        // If the extension is missing skip & continue.
      }
    }

    // Scan core ang/afform directory
    $this->appendFilePaths($paths, Civi::paths()->getPath('[civicrm.root]/ang/afform'), 'civicrm');
    // Scan uploads/files directory
    $this->appendFilePaths($paths, $this->getSiteLocalPath(), '');

    if ($this->isUseCachedPaths()) {
      $this->cache->set('afformAllPaths', $paths);
    }
    return $paths;
  }

  /**
   * Is the cache to be used.
   *
   * Skipping the cache helps developers moving files around & messes with developers
   * debugging performance. It's a cruel world.
   *
   * FIXME: Use a separate setting. Maybe use the asset-builder cache setting?
   *
   * @return bool
   */
  private function isUseCachedPaths(): bool {
    return !CRM_Core_Config::singleton()->debug;
  }

  /**
   * Get the absolute path to the given file.
   *
   * @param string $formName
   *   Ex: 'afformViewIndividual'
   * @param string $suffix
   *   Ex: 'aff.json'
   * @return string|NULL
   *   Ex: '/var/www/sites/default/files/civicrm/ang/afform/afformViewIndividual.aff.json'
   */
  public function findFilePath(string $formName, string $suffix): ?string {
    $paths = $this->findFilePaths();

    if (isset($paths[$formName])) {
      foreach ($paths[$formName] as $path) {
        if (file_exists($path . '.' . $suffix)) {
          return $path . '.' . $suffix;
        }
      }
    }

    return NULL;
  }

  /**
   * Determine the path where we can write our own customized/overridden
   * version of a file.
   *
   * @param string $formName
   *   Ex: 'afformViewIndividual'
   * @param string $fileType
   *   Ex: 'aff.json'
   * @return string
   *   Ex: '/var/www/sites/default/files/civicrm/afform/afformViewIndividual.aff.json'
   */
  public function createSiteLocalPath(string $formName, string $fileType): string {
    return $this->getSiteLocalPath() . DIRECTORY_SEPARATOR . $formName . '.' . $fileType;
  }

  public function clear(): void {
    $this->cache->delete('afformAllPaths');
  }

  /**
   * Get the effective metadata for a form.
   *
   * @param string $name
   *   Ex: 'afformViewIndividual'
   * @return array
   *   An array with some mix of the following keys: name, title, description, server_route, requires, is_public.
   *   NOTE: This is only data available in *.aff.json. It does *NOT* include layout.
   *   Ex: [
   *     'name' => 'afformViewIndividual',
   *     'title' => 'View an individual contact',
   *     'server_route' => 'civicrm/view-individual',
   *     'requires' => ['afform'],
   *   ]
   */
  public function getMeta(string $name): ?array {
    // FIXME error checking

    $defaults = [
      'requires' => [],
      'title' => '',
      'description' => '',
      'is_public' => FALSE,
      'permission' => ['access CiviCRM'],
      'type' => 'system',
    ];
    $defn = [];

    // If there is a local file it will be json - read from that first
    $jsonFile = $this->findFilePath($name, self::METADATA_JSON);
    if ($jsonFile !== NULL) {
      $defn = json_decode(file_get_contents($jsonFile), 1);
    }
    // Extensions may provide afform definitions in php files
    else {
      $phpFile = $this->findFilePath($name, self::METADATA_PHP);
      if ($phpFile !== NULL) {
        $defn = include $phpFile;
      }
    }
    // A form must have at least a layout file (if no metadata file, just use defaults)
    if (!$defn && !$this->findFilePath($name, self::LAYOUT_FILE)) {
      return NULL;
    }
    $defn = array_merge($defaults, $defn, ['name' => $name]);
    // Previous revisions of GUI allowed permission==''. array_merge() doesn't catch all forms of missing-ness.
    if (empty($defn['permission'])) {
      $defn['permission'] = $defaults['permission'];
    }
    return $defn;
  }

  /**
   * Adds base_module, has_local & has_base to an afform metadata record
   *
   * @param array $record
   */
  public function addComputedFields(array &$record) {
    $name = $record['name'];
    // Ex: $allPaths['viewIndividual']['org.civicrm.foo'] == '/var/www/foo/ang/afformViewIndividual'].
    $allPaths = $this->findFilePaths()[$name] ?? [];
    // Empty string key refers to the site local path
    $record['has_local'] = isset($allPaths['']);
    if (!isset($record['has_base'])) {
      $record['base_module'] = \CRM_Utils_Array::first(array_filter(array_keys($allPaths)));
      $record['has_base'] = !empty($record['base_module']);
    }
  }

  /**
   * @param string $formName
   *   Ex: 'view-individual'
   * @return string|NULL
   *   Ex: '<em>Hello world!</em>'
   *   NULL if no layout exists
   */
  public function getLayout($formName) {
    $filePath = $this->findFilePath($formName, self::LAYOUT_FILE);
    return $filePath === NULL ? NULL : file_get_contents($filePath);
  }

  /**
   * Get the effective metadata for all file-based forms.
   *
   * @return array
   *   A list of all forms, keyed by form name.
   *   NOTE: This is only data available in metadata files. It does *NOT* include layout.
   *   Ex: ['afformViewIndividual' => ['title' => 'View an individual contact', ...]]
   */
  public function getMetas(): array {
    $result = [];
    foreach (array_keys($this->findFilePaths()) as $name) {
      $result[$name] = $this->getMeta($name);
    }
    return $result;
  }

  /**
   * @param array[] $formPaths
   *   List of all form paths.
   *   Ex: ['foo' => [0 => '/var/www/org.example.foobar/ang']]
   * @param string $parent
   *   Ex: '/var/www/org.example.foobar/afform/'
   * @param string $module
   *   Name of module or '' empty string for local files.
   */
  private function appendFilePaths(array &$formPaths, string $parent, string $module) {
    $files = preg_grep(self::FILE_REGEXP, (array) glob("$parent/*"));

    foreach ($files as $file) {
      $fileBase = preg_replace(self::FILE_REGEXP, '', $file);
      $name = basename($fileBase);
      $formPaths[$name][$module] = $fileBase;
      // Local files get top priority
      ksort($formPaths[$name]);
    }
  }

  /**
   * Get the path where site-local form customizations are stored.
   *
   * @return string
   *   Ex: '/var/www/sites/default/files/civicrm/afform'.
   */
  public function getSiteLocalPath(): string {
    // TODO Allow a setting override.
    // return Civi::paths()->getPath(Civi::settings()->get('afformPath'));
    return Civi::paths()->getPath('[civicrm.files]/ang');
  }

}
