<?php
namespace WPDRMS\ASP\Core;

use WP_Error;
use WPDRMS\ASP\Core\Models\SearchInstance;
use WPDRMS\ASP\Misc\Themes;
use WPDRMS\ASP\Options\Data\SearchOptions;
use WPDRMS\ASP\Patterns\SingletonTrait;

defined('ABSPATH') or die("You can't access this file directly.");

/**
 * Class WD_ASP_Instances
 *
 * This class handles the data transfer between code and instance data
 *
 * @class         WD_ASP_Instances
 * @version       1.0
 * @package       AjaxSearchPro/Classes/Core
 * @category      Class
 * @author        Ernest Marcinko
 */
class Instances {

	use SingletonTrait;

	public const ALL_INSTANCES = -1;

	/**
	 * This holds the search instances
	 *
	 * @var SearchInstance[]
	 */
	private array $search_instances;

	/**
	 * This holds the search instances without data
	 *
	 * @var array{
	 *     id: int,
	 *     name: string
	 * }
	 */
	private array $instances_no_data;

	/**
	 * When updating or first demand, this variable sets to true, telling that instances need re-parsing
	 *
	 * @var bool
	 */
	private bool $refresh = true;

	/**
	 * The search instances init script JSON data
	 *
	 * @var string[]
	 */
	private array $script_data = array();

	/**
	 * Gets the search instance if exists
	 *
	 * @param int      $id
	 * @param bool     $force_refresh
	 * @param bool|int $check_ownership
	 * @return ( $id is positive-int ? SearchInstance : SearchInstance[] )
	 */
	public function get( int $id = self::ALL_INSTANCES, bool $force_refresh = false, $check_ownership = false ) {
		if ( $this->refresh || $force_refresh ) {
			$this->init();
			$this->refresh = false;
		}

		if ( $check_ownership !== false && !is_super_admin() ) {
			if ( is_int($check_ownership) ) {
				$user_id = $check_ownership;
			} else {
				$user_id = intval( get_current_user_id() );
			}
			foreach ( $this->search_instances as $key => $inst ) {
				if ( $inst['data']['owner'] != 0 && $inst['data']['owner'] != $user_id ) {
					unset($this->search_instances[ $key ]);
				}
			}
		}

		if ( $id > -1 ) {
			return isset($this->search_instances[ $id ]) ? $this->search_instances[ $id ] : array();
		}

		return $this->search_instances;
	}

	/**
	 * Get an option value for a search instance ID
	 *
	 * @param $instance
	 * @param $option_name
	 * @return false|mixed
	 */
	public function getOption( $instance, $option_name ) {
		if ( $this->exists($instance) ) {
			$instance = $this->get($instance);
			return $instance['data'][ $option_name ] ?? false;
		}

		return false;
	}

	/**
	 * Temporary changes the search instance data within the cache variable (not permanent)
	 *
	 * @param int   $id
	 * @param array $data
	 * @return bool|array
	 */
	public function set( $id = 0, $data = array() ) {
		if ( $this->refresh ) {
			$this->init();
			$this->refresh = false;
		}
		if ( isset($this->search_instances[ $id ]) ) {
			$this->search_instances[ $id ]['data'] = array_merge($this->search_instances[ $id ]['data'], $data);
			return true;
		}
		return false;
	}

	/**
	 * Gets the search instance if exists, without data
	 *
	 * @param int  $id
	 * @param bool $force_refresh
	 * @return array
	 */
	public function getWithoutData( $id = self::ALL_INSTANCES, $force_refresh = false ) {
		if ( $this->refresh || $force_refresh ) {
			$this->init();
			$this->refresh = false;
		}
		if ( $id > -1 ) {
			return isset($this->instances_no_data[ $id ]) ? $this->instances_no_data[ $id ] : array();
		}

		return $this->instances_no_data;
	}

