<?php
/** Driver for https://api-docs.igdb.com/
* @link https://demo.adminer.org/igdb/?igdb=IGDB&db=api
* username: your Client-ID
* password: your access token from https://id.twitch.tv/oauth2/token
* @link https://www.adminer.org/static/plugins/igdb.png
*/

namespace Adminer;

add_driver("igdb", "APICalypse");

if (isset($_GET["igdb"])) {
	define('Adminer\DRIVER', "igdb");

	class Db extends SqlDb {
		public $extension = "json";
		public $server_info = "v4";
		private $username;
		private $password;

		function attach($server, $username, $password): string {
			$this->username = $username;
			$this->password = $password;
			return '';
		}

		function select_db($database) {
			return ($database == "api");
		}

		function request($endpoint, $query, $method = 'POST') {
			$context = stream_context_create(array('http' => array(
				'method' => $method,
				'header' => array(
					"Content-Type: text/plain",
					"Client-ID: $this->username",
					"Authorization: Bearer $this->password",
				),
				'content' => $query,
				'ignore_errors' => true,
			)));
			list($response, $status) = get_url("https://api.igdb.com/v4/$endpoint", $context);
			$return = json_decode($response, true);
			if ($status != 200) {
				if (is_array($return)) {
					foreach (is_array($return[0]) ? $return : array($return) as $rows) {
						foreach ($rows as $key => $val) {
							$this->error .= '<b>' . h($key) . ':</b> ' . (is_url($val) ? '<a href="' . h($val) . '"' . target_blank() . '>' . h($val) . '</a>' : h($val)) . '<br>';
						}
					}
				} else {
					$this->error = htmlspecialchars(strip_tags($response), 0, null, false);
				}
				return false;
			}
			return $return;
		}

		function query($query, $unbuffered = false) {
			if (preg_match('~^SELECT COUNT\(\*\) FROM (\w+)( WHERE ((MATCH \(search\) AGAINST \((.+)\))|.+))?$~', $query, $match)) {
				return new Result(array($match[1] == 'dumps' ? array('count' => 50) : $this->request("$match[1]/count", ($match[5] ? 'search "' . addcslashes($match[5], '\\"') . '";'
					: ($match[3] ? 'where ' . str_replace(' AND ', ' & ', $match[3]) . ';'
					: ''
				)))));
			}
			if (preg_match('~^\s*(GET|POST|DELETE)\s+([\w/?=]+)\s*;\s*(.*)$~s', $query, $match)) {
				$endpoint = $match[2];
				$response = $this->request($endpoint, $match[3], $match[1]);
				if ($response === false) {
					return $response;
				}
				$return = new Result(is_array($response[0]) ? $response : array($response));
				$return->table = $endpoint;
				if ($endpoint == 'multiquery') {
					$return->results = $response;
				}
				return $return;
			}
			$this->error = "Syntax:<br>POST &lt;endpoint>; fields ...;";
			return false;
		}

		function store_result() {
			if ($this->multi && ($result = current($this->multi->results))) {
				echo "<h3>" . h($result['name']) . "</h3>\n";
				$this->multi->__construct($result['count'] ? array(array('count' => $result['count'])) : $result['result']);
			}
			return $this->multi;
		}

		function next_result(): bool {
			return $this->multi && next($this->multi->results);
		}

		function quote($string): string {
			return $string;
		}
	}

	class Result {
		public $num_rows;
		public $table;
		public $results = array();
		private $result;
		private $fields;

		function __construct($result) {
			$keys = array();
			foreach ($result as $i => $row) {
				foreach ($row as $key => $val) {
					$keys[$key] = null;
					if (is_array($val) && is_int($val[0])) {
						$result[$i][$key] = "(" . implode(",", $val) . ")";
					}
				}
			}
			foreach ($result as $i => $row) {
				$result[$i] = array_merge($keys, $row);
			}
			$this->result = $result;
			$this->num_rows = count($result);
			$this->fields = array_keys(idx($result, 0, array()));
		}

		function fetch_assoc() {
			$row = current($this->result);
			next($this->result);
			return $row;
		}

		function fetch_row() {
			$row = $this->fetch_assoc();
			return ($row ? array_values($row) : false);
		}

		function fetch_field(): \stdClass {
			$field = current($this->fields);
			next($this->fields);
			return ($field != '' ? (object) array('name' => $field, 'type' => 15, 'charsetnr' => 0, 'orgtable' => $this->table) : false);
		}
	}

	class Driver extends SqlDriver {
		static $extensions = array("json");
		static $jush = "igdb";

		public $delimiter = ";;";
		public $operators = array("=", "<", ">", "<=", ">=", "!=", "~");

		public $tables = array();
		public $links = array();
		public $fields = array();
		public $foreignKeys = array();
		public $foundRows = null;

		private static function docsFilename(): string {
			return get_temp_dir() . "/adminer-igdb-api.html";
		}

		static function connect($server, $username, $password) {
			$filename = self::docsFilename();
			if (!file_exists($filename)) {
				list($contents, $status) = get_url("https://api-docs.igdb.com/", stream_context_create(array('http' => array('ignore_errors' => true))));
				if ($status != 200 || !file_put_contents($filename, $contents)) {
					return "Download https://api-docs.igdb.com/ and save it as $filename";
				}
			}
			return parent::connect($server, $username, $password);
		}

		function __construct($connection) {
			parent::__construct($connection);
			libxml_use_internal_errors(true);
			$dom = new \DOMDocument();
			$dom->loadHTMLFile(self::docsFilename());
			$xpath = new \DOMXPath($dom);
			$els = $xpath->query('//div[@class="content"]/*');
			$link = '';
			foreach ($els as $i => $el) {
				if ($el->tagName == 'h2') {
					$link = $el->getAttribute('id');
				}
				if ($el->nodeValue == 'Request Path') {
					$table = preg_replace('~^https://api.igdb.com/v4/~', '', $els[$i+1]->firstElementChild->nodeValue);
					$comment = $els[$i-1]->tagName == 'p' ? $els[$i-1]->nodeValue : '';
					if (preg_match('~^DEPRECATED!~', $comment)) {
						continue;
					}
					$this->fields[$table]['id'] = array('full_type' => 'bigint', 'comment' => '');
					$this->links[$link] = $table;
					$this->tables[$table] = array('Name' => $table, 'Engine' => 'endpoint', 'Comment' => $comment);
					foreach ($xpath->query('tbody/tr', $els[$i+2]) as $tr) {
						$tds = $xpath->query('td', $tr);
						$field = $tds[0]->nodeValue;
						$comment = $tds[2]->nodeValue;
						if ($field != 'checksum' && $field != 'content_descriptions' && !preg_match('~^DEPRECATED!~', $comment)) {
							$this->fields[$table][$field] = array(
								'full_type' => str_replace('  ', ' ', $tds[1]->nodeValue),
								'comment' => str_replace('  ', ' ', $comment),
							);
							$ref = $xpath->query('a/@href', $tds[1]);
							if (count($ref) && !in_array($ref[0]->value, array('#game-version-feature-enums', '#tag-numbers'))) {
								$this->foreignKeys[$table][$field] = substr($ref[0]->value, 1);
							} elseif ($field === 'game_id') { // game_time_to_beats, popularity_primitives
								$this->foreignKeys[$table][$field] = 'game';
							}
						}
					}
					uksort($this->fields[$table], function ($a, $b) use ($table) {
						return (($b == 'id') - ($a == 'id'))
							?: (($b == 'name') - ($a == 'name'))
							?: (($a == 'updated_at') - ($b == 'updated_at'))
							?: (($a == 'created_at') - ($b == 'created_at'))
							?: (!idx($this->foreignKeys[$table], $b) - !idx($this->foreignKeys[$table], $a))
							?: ($a < $b ? -1 : 1)
						;
					});
				}
			}

			$this->tables['dumps'] = array(
				'Name' => 'dumps',
				'Engine' => 'dumps',
				'Comment' => 'Daily updated CSV Data Dumps which can be used to kick start your projects or keep your data up to date (within 24 hours)',
			);
			$this->links['data-dumps'] = 'dumps';
			$this->fields['dumps'] = array(
				's3_url' => array('comment' => 'The download Url is a presigned S3 url that is valid for 5 minutes'),
				'endpoint' => array(),
				'file_name' => array(),
				'size_bytes' => array(),
				'updated_at' => array('full_type' => 'datetime'),
				'schema_version' => array('full_type' => 'datetime', 'comment' => 'Will change when the schema changes'),
				'schema' => array('comment' => 'Reflects the current data structure and data type that the Dump is using'),
			);

			$this->tables['webhooks'] = array(
				'Name' => 'webhooks',
				'Engine' => 'webhooks',
				'Comment' => 'Webhooks allow us to push data to you when it is added, updated, or deleted',
			);
			$this->links['webhooks'] = 'webhooks';
			$this->fields['webhooks'] = array(
				'endpoint' => array(
					'full_type' => 'String',
					'comment' => 'Specify what type of data you want from your webhook',
					'privileges' => array('insert' => 1),
				),
				'id' => array('comment' => 'A unique ID for the webhook'),
				'url' => array(
					'full_type' => 'String',
					'length' => '100',
					'comment' => 'Your prepared url that is ready to accept data from us',
					'privileges' => array('select' => 1, 'insert' => 1),
				),
				'method' => array(
					'full_type' => 'enum',
					'length' => "('create','delete','update')",
					'comment' => 'The type of data you are expecting to your url, there are three types of methods',
					'privileges' => array('insert' => 1),
				),
				'category' => array('comment' => 'Based on the endpoint you chose'),
				'sub_category' => array('comment' => 'Based on your method (can be 0, 1, 2)'),
				'active' => array('comment' => 'Is the webhook currently active'),
				'api_key' => array('comment' => 'Displays the api key the webhook is connected to'),
				'secret' => array(
					'full_type' => 'String',
					'comment' => 'Your “secret” password for your webhook',
					'privileges' => array('select' => 1, 'insert' => 1),
				),
				'created_at' => array('comment' => 'Created at date'),
				'updated_at' => array('comment' => 'Updated at date'),
			);
		}

		function select($table, $select, $where, $group, $order = array(), $limit = 1, $page = 0, $print = false) {
			$query = '';
			$search = preg_match('~^MATCH \(search\) AGAINST \((.+)\)$~', $where[0], $match);
			if ($search) {
				$query = 'search "' . addcslashes($match[1], '\\"') . "\";\n";
				unset($where[0]);
			}
			foreach ($where as $i => $val) {
				$where[$i] = str_replace(' OR ', ' | ', $val);
			}
			$columns = ($select != array('*') ? $select : array_keys($this->fields[$table]));
			$common = ($where ? "\nwhere " . implode(" & ", $where) . ";" : "");
			$method = ($table == 'webhooks' || $table == 'dumps' ? 'GET' : 'POST');
			if ($method == 'POST') {
				$query .= "fields " . implode(",", $select) . ";"
					. ($select == array('*') ? "\nexclude checksum;" : "")
					. $common
					. ($order ? "\nsort " . strtolower(implode(",", $order)) . ";" : "")
					. "\nlimit $limit;"
					. ($page ? "\noffset " . ($page * $limit) . ";" : "")
				;
			}
			$start = microtime(true);
			$multi = (!$search && $method == 'POST' && array_key_exists($table, driver()->tables));
			$realQuery = str_replace("*;\nexclude checksum", implode(',', $columns), $query); // exclude deprecated columns
			$return = ($multi
				? $this->conn->request('multiquery', "query $table \"result\" { $realQuery };\nquery $table/count \"count\" { $common };")
				: $this->conn->request(($method == 'GET' && $where ? "$table/" . reset($_GET["where"]) : $table), $realQuery, $method)
			);
			if ($print) {
				echo adminer()->selectQuery("$method $table;\n$query", $start);
			}
			if ($return === false) {
				return $return;
			}
			$this->foundRows = ($multi ? $return[1]['count'] : null);
			$return = ($multi ? $return[0]['result'] : $return);
			if ($table == 'dumps' && $where) {
				$return = array($return);
			} elseif ($return && $method == 'POST') {
				$return[0] = array_merge(array_fill_keys($columns, null), $return[0]);
			}
			return new Result($return);
		}

		function insert($table, $set) {
			$content = array();
			foreach ($set as $key => $val) {
				if ($key != 'endpoint') {
					$content[] = urlencode($key) . '=' . urlencode($val);
				}
			}
			return queries("POST $set[endpoint]/$table; " . implode('&', $content));
		}

		function delete($table, $queryWhere, $limit = 0) {
			preg_match_all('~\bid = (\d+)~', $queryWhere, $matches);
			$this->conn->affected_rows = 0;
			foreach ($matches[1] as $id) {
				$result = queries("DELETE $table/$id;");
				if (!$result) {
					return false;
				}
				$row = $result->fetch_row();
				if (!$row[0]) {
					$this->conn->error = "ID $id not found.";
					return false;
				}
				$this->conn->affected_rows++;
			}
			return true;
		}

		function value($val, $field): ?string {
			return ($val && in_array($field['full_type'], array('Unix Time Stamp', 'datetime')) ? str_replace(' 00:00:00', '', gmdate('Y-m-d H:i:s', $val)) : $val);
		}

		function tableHelp($name, $is_view = false) {
			return strtolower("https://api-docs.igdb.com/#" . array_search($name, $this->links));
		}
	}

	function logged_user() {
		return $_GET["username"];
	}

	function get_databases($flush) {
		return array("api");
	}

	function collations() {
		return array();
	}

	function db_collation($db, $collations) {
	}

	function information_schema($db) {
	}

	function indexes($table, $connection2 = null) {
		$return = array(array('type' => 'PRIMARY', 'columns' => array($table == 'dumps' ? 'endpoint' : 'id')));
		if (in_array($table, array('characters', 'collections', 'games', 'platforms', 'themes'))) { // https://api-docs.igdb.com/#search-1
			$return[] = array("type" => "FULLTEXT", "columns" => array("search"));
		}
		return $return;
	}

	function fields($table) {
		$return = array();
		foreach (driver()->fields[$table] ?: array() as $key => $val) {
			$type = strtolower(preg_replace('~ .*~', '', $val['full_type']));
			$return[$key] = $val + array(
				"field" => $key,
				"type" => ($type == 'reference' ? 'int' : $type), // align right reference columns
				"privileges" => array("select" => 1) + ($table == 'webhooks' || $table == 'dumps' ? array() : array("where" => 1, "order" => 1)),
			);
		}
		return $return;
	}

	function convert_field($field) {
	}

	function unconvert_field($field, $return) {
		return $return;
	}

	function limit($query, $where, $limit, $offset = 0, $separator = " ") {
		return $query;
	}

	function idf_escape($idf) {
		return $idf;
	}

	function table($idf) {
		return idf_escape($idf);
	}

	function foreign_keys($table) {
		$return = array();
		foreach (driver()->foreignKeys[$table] ?: array() as $key => $val) {
			$return[] = array(
				'table' => driver()->links[$val],
				'source' => array($key),
				'target' => array('id'),
			);
		}
		return $return;
	}

	function tables_list() {
		return array_fill_keys(array_keys(table_status()), 'table');
	}

	function table_status($name = "", $fast = false) {
		$tables = driver()->tables;
		return ($name != '' ? ($tables[$name] ? array($name => $tables[$name]) : array()) : $tables);
	}

	function count_tables($databases) {
		return array(reset($databases) => count(tables_list()));
	}

	function error() {
		return connection()->error;
	}

	function is_view($table_status) {
		return false;
	}

	function found_rows($table_status, $where) {
		return driver()->foundRows;
	}

	function fk_support($table_status) {
		return true;
	}

	function last_id($result): string {
		$row = $result->fetch_assoc();
		return (string) $row['id'];
	}

	function support($feature) {
		return in_array($feature, array('columns', 'comment', 'sql', 'table'));
	}
}
