<?php
declare(strict_types=1);

/**
 * Contains the description of a user query
 *
 * It allows to extract the meaningful bits of the query to be manipulated in an
 * easy way.
 */
class FreshRSS_UserQuery {
	private bool $deprecated = false;
	private string $get = '';
	private string $get_name = '';
	private string $get_type = '';
	/** XML-encoded name */
	private string $name = '';
	private string $order = '';
	private readonly FreshRSS_BooleanSearch $search;
	private int $state = 0;
	private string $url = '';
	private string $token = '';
	private bool $shareRss = false;
	private bool $shareOpml = false;
	private bool $includeUserLabels = false;
	private bool $excludeArticleTags = false;
	private string $userLabelPrefix = '';
	/** @var array<int,FreshRSS_Category> $categories where the key is the category ID */
	private array $categories;
	/** @var array<int,FreshRSS_Tag> $labels where the key is the label ID */
	private array $labels;
	/** XML-encoded description */
	private string $description = '';
	private string $imageUrl = '';

	public static function generateToken(string $salt): string {
		if (!FreshRSS_Context::hasSystemConf()) {
			return '';
		}
		$hash = md5(FreshRSS_Context::systemConf()->salt . $salt . random_bytes(16));
		if (function_exists('gmp_init')) {
			// Shorten the hash if possible by converting from base 16 to base 62
			$hash = gmp_strval(gmp_init($hash, 16), 62);
		}
		return $hash;
	}

	/**
	 * @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,
	 * 	shareRss?:bool,shareOpml?:bool,includeUserLabels?:bool,excludeArticleTags?:bool,userLabelPrefix?:string,
	 * 	publishLabelsInsteadOfTags?:bool,description?:string,imageUrl?:string} $query
	 * @param array<FreshRSS_Category> $categories
	 * @param array<FreshRSS_Tag> $labels
	 */
	public function __construct(array $query, array $categories, array $labels) {
		$this->categories = [];
		foreach ($categories as $category) {
			$this->categories[$category->id()] = $category;
		}
		$this->labels = [];
		foreach ($labels as $label) {
			$this->labels[$label->id()] = $label;
		}
		if (isset($query['get'])) {
			$this->parseGet($query['get']);
		} else {
			$this->get_type = 'all';
		}
		if (isset($query['name'])) {
			$this->name = trim($query['name']);
		}
		if (isset($query['order'])) {
			$this->order = $query['order'];
		}
		if (empty($query['url'])) {
			if (!empty($query)) {
				$link = $query;
				unset($link['description']);
				unset($link['imageUrl']);
				unset($link['name']);
				unset($link['shareOpml']);
				unset($link['shareRss']);
				unset($link['includeUserLabels']);
				unset($link['excludeArticleTags']);
				unset($link['userLabelPrefix']);
				unset($link['publishLabelsInsteadOfTags']);
				$this->url = Minz_Url::display(['params' => $link]);
			}
		} else {
			$this->url = $query['url'];
		}
		if (!isset($query['search'])) {
			$query['search'] = '';
		}
		if (!empty($query['token'])) {
			$this->token = $query['token'];
		}
		if (isset($query['shareRss'])) {
			$this->shareRss = $query['shareRss'];
		}
		if (isset($query['shareOpml'])) {
			$this->shareOpml = $query['shareOpml'];
		}

		if (isset($query['publishLabelsInsteadOfTags'])) {	// Legacy
			$this->includeUserLabels = (bool)$query['publishLabelsInsteadOfTags'];
			$this->excludeArticleTags = (bool)$query['publishLabelsInsteadOfTags'];
			if ($this->includeUserLabels) {
				$this->userLabelPrefix = '';
			}
		}
		if (isset($query['includeUserLabels'])) {
			$this->includeUserLabels = (bool)$query['includeUserLabels'];
		}
		if (isset($query['excludeArticleTags'])) {
			$this->excludeArticleTags = (bool)$query['excludeArticleTags'];
		}
		if (isset($query['userLabelPrefix'])) {
			$this->userLabelPrefix = (string)$query['userLabelPrefix'];
		}

		if (isset($query['description'])) {
			$this->description = $query['description'];
		}
		if (isset($query['imageUrl'])) {
			$this->imageUrl = $query['imageUrl'];
		}

		// linked too deeply with the search object, need to use dependency injection
		$this->search = new FreshRSS_BooleanSearch($query['search'], 0, 'AND', allowUserQueries: true);
		if (!empty($query['state'])) {
			$this->state = intval($query['state']);
		}
	}

