<?php

namespace LakeDrops\Component\Composer;

use Composer\Json\JsonFile;
use Seld\JsonLint\ParsingException;
use Symfony\Component\Yaml\Yaml;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Twig\Loader\ArrayLoader;

/**
 * Manages project config.
 */
final class Config {

  /**
   * All config values.
   *
   * @var array
   */
  private static array $values;

  /**
   * Custom config values.
   *
   * @var array
   */
  private static array $customValues;

  /**
   * Config values to overwrite.
   *
   * @var array
   */
  private static array $overwriteValues;

  /**
   * Use specific config values.
   *
   * @var array
   */
  private static array $userValues;

  /**
   * The component for which this config object holds the values.
   *
   * @var string
   */
  private string $component;

  /**
   * The Twig array loaded.
   *
   * @var \Twig\Loader\ArrayLoader
   */
  private ArrayLoader $twigLoader;

  /**
   * The Twig environment.
   *
   * @var \Twig\Environment
   */
  private Environment $twig;

  /**
   * The .env service.
   *
   * @var \LakeDrops\Component\Composer\Dotenv
   */
  private Dotenv $env;

  /**
   * The filename of the config file.
   *
   * @var string
   */
  private string $configFile;

  /**
   * The filename of user specific config file.
   *
   * @var string
   */
  private string $userConfigFile;

  /**
   * Config constructor.
   *
   * @param string $component
   *   The component for which this config object holds the values.
   * @param array $default_values
   *   The default configuration from the component.
   * @param \LakeDrops\Component\Composer\Dotenv $env
   *   The .env service.
   */
  public function __construct(string $component, array $default_values, Dotenv $env) {
    $this->component = $component;
    $this->env = $env;
    $this->twigLoader = new ArrayLoader([]);
    $this->twig = new Environment($this->twigLoader);
    $this->configFile = getcwd() . '/.lakedrops.yml';
    $this->userConfigFile = getcwd() . '/.lakedrops.user.yml';

    $this->init();
    $this->merge($default_values, self::$customValues[$this->component] ?? []);
    $this->merge([], self::$overwriteValues[$this->component] ?? [], FALSE);
    $this->merge([], self::$userValues[$this->component] ?? [], FALSE);
    $this->migrateFromComposerJson();
  }

  /**
   * Read project configuration if not already happened.
   */
  private function init(): void {
    if (!isset(self::$values)) {
      self::$values = [];
      self::$overwriteValues = [];
      if (!file_exists($this->configFile)) {
        self::$customValues = [];
        $this->save();
      }
      else {
        self::$customValues = Yaml::parseFile($this->configFile);
        // Apply overwrite by stage declarations.
        $stage = getenv('PROJECT_BRANCH');
        if (isset(self::$customValues['stage_overwrites'][$stage])) {
          self::$overwriteValues = self::$customValues['stage_overwrites'][$stage];
        }
      }
      if (!file_exists($this->userConfigFile)) {
        self::$userValues = [];
      }
      else {
        self::$userValues = Yaml::parseFile($this->userConfigFile);
      }
    }
  }

  /**
   * Migrate custom settings from composer.json if required.
   */
  private function migrateFromComposerJson(): void {
    $filename = getcwd() . '/composer.json';
    $jsonFile = new JsonFile($filename);
    try {
      $content = $jsonFile->read();
    }
    catch (ParsingException $e) {
      $content = [];
    }
    if (isset($content['extra'][$this->component])) {
      $this->merge([], $content['extra'][$this->component]);
      $this->save();
      unset($content['extra'][$this->component]);
      try {
        $jsonFile->write($content);
      }
      catch (\Exception $ex) {
        // Ignored, if composer.json is read-only there is a general problem.
      }
    }
  }

  /**
   * Helper function to merge and optionally store config values.
   *
   * @param array $defaultValues
   *   Array with default values.
   * @param array $customValues
   *   Array with custom value to be merged on top of default.
   * @param bool $store
   *   TRUE, if the resulting array should be stored back to the config file.
   */
  private function merge(array $defaultValues, array $customValues, bool $store = TRUE): void {
    $default = self::$values[$this->component] ?? [];
    $custom = self::$customValues[$this->component] ?? [];
    if (!empty($defaultValues)) {
      $default = NestedArray::mergeDeep($default, $defaultValues);
    }
    if (!empty($customValues)) {
      $default = NestedArray::mergeDeep($default, $customValues);
      if ($store) {
        $custom = NestedArray::mergeDeep($custom, $customValues);
      }
    }
    self::$values[$this->component] = $this->env->replaceEnvironmentVariables($default);
    self::$customValues[$this->component] = $custom;
  }

  /**
   * Save the current settings.
   */
  private function save(): void {
    file_put_contents($this->configFile, Yaml::dump(self::$customValues, 9, 2));
  }

  /**
   * Read a value from config array.
   *
   * @param array $values
   *   The array holding all config values.
   * @param array $keys
   *   List of keys of how we should dive deep into the value array.
   *
   * @return mixed|null
   *   The found value or NULL, if it doesn't exist.
   */
  private function readValueFromArray(array $values, array $keys) {
    $key = array_shift($keys);
    if (!isset($values[$key])) {
      return NULL;
    }
    if (empty($keys)) {
      return $values[$key];
    }
    return $this->readValueFromArray($values[$key], $keys);
  }

  /**
   * Read value from config.
   *
   * @param string|array $keys
   *   List of keys of how we should dive deep into the value array. If a top
   *   level value should be read, the key can be provided as a string instead
   *   of an array.
   *
   * @return mixed|null
   *   The found value or NULL, if it doesn't exist.
   */
  public function readValue($keys) {
    if (is_string($keys)) {
      $keys = [$keys];
    }
    return $this->readValueFromArray(self::$values[$this->component], $keys);
  }

  /**
   * Set a config value.
   *
   * @param string $key
   *   The top level key of the value in the config array.
   * @param mixed $value
   *   The value to be stored.
   * @param bool $store
   *   Whether the new value should be stored in the config file.
   */
  public function setValue(string $key, $value, bool $store = TRUE): void {
    $this->merge([], [$key => $value], $store);
    $this->save();
  }

  /**
   * Render a Twig template by using config values.
   *
   * @param string $filename
   *   The filename of the Twig template.
   * @param string $content
   *   The content to be rendered.
   *
   * @return string
   *   The rendered string.
   */
  public function render(string $filename, string $content): string {
    $this->twigLoader->setTemplate($filename, $content);
    try {
      return $this->twig->render($filename, self::$values[$this->component]);
    }
    catch (LoaderError | RuntimeError | SyntaxError $e) {
    }
    return '';
  }

}