	/**
	 * Checks if the given search instance exists
	 *
	 * @param $id
	 * @return bool
	 */
	public function exists( $id = false ) {
		if ( $this->refresh ) {
			$this->init();
			$this->refresh = false;
		}
		if ( $id === false ) {
			return count($this->search_instances) > 0;
		} else {
			return isset($this->search_instances[ $id ]);
		}
	}

	/**
	 * Create a new search instance with the default options set
	 *
	 * @param $name
	 * @param int $owner User ID of the owner
	 * @return bool|int
	 */
	public function add( $name, $owner = 0 ) {
		global $wpdb;

		$this->refresh = true;
		$data          = wd_asp()->options['asp_defaults'];
		$data['owner'] = intval($owner);

		if (
			$wpdb->query(
				'INSERT INTO ' . wd_asp()->db->table('main') . "
                            (name, data) VALUES
                            ('" . esc_sql($name) . "', '" . wd_mysql_escape_mimic(json_encode($data)) . "')"
			) !== false
		) {
			return $wpdb->insert_id;
		}

		return false;
	}

	/**
	 * Import the search from the Lite version, as a new search instance
	 *
	 * @param $name
	 * @param int $owner User ID of the owner
	 * @return bool|int
	 */
	public function importFromLite( $name, $owner = 0 ) {
		global $wpdb;

		$this->refresh = true;
		$data          = wd_asp()->options['asp_defaults'];
		$data['owner'] = intval($owner);

		$lite = get_option('asl_options', array());
		if ( count($lite) > 0 && get_option('asl_version', 0) > 4732 ) {
			// --- Resolve the options from the lite version ---
			// 1. Resolve these as-is, no change required
			$as_is = array(
				'keyword_logic',
				'triggerontype',
				'customtypes',
				'searchintitle',
				'searchincontent',
				'searchinexcerpt',
				'search_in_permalinks',
				'search_in_ids',
				'search_all_cf',
				'customfields',
				'post_status',
				'override_default_results',
				'override_method',
				'exactonly',
				'exact_match_location',
				'searchinterms',
				'charcount',
				'itemscount',
				'orderby_primary',
				'orderby_secondary',
				'show_images',
				'image_width',
				'image_height',
				'image_source1',
				'image_source2',
				'image_source3',
				'image_source4',
				'image_source5',
				'image_source_featured',
				'image_custom_field',
				'show_frontend_search_settings',
				'box_width',
				'resultsposition',
				'defaultsearchtext',
				'showmoreresults',
				'showmoreresultstext',
				'v_res_max_height',
				'results_click_blank',
				'scroll_to_results',
				'resultareaclickable',
				'close_on_document_click',
				'show_close_icon',
				'showauthor',
				'showdate',
				'showdescription',
				'descriptionlength',
				'description_context',
				'noresultstext',
				'didyoumeantext',
				'autocomplete',
				'shortcode_op',
				'striptagsexclude',
				'excludeposts',
				'wpml_compatibility',
				'polylang_compatibility',
				'click_action_location',
				'return_action_location',
				'showcustomtypes',
				// 4.9
				'primary_titlefield',
				'primary_descriptionfield',
				'primary_titlefield_cf',
				'primary_descriptionfield_cf',
				'advtitlefield',
				'advdescriptionfield',
				'kw_exceptions',
				'kw_exceptions_e',
				'single_highlight',
				'single_highlightwholewords',
				'single_highlightcolor',
				'single_highlightbgcolor',
				'single_highlight_scroll',
				'single_highlight_offset',
				'single_highlight_selector',
				'image_display_mode',
			);
			foreach ( $as_is as $key ) {
				if ( isset($lite[ $key ]) ) {
					$data[ $key ] = $lite[ $key ];
				}
			}
			// 2. Resolve by difference in keys only
			$resolve_keys = array(
				'trigger_on_facet_change'  => 'trigger_on_facet',
				'titlefield'               => 'primary_titlefield',
				'descriptionfield'         => 'primary_descriptionfield',
				'titlefield_cf'            => 'primary_titlefield_cf',
				'descriptionfield_cf'      => 'primary_descriptionfield_cf',
				'kw_suggestions'           => 'keywordsuggestions',
				'kw_length'                => 'keyword_suggestion_length',
				'kw_count'                 => 'keyword_suggestion_count',
				'kw_google_lang'           => 'keywordsuggestionslang',
				'kw_exceptions'            => 'autocompleteexceptions',
				'kw_highlight'             => 'highlight',
				'kw_highlight_whole_words' => 'highlightwholewords',
				'highlight_color'          => 'highlightcolor',
				'highlight_bg_color'       => 'highlightbgcolor',
				'redirect_click_to'        => 'click_action',
				'redirect_enter_to'        => 'return_action',
				'custom_redirect_url'      => 'redirect_url',
				'maxresults'               => 'posts_limit',

			);
			foreach ( $resolve_keys as $lkey => $pkey ) {
				if ( isset($lite[ $lkey ]) ) {
					$data[ $pkey ] = $lite[ $lkey ];
				}
			}
			// 3. Manually Resolve
			// -- Default image
			if ( !empty($lite['image_default']) && strpos($lite['image_default'], 'img/default.jpg') === false ) {
				$data['image_default'] = $lite['image_default'];
			}
			// -- Generic filters
			$data['frontend_fields']['selected']     = array();
			$data['frontend_fields']['unselected'][] = 'exact';
			if ( $lite['showexactmatches'] == 1 ) {
				$data['frontend_fields']['selected'][]      = 'exact';
				$data['frontend_fields']['labels']['exact'] = $lite['exactmatchestext'];
			}
			if ( $lite['showsearchintitle'] == 1 ) {
				$data['frontend_fields']['selected'][]      = 'title';
				$data['frontend_fields']['labels']['title'] = $lite['searchintitletext'];
			}
			if ( $lite['showsearchincontent'] == 1 ) {
				$data['frontend_fields']['selected'][]        = 'content';
				$data['frontend_fields']['labels']['content'] = $lite['searchincontenttext'];
			}
			if ( $lite['showsearchinexcerpt'] == 1 ) {
				$data['frontend_fields']['selected'][]        = 'excerpt';
				$data['frontend_fields']['labels']['excerpt'] = $lite['searchinexcerpttext'];
			}
			if ( count($data['frontend_fields']['selected']) > 0 ) {
				$data['frontend_fields']['unselected'] = array_diff(
					$data['frontend_fields']['unselected'],
					$data['frontend_fields']['selected']
				);
			}
			// -- Old version Post type filters
			if ( isset($lite['showsearchinposts']) && $lite['showsearchinposts'] == 1 ) {
				if ( $data['showcustomtypes'] == '' ) {
					$data['showcustomtypes'] = 'post;' . $lite['searchinpoststext'];
				} else {
					$data['showcustomtypes'] = 'post;' . $lite['searchinpoststext'] . '|' . $data['showcustomtypes'];
				}
			}
			if ( isset($lite['showsearchinpages']) && $lite['showsearchinpages'] == 1 ) {
				if ( $data['showcustomtypes'] == '' ) {
					$data['showcustomtypes'] = 'page;' . $lite['searchinpagestext'];
				} else {
					$data['showcustomtypes'] = 'page;' . $lite['searchinpagestext'] . '|' . $data['showcustomtypes'];
				}
			}
			// -- Category filters
			if ( $lite['showsearchincategories'] == 1 && $lite['exsearchincategories'] != '' ) {
				$categories = explode('|', $lite['exsearchincategories']);
				foreach ( $categories as $ck => $cc ) {
					if ( $cc == '' ) {
						unset($categories[ $ck ]);
					}
				}
				if ( count($categories) > 0 ) {
					// Set the display mode
					$data['show_terms']['display_mode']['category'] = array(
						'type'            => 'checkboxes',
						'select_all'      => false,
						'box_header_text' => $lite['exsearchincategoriestext'],
						'select_all_text' => '',
						'default'         => 'checked',
					);
					$categories                                     = get_terms(
						array(
							'taxonomy' => 'category',
							'include'  => $categories,
							'orderby'  => 'include',
						)
					);
					if ( !is_wp_error($categories) && count($categories) > 0 ) {
						$categories_sorted = array();
						wd_sort_terms_hierarchicaly($categories, $categories_sorted);
						wd_flatten_hierarchical_terms($categories_sorted, $categories);
						foreach ( $categories as $category ) {
							$data['show_terms']['terms'][] = array(
								'taxonomy' => 'category',
								'id'       => $category->term_id,
								'level'    => $category->level,
								'ex_ids'   => array(),
							);
						}
					}
				}
			}
			// -- Themes & layout
			$themes = array(
				'simple-red'   => 'Simple Red vertical (default)',
				'simple-blue'  => 'Simple Blue vertical',
				'simple-grey'  => 'Simple Grey vertical',
				'classic-blue' => 'Classic Blue vertical',
				'curvy-black'  => 'Curvy Black vertical',
				'curvy-red'    => 'Curvy Red vertical',
				'curvy-blue'   => 'Curvy Blue vertical',
				'underline'    => 'Underline Grey vertical',
			);
			if ( isset($themes[ $lite['theme'] ]) ) {
				$data['themes'] = $themes[ $lite['theme'] ];
				if ( $lite['theme'] != 'simple-red' ) {
					$theme = Themes::get('search', $themes[ $lite['theme'] ]);
					foreach ( $theme as $tkey => $tval ) {
						$data[ $tkey ] = $tval;
					}
				}
			}
			if ( $lite['override_bg'] == 1 ) {
				$data['boxbackground'] = $lite['override_bg_color'];
				// Simple themes, input background
				if ( strpos($lite['theme'], 'simple-') === 0 ) {
					$data['inputbackground'] = $lite['override_bg_color'];
				}
			}
			if ( $lite['override_icon'] == 1 ) {
				$data['settingsbackground']   = $lite['override_icon_bg_color'];
				$data['magnifierbackground']  = $lite['override_icon_bg_color'];
				$data['settingsimage_color']  = $lite['override_icon_color'];
				$data['magnifierimage_color'] = $lite['override_icon_color'];
			}
			if ( $lite['override_border'] == 1 ) {
				$data['boxborder'] = $lite['override_border_style'];
			}
			if ( $lite['results_bg_override'] == 1 ) {
				$data['resultsbackground'] = $lite['results_bg_override_color'];
			}
			if ( $lite['results_item_bg_override'] == 1 ) {
				$data['resultscontainerbackground'] = $lite['results_item_bg_override_color'];
			}
			if ( $lite['results_override_border'] == 1 ) {
				$data['resultsborder'] = $lite['results_override_border_style'];
			}
			if ( $lite['settings_bg_override'] == 1 ) {
				$data['settingsdropbackground'] = $lite['settings_bg_override_color'];
			}

			// -- Category based exclusions
			if ( $lite['excludecategories'] != '' ) {
				$terms = explode('|', $lite['excludecategories']);
				foreach ( $terms as $tk => $tt ) {
					if ( $tt == '' ) {
						unset($terms[ $tk ]);
					}
				}
				if ( count($terms) > 0 ) {
					foreach ( $terms as $term ) {
						$data['exclude_by_terms']['terms'][] = array(
							'taxonomy' => 'category',
							'id'       => $term,
							'level'    => 0,
							'ex_ids'   => array(),
						);
					}
				}
			}
		} else {
			return false;
		}

		// Resume the import
		if (
			$wpdb->query(
				'INSERT INTO ' . wd_asp()->db->table('main') . "
                            (name, data) VALUES
                            ('" . esc_sql($name) . "', '" . wd_mysql_escape_mimic(json_encode($data)) . "')"
			) !== false
		) {
			// Final import options, the form override
			if ( $lite['override_search_form'] == 1 ) {
				update_option('asp_st_override', $wpdb->insert_id);
			}
			if ( $lite['override_woo_search_form'] == 1 ) {
				update_option('asp_woo_override', $wpdb->insert_id);
			}
			return $wpdb->insert_id;
		}

		return false;
	}