	/**
	 * Convert the current object to an array.
	 *
	 * @return array{get?:string,name?:string,order?:string,search?:string,
	 * 	state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool,
	 * 	includeUserLabels?:bool,excludeArticleTags?:bool,userLabelPrefix?:string,description?:string,imageUrl?:string}
	 */
	public function toArray(): array {
		return array_filter([
			'get' => $this->get,
			'name' => $this->name,
			'order' => $this->order,
			'search' => $this->search->toString(expandUserQueries: false),
			'state' => $this->state,
			'url' => $this->url,
			'token' => $this->token,
			'shareRss' => $this->shareRss,
			'shareOpml' => $this->shareOpml,
			'includeUserLabels' => $this->includeUserLabels,
			'excludeArticleTags' => $this->excludeArticleTags,
			'userLabelPrefix' => $this->userLabelPrefix,
			'description' => $this->description,
			'imageUrl' => $this->imageUrl,
		], fn($v): bool => $v !== '' && $v !== 0 && $v !== false);
	}

	/**
	 * Parse the get parameter in the query string to extract its name and type
	 */
	private function parseGet(string $get): void {
		$this->get = $get;
		if ($this->get === '') {
			$this->get_type = 'all';
		} elseif (preg_match('/(?P<type>[aAcfistTZ])(_(?P<id>\d+))?/', $get, $matches)) {
			$id = intval($matches['id'] ?? '0');
			switch ($matches['type']) {
				case 'a':	// All PRIORITY_MAIN_STREAM
					$this->get_type = 'all';
					break;
				case 'A':	// All except PRIORITY_HIDDEN
					$this->get_type = 'A';
					break;
				case 'Z':	// All including PRIORITY_HIDDEN
					$this->get_type = 'Z';
					break;
				case 'c':	// Category
					$this->get_type = 'category';
					$c = $this->categories[$id] ?? null;
					$this->get_name = $c === null ? '' : $c->name();
					break;
				case 'f':	// Feed
					$this->get_type = 'feed';
					$f = FreshRSS_Category::findFeed($this->categories, $id);
					$this->get_name = $f === null ? '' : $f->name();
					break;
				case 'i':	// Priority important feeds
					$this->get_type = 'important';
					break;
				case 's':	// Starred. Deprecated: use $state instead
					$this->get_type = 'favorite';
					break;
				case 't':	// Tag (label)
					$this->get_type = 'label';
					$l = $this->labels[$id] ?? null;
					$this->get_name = $l === null ? '' : $l->name();
					break;
				case 'T':	// Any tag (label)
					$this->get_type = 'all_labels';
					break;
			}
			if ($this->get_name === '' && in_array($matches['type'], ['c', 'f', 't'], true)) {
				$this->deprecated = true;
			}
		}
	}

	/**
	 * Check if the current user query is deprecated.
	 * It is deprecated if the category or the feed used in the query are
	 * not existing.
	 */
	public function isDeprecated(): bool {
		return $this->deprecated;
	}

	/**
	 * Check if the user query has parameters.
	 */
	public function hasParameters(): bool {
		if ($this->get_type !== 'all') {
			return true;
		}
		if ($this->hasSearch()) {
			return true;
		}
		if (!in_array($this->state, [
				0,
				FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ,
				FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ | FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE
			], true)) {
			return true;
		}
		if ($this->order !== '' && $this->order !== FreshRSS_Context::userConf()->sort_order) {
			return true;
		}
		return false;
	}

	/**
	 * Check if there is a search in the search object
	 */
	public function hasSearch(): bool {
		return $this->search->toString() !== '';
	}

	public function getGet(): string {
		return $this->get;
	}

	public function getGetName(): string {
		return $this->get_name;
	}

