<?php
/**
 * OrokuBlocks Settings
 * 
 * @package OrokuBlocks
 * @author OrokuBlocks (https://orokublocks.com/)
 * 
 * @since 1.0.0
 * 
 */

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
  exit;
}

final class OrokuBlocks_Settings {
  /**
   * @var array Settings by id
   * 
   * @access private
   */
  private $settings = [];

  /**
   * Prevent more than one instance from being created
   * 
   * @since 1.0.0
   * 
   * @access private
   */
  private function __construct() {}

  /**
   * Prevent cloning
   * 
   * @since 1.0.0
   * 
   * @access private
   */
  private function __clone() {}

  /**
   * Returns the class instance
   * 
   * @since 1.0.0
   * 
   * @return OrokuBlocks_Settings $instance  Class instance
   */
  public static function getInstance() {
    static $instance = null;

    if ( $instance === null ) {
      $instance = new OrokuBlocks_Settings();

      // setup
      $instance->initialize();
      $instance->addActions();
    }

    return $instance;
  }

  /**
   * Initialize property values
   * 
   * @since 1.0.0
   * 
   * @access private
   */
  private function initialize() {
    // initialize settings
    $this->initializeSettings();

    // update version
    $current_version = orokublocks()->version;
    $previous_version = $this->settings['version']['current'];

    if ( $current_version !== $previous_version ) {
      $this->updateSetting( 'version', [ 'current' => $current_version, 'previous' => $previous_version ] );
    }
  }

  /**
   * Register actions
   * 
   * @since 1.0.0
   * 
   * @access private
   */
  private function addActions() {
    // Ajax
    add_action( 'wp_ajax_orokublocks_settings_update_ajax', array( $this, 'updateSettingsAjax' ) );
  }

  /**
   * Return plugin settings
   * 
   * @since 1.0.0
   * 
   * @return array      Plugin settings.
   */
  public function getSettings() {
    return $this->settings;
  }

  /**
   * Return setting form ids.
   * 
   * @since 1.0.0
   * 
   * @return array $setting_form_ids        Setting form ids.
   */
  public function getSettingFormIds() {
    $setting_form_ids = [ 'oroku-widget-settings' ];

    return apply_filters( 'orokublocks_settings_form_ids', $setting_form_ids );
  }

  /**
   * Returns setting schema
   * 
   * @since 1.0.0
   * 
   * @return array $setting_schema      Setting schema.
   * 
   * @access private
   */
  private function getSettingSchema() {
    $setting_schema = [
      'version' => [
        'current'   => [
          'id'      => 'current',
          'type'    => 'text',
          'default' => '1.0.0'
        ],
        'previous'  => [
          'id'      => 'previous',
          'type'    => 'text',
          'default' => '1.0.0'
        ]
      ]
    ];

    // initialize widget status schema
    $setting_schema['widget_enabled'] = [];

    foreach ( orokublocks()->widgets as $widget_id => $widget ) {
      $setting_schema['widget_enabled'][$widget_id] = [
        'id'      => $widget_id,
        'name'    => $widget['name'],
        'type'    => 'checkbox',
        'default' => true
      ];
    }

    return $setting_schema;
  }

  /**
   * Returns setting sections metadata.
   * 
   * @since 1.0.0
   * 
   * @return array $setting_sections_meta       Setting sections metadata.
   * 
   * @access private
   */
  private function getSettingSectionsMeta() {
    $setting_sections_meta = [];

    return $setting_sections_meta;
  }

  /**
   * Return true if the provided setting id is a valid setting of that section
   * 
   * @since 1.0.0
   * 
   * @param string $setting_section_id        Id of the setting section
   * @param string $setting_id                Id of the setting to validate
   * @return bool  $setting_is_valid          True if setting is valid, false otherwise
   * 
   * @access private
   */
  private function settingIsValid( $setting_section_id, $setting_id ) {
    return array_key_exists( $setting_section_id, $this->settings )  && array_key_exists( $setting_id, $this->settings[$setting_section_id] );
  }