	/**
	 * Update the search data
	 *
	 * @param $id
	 * @param $data
	 * @param $update_owner bool|int
	 * @return false|int
	 */
	public function update( $id, $data = array(), $update_owner = false ) {
		global $wpdb;

		$this->refresh = true;

		if ( isset($this->search_instances[ $id ], $this->search_instances[ $id ]['data']) ) {
			$data = array_merge($this->search_instances[ $id ]['data'], $data);
		}
		$data = $this->clearData($data);

		if ( $update_owner === true ) {
			$data['owner'] = intval( get_current_user_id() );
		} elseif ( $update_owner !== false ) {
			$update_owner = intval($update_owner);
			if ( $update_owner >= 0 ) {
				$data['owner'] = $update_owner;
			}
		}

		return $wpdb->query(
			'
            UPDATE ' . wd_asp()->db->table('main') . "
            SET data = '" . wd_mysql_escape_mimic(json_encode($data)) . "'
            WHERE id = " . intval($id) . '
        '
		);
	}

	/**
	 * Renames a search instance
	 *
	 * @param $new_name string
	 * @param $id int
	 * @return bool|int
	 */
	public function rename( $new_name, $id ) {
		global $wpdb;

		$this->refresh = true;

		return $wpdb->query(
			$wpdb->prepare('UPDATE ' . wd_asp()->db->table('main') . " SET name = '%s' WHERE id = %d", $new_name, $id)
		);
	}