	public function getGetType(): string {
		return $this->get_type;
	}

	public function getName(): string {
		return $this->name;
	}

	public function getOrder(): string {
		return $this->order ?: FreshRSS_Context::userConf()->sort_order;
	}

	public function getSearch(): FreshRSS_BooleanSearch {
		return $this->search;
	}

	public function getState(): int {
		$state = $this->state;
		if (!($state & FreshRSS_Entry::STATE_READ) && !($state & FreshRSS_Entry::STATE_NOT_READ)) {
			$state |= FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ;
		}
		if (!($state & FreshRSS_Entry::STATE_FAVORITE) && !($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
			$state |= FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE;
		}
		return $state;
	}

	public function getUrl(): string {
		return $this->url;
	}

	public function getToken(): string {
		return $this->token;
	}

	public function setToken(string $token): void {
		$this->token = $token;
	}

	public function setShareRss(bool $shareRss): void {
		$this->shareRss = $shareRss;
	}

	public function shareRss(): bool {
		return $this->shareRss;
	}

	public function setShareOpml(bool $shareOpml): void {
		$this->shareOpml = $shareOpml;
	}

	public function shareOpml(): bool {
		return $this->shareOpml;
	}

	public function setIncludeUserLabels(bool $includeUserLabels): void {
		$this->includeUserLabels = $includeUserLabels;
	}

	public function includeUserLabels(): bool {
		return $this->includeUserLabels;
	}

	public function setExcludeArticleTags(bool $excludeArticleTags): void {
		$this->excludeArticleTags = $excludeArticleTags;
	}

	public function excludeArticleTags(): bool {
		return $this->excludeArticleTags;
	}

	public function setUserLabelPrefix(string $userLabelPrefix): void {
		$this->userLabelPrefix = $userLabelPrefix;
	}

	public function userLabelPrefix(): string {
		return $this->userLabelPrefix;
	}

	protected function sharedUrl(bool $xmlEscaped = true): string {
		$currentUser = Minz_User::name() ?? '';
		return Minz_Url::display("/api/query.php?user={$currentUser}&t={$this->token}", $xmlEscaped ? 'html' : '', true);
	}

	public function sharedUrlRss(bool $xmlEscaped = true): string {
		if ($this->shareRss && $this->token !== '') {
			return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=rss';
		}
		return '';
	}

	public function sharedUrlGreader(bool $xmlEscaped = true): string {
		if ($this->shareOpml && $this->token !== '') {
			return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=greader';
		}
		return '';
	}

	public function sharedUrlHtml(bool $xmlEscaped = true): string {
		if ($this->shareRss && $this->token !== '') {
			return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=html';
		}
		return '';
	}

	/**
	 * OPML is only safe for some query types, otherwise it risks leaking unwanted feed information.
	 */
	public function safeForOpml(): bool {
		return in_array($this->get_type, ['all', 'category', 'feed'], true);
	}

	public function sharedUrlOpml(bool $xmlEscaped = true): string {
		if ($this->shareOpml && $this->token !== '' && $this->safeForOpml()) {
			return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=opml';
		}
		return '';
	}

	public function getDescription(): string {
		return $this->description;
	}

	public function setDescription(string $description): void {
		$this->description = $description;
	}

	public function getImageUrl(): string {
		return $this->imageUrl;
	}

	public function setImageUrl(string $imageUrl): void {
		$this->imageUrl = $imageUrl;
	}

	/**
	 * Remove queries where $get is appearing.
	 * @param string $get the get attribute which should be removed.
	 * @param array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,
	 * 	shareRss?:bool,shareOpml?:bool,description?:string,imageUrl?:string}> $queries an array of queries.
	 * @return array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,
	 * 	shareRss?:bool,shareOpml?:bool,description?:string,imageUrl?:string}> without queries where $get is appearing.
	 */
	public static function remove_query_by_get(string $get, array $queries): array {
		$final_queries = [];
		foreach ($queries as $query) {
			if (empty($query['get']) || $query['get'] !== $get) {
				$final_queries[] = $query;
			}
		}
		return $final_queries;
	}
}