  /**
   * Return true if the provided option name is a valid option for that section and setting
   * 
   * @since 1.0.0
   * 
   * @param string $setting_section_id    Id of the section the setting is included in.
   * @param string $setting_id            Id of the setting to check options from.
   * @param string $option_id             Id of the option to validate.
   * @return bool  $option_is_valid       True if the option name is valid, false otherwise.
   * 
   * @access private
   */
  public function isValidSelectOption( $setting_section_id, $setting_id, $option_id ) {
    // if it is a valid setting
    if ( $this->settingIsValid( $setting_section_id, $setting_id ) ) {
      $setting_schema = $this->getSettingSchema();

      $setting = $setting_schema[$setting_section_id][$setting_id];

      // do not continue if the setting is not a of select type
      if ( $setting['type'] !== 'select' ) {
        return false;
      }

      // check if the option id is valid
      foreach ( $setting['options'] as $option ) {
        if ( $option['id'] === $option_id ) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Initialize settings
   * 
   * @since 1.0.0
   * 
   * @access private
   */
  private function initializeSettings() {
    $setting_schema = $this->getSettingSchema();

    foreach ( $setting_schema as $setting_section_id => $settings ) {
      // initialize setting section
      $this->settings[$setting_section_id] = [];

      // initialize settings for that section
      // without overwriting user saved settings
      $setting_defaults = [];

      foreach ( $settings as $setting_id => $setting ) {
        $setting_defaults[$setting_id] = $setting['default'];
      }

      $setting_saved_values = get_option( 'orokublocks_' . $setting_section_id, [] );
      $setting_updated_values = array_merge( $setting_defaults, $setting_saved_values );

      $this->updateOption( $setting_section_id, $setting_updated_values );
      $this->settings[$setting_section_id] = $setting_updated_values;
    }
  }

  /**
   * Return settings sections
   * 
   * @since 1.0.0
   * 
   * @param array $allowed_setting_sections         Array of setting sections ids to get.
   * @return array $setting_sections {
   *   @type string $id         Setting section id.
   *   @type string $name       Setting section display name.
   *   @type string $icon       Name of the icon to put next to the settings display name. Optional.
   *   @type array  $settings {
   *     @type string $id         Setting id.
   *     @type string $name       Setting display name.
   *     @type string $type       Setting input type.
   *     @type string $default    Setting default value.
   *     @type string $value      Setting current saved value.
   *     @type array  $options {  Optional. Required when type is set to 'select'.
   *       @type string $id     Select option id.
   *       @type string $name   Select option display name.
   *     }
   *   }
   * }
   */
  private function getSettingsSections( $allowed_setting_sections ) {
    $settings_sections_data = $this->getSettingSectionsMeta();
    $setting_schema = $this->getSettingSchema();

    $setting_sections = [];

    foreach ( $setting_schema as $setting_section_id => $settings ) {
      // don't add blacklisted setting sections
      if ( in_array( $setting_section_id, $allowed_setting_sections ) ) {
        $setting_sections[$setting_section_id] = $settings_sections_data[$setting_section_id];
        $setting_sections[$setting_section_id]['settings'] = [];

        foreach ( $settings as $setting_id => $setting ) {
          $setting_sections[$setting_section_id]['settings'][$setting_id] = $setting;
          $setting_sections[$setting_section_id]['settings'][$setting_id]['value'] = $this->settings[$setting_section_id][$setting_id];
        }
      }
    }

    return $setting_sections;
  }

  /**
   * Return general settings sections
   * 
   * @since 1.0.0
   * 
   * @see getSettingsSections     For setting_sections details.
   * 
   * @return array $setting_sections
   */
  public function getGeneralSettingsSections() {
    $allowed_setting_sections = [];

    return $this->getSettingsSections( $allowed_setting_sections );
  }

  /**
   * Returns widget page settings sections
   * 
   * @since 1.0.0
   * 
   * @return array $setting_sections {
   *   @type string $id         Setting section id.
   * }
   */
  public function getWidgetSettingsSections() {
    $setting_sections = [];

    foreach ( orokublocks()->categories as $category_id => $category ) {
      $setting_section = [
        'id'        => $category_id,
        'name'      => $category['name'],
        'settings'  => []
      ];

      if ( array_key_exists( 'plugin', $category ) ) {
        $setting_section['plugin'] = $category['plugin'];
      }

      foreach ( orokublocks()->widgets as $widget_id => $widget ) {
        if ( in_array( $category_id, $widget['categories'] ) ) {
          $widget_setting = [
            'theme'       => array_key_exists( 'theme', $category ) ? $category['theme'] : $category_id,
            'icon'        => $widget_id,
            'title'       => $widget['name'],
            'text'        => sprintf(
              /* translators: number of widgets */
              __( '%s Widgets', 'orokublocks' ),
              count( $widget['versions'] )
            ),
            'unlock_url'  => orokublocks()->getUnlockProUrl(),
            'tags'        => [],
            'links'       => [
              [
                'name'  => __( 'Demo', 'orokublocks' ),
                'href'  => $widget['urls']['demo']
              ],
              [
                'name'  => __( 'Documentation', 'orokublocks' ),
                'href'  => $widget['urls']['documentation']
              ]
            ]
          ];

          foreach ( $widget['tags'] as $tag_id ) {
            $widget_setting['tags'][] = orokublocks()->tags[$tag_id];
          }

          if ( ( ! orokublocks()->widgetHasFreeVersion( $widget_id ) ) && ( ! orokublocks()->isProVersionValid() ) ) {
            $widget_setting['locked'] = true;
          } else {
            $widget_setting['id'] = $widget_id;
            $widget_setting['checked'] = array_key_exists( $widget_id, $this->settings['widget_enabled'] ) ? $this->settings['widget_enabled'][$widget_id] : false;
          }

          $setting_section['settings'][] = $widget_setting;
        }
      }

      if ( count( $setting_section['settings'] ) > 0 ) {
        $setting_sections[$category_id] = $setting_section;
      }
    }

    return $setting_sections;
  }

  /**
   * Updates database option
   * If database option was updated successfully, then also updates local option to match database
   * 
   * @since 1.0.0
   * 
   * @param string  $option_name          Option name
   * @param mixed   $option_value         Option value
   * @return bool   $option_was_updated   True if option was updated, false otherwise
   * 
   * @access private
   */
  private function updateOption( $option_name, $option_value ) {
    $option_value_to_save = $option_value;
    $option_db_name = 'orokublocks_' . $option_name;

    // if updating an option array, merge with existing database option value to avoid removing previous state
    if ( is_array( $option_value_to_save ) ) {
      $option_db_value = get_option( $option_db_name, [] );
      $option_value_to_save = array_merge( $option_db_value, $option_value_to_save );
    }

    $option_was_updated = update_option( $option_db_name, $option_value_to_save );

    // update local options to match database option
    $updated_option_value = $option_value;

    // if updated option value is an array, merge with existing option value if it exists
    if ( is_array( $updated_option_value ) && array_key_exists( $option_name, $this->settings ) ) {
      $updated_option_value = array_merge( $this->settings[$option_name], $updated_option_value );
    }

    $this->settings[$option_name] = $updated_option_value;

    return $option_was_updated;
  }

  /**
   * Update setting value
   * 
   * @since 1.0.0
   * 
   * @param string $setting_section_id        Id of the setting section to update
   * @param array  $settings                  Settings value
   * @return bool  $option_was_updated        True if option was updated, false otherwise
   */
  public function updateSetting( $setting_section_id, $settings ) {
    // only allow to update existing setting sections
    if ( ! array_key_exists( $setting_section_id , $this->settings ) ) {
      return false;
    }

    // only allow to update settings of that section
    $setting_section_value = array_filter(
      $settings,
      function ( $setting_id ) use ( $setting_section_id ) {
        return $this->settingIsValid( $setting_section_id, $setting_id );
      },
      ARRAY_FILTER_USE_KEY
    );

    $setting_schema = $this->getSettingSchema();

    // convert settings values to match setting type if needed
    foreach ( $setting_section_value as $setting_id => $setting_value) {
      $setting_type = $setting_schema[$setting_section_id][$setting_id]['type'];
      // if checkbox type, convert to bool
      if ( $setting_type === 'checkbox' ) {
        $setting_section_value[$setting_id] = rest_sanitize_boolean( $setting_value );
      } else if ( $setting_type === 'select' ) {
        // do not update if not a valid select option
        if ( ! $this->isValidSelectOption( $setting_section_id, $setting_id, $setting_value ) ) {
          return false;
        }
      }
    }
    
    $option_was_updated = $this->updateOption( $setting_section_id, $setting_section_value );

    return $option_was_updated;
  }

  /**
   * Update global settings via Ajax call
   * 
   * @since 1.0.0
   */
  public function updateSettingsAjax() {
    // nonce check, dies early if the nonce cannot be verified
    check_ajax_referer('orokublocks-settings');

    // don't allow unauthorized users
    if ( ! current_user_can( 'manage_options' ) ) {
      wp_send_json_error( 'User authorization error' );
    }

    $data = false;

    if ( isset( $_POST['data'] ) ) {
      $data = wp_unslash( $_POST['data'] );
    }

    $widget_enabled = array_key_exists( 'widget_enabled', $data ) ? $data['widget_enabled'] : false;

    // sanitize data
    $setting_sections = $widget_enabled ? [ 'widget_enabled' => array_map( 'rest_sanitize_boolean', $widget_enabled ) ] : false;

    // data is required to update widget status
    if ( ! $setting_sections ) {
      wp_send_json_error( 'Missing parameters' );
    }

    foreach ( $setting_sections as $setting_section_id => $settings ) {
      $this->updateSetting( $setting_section_id, $settings );
    }

    wp_send_json_success( $this->settings );
  }
}