	/**
	 * Resets the search instance to the default options.
	 *
	 * @param int $id Search instance ID
	 */
	public function reset( $id ) {
		global $wpdb;
		$this->refresh = true;

		$query = 'UPDATE ' . wd_asp()->db->table('main') . "
             SET
                data='" . wd_mysql_escape_mimic(json_encode(wd_asp()->options['asp_defaults'])) . "'
             WHERE id=" . intval($id);
		$wpdb->query($query);
	}

	/**
	 * Duplicates a search instance
	 *
	 * @param $id int
	 * @return bool|int
	 */
	public function duplicate( $id ) {
		global $wpdb;

		$this->refresh = true;

		return $wpdb->query(
			$wpdb->prepare(
				'
            INSERT INTO ' . wd_asp()->db->table('main') . "( name, data )
            SELECT CONCAT(name, ' duplicate'), data FROM " . wd_asp()->db->table('main') . '
            WHERE id=%d;',
				$id
			)
		);
	}

	/**
	 * Deletes a search instance
	 *
	 * @param $id int
	 * @return bool|int
	 */
	public function delete( $id ) {
		global $wpdb;

		$this->refresh = true;

		return $wpdb->query(
			$wpdb->prepare('DELETE FROM ' . wd_asp()->db->table('main') . ' WHERE id=%d', $id)
		);
	}

