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

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

if ( ! class_exists( 'OrokuBlocks_Settings' ) ) {
  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_updateSettingsAjax', 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 );
    }
  }
}