	/**
	 * @param int $id
	 * @return string
	 */
	public function export( $id = 0 ) {
		if ( $this->exists($id) ) {
			return base64_encode( json_encode( $this->get($id) ) );
		}
		return '';
	}

	/**
	 * @param array|string $data json encoded array of base 64 encoded instance objects (or only one base 64 encoded instance)
	 * @return int|string|WP_Error number of instances (int) or an error message from the Exception
	 */
	public function import( $data ) {
		$imported  = 0;
		$instances = array();
		if ( is_array($data) ) {
			foreach ( $data as $d ) {
				$instances[] = json_decode(base64_decode($d), true);
				if ( json_last_error() != 0 ) {
					break;
				}
			}
		} elseif ( is_string($data) ) {
			$instances[] = json_decode(base64_decode($data), true);
		} else {
			return new WP_Error('data', __('Invalid data', 'ajax-search-pro'));
		}

		if ( json_last_error() != 0 ) {
			return new WP_Error( 'json', __('Invalid JSON data', 'ajax-search-pro'));
		}

		foreach ( $instances as $instance ) {
			if ( isset($instance['name']) ) {
				$id = $this->add($instance['name'] . ' imported');
				$this->update($id, $instance['data']);
				++$imported;
			}
		}
		return $imported;
	}

	/**
	 * This method is intended to use on params AFTER parsed from the database
	 *
	 * @param $params
	 * @return mixed
	 */
	public function decode_params( $params ) {
		/**
		 * New method for future use.
		 * Detects if there is a _decode_ prefixed input for the current field.
		 * If so, then decodes and overrides the posted value.
		 */
		foreach ( $params as $k =>$v ) {
			if ( gettype($v) === 'string' && substr($v, 0, strlen('_decode_')) == '_decode_' ) {
				$real_v       = substr($v, strlen('_decode_'));
				$params[ $k ] = json_decode(base64_decode($real_v), true);
			}
		}
		return $params;
	}

	public function add_script_data( $id, array $data ) {
		$this->script_data[ $id ] = wp_json_encode($data);
		update_site_option('_asp_script_data', $this->script_data);
	}

	public function get_script_data(): array {
		/**
		 * To avoid any data corruption, the datatype should be checked explicitly
		 * https://github.com/WPDreams/ajax-search-pro-development/issues/101
		 */
		$script_data = get_option('_asp_script_data', array());
		return is_array($script_data) ? $script_data : array();
	}

	public function getInstancesPrinted(): array {
		return array_keys($this->script_data);
	}

	public function areInstancesOnCurrentPage(): bool {
		return count($this->script_data) > 0;
	}

	// ------------------------------------------------------------
	// ---------------- PRIVATE --------------------
	// ------------------------------------------------------------

	/**
	 * Clears unwanted keys from the search instance data array
	 *
	 * @param $data array
	 * @return array
	 */
	private function clearData( $data ) {
		$remove_keys = array( 'asp_options_serialized' );

		if ( is_array($data) ) {
			foreach ( $remove_keys as $key ) {
				if ( isset($data[ $key ]) ) {
					unset($data[ $key ]);
				}
			}
		}

		return $data;
	}

	/**
	 * Fetches the search instances from the DB and stores them internally for future use
	 */
	private function init() {
		global $wpdb;

		// Reset both variables, so in case of deleting no remains are left
		$this->search_instances  = array();
		$this->instances_no_data = array();

		if ( !wd_asp()->db->exists('main') ) {
			return;
		}

		$instances = $wpdb->get_results('SELECT * FROM ' . wd_asp()->db->table('main') . ' ORDER BY id ASC', ARRAY_A);

		foreach ( $instances as $k => $inst ) {
			$instance                                 = new SearchInstance(
				array(
					'name' => $inst['name'],
					'id'   => $inst['id'],
				)
			);
			$this->instances_no_data[ $instance->id ] = array(
				'name' => $instance->name,
				'id'   => $instance->id,
			);

			/**
			 * Explanation:
			 *  1. json_decode(..) -> converts the params from the database to PHP format
			 *  2. $this->decode_params(..) -> decodes params that are stored in base64 and prefixed to be decoded
			 *
			 * This is not equivalent with wd_parse_params(..) as that runs before inserting to the DB as well,
			 * and $this->decode_params(..) runs after getting the data from the database, so it stays redundant.
			 *
			 * NOTE:
			 *  raw_data is used in backwards compatibility checks, to see if a certain option exists before merging
			 *           with the defaults
			 */
			$instance->raw_data = $this->decode_params(json_decode($inst['data'], true));
			$instance->data     = array_merge(
				wd_asp()->options['asp_defaults'],
				$instance->raw_data
			);
			foreach ( $instance->data as $option_key => $value ) {
				if ( $value === null && isset(wd_asp()->options['asp_defaults'][ $option_key ]) ) {
					$instance->data[ $option_key ] = wd_asp()->options['asp_defaults'][ $option_key ];
				}
			}
			$instance->options                       = new SearchOptions(json_decode($inst['data'], true));
			$instance->data                          = apply_filters('asp_instance_options', $instance->data, $instance->id);
			$this->search_instances[ $instance->id ] = $instance;
		}
	}
}
