<?php

namespace XF\Db;

use function is_array, is_int;

abstract class AbstractStatement
{
	/**
	 * @var AbstractAdapter
	 */
	protected $adapter;

	protected $query;

	protected $params;

	/**
	 * @var array
	 */
	protected $metaFields;

	protected $keys = [];

	protected $values = [];

	abstract public function prepare();

	/**
	 * @return bool
	 * @throws Exception
	 */
	abstract public function execute();
	abstract public function fetchRowValues();
	abstract public function rowsAffected();
	abstract public function reset();
	abstract protected function closeStatement();

	public function __construct(AbstractAdapter $adapter, $query, $params = [])
	{
		$this->adapter = $adapter;
		$this->query = $query;
		$this->params = is_array($params) ? $params : [$params];
	}

	public function query()
	{
		return $this->query;
	}

	public function params(?array $params = null)
	{
		if ($params === null)
		{
			return $this->params;
		}

		$this->params = $params;

		return $this;
	}

	public function fetch()
	{
		$values = $this->fetchRowValues();
		if (!$values)
		{
			return false;
		}

		return array_combine($this->keys, $values);
	}

	public function fetchColumn($key = 0)
	{
		$values = $this->fetchRowValues();
		if (!$values)
		{
			return false;
		}

		if (is_int($key))
		{
			return $values[$key] ?? null;
		}
		else
		{
			$values = array_combine($this->keys, $values);
			return $values[$key] ?? null;
		}
	}

	public function fetchAliasGrouped()
	{
		$values = $this->fetchRowValues();
		if (!$values)
		{
			return false;
		}

		$output = [];
		foreach ($this->metaFields AS $k => $field)
		{
			$table = $field->table ?: '__extra';

			$output[$table][$field->name] = $values[$k];
		}

		return $output;
	}

	public function fetchAll()
	{
		$output = [];
		while ($v = $this->fetch())
		{
			$output[] = $v;
		}

		return $output;
	}

	public function fetchAllNum()
	{
		$output = [];
		while ($v = $this->fetchRowValues())
		{
			$output[] = $v;
		}

		return $output;
	}

	public function fetchAllKeyed($key, $fallbackPrefix = '_default')
	{
		$output = [];
		$i = 0;
		while ($v = $this->fetch())
		{
			if (isset($v[$key]))
			{
				$output[$v[$key]] = $v;
			}
			else
			{
				$fallbackKey = $fallbackPrefix . $i;
				$i++;
				$output[$fallbackKey] = $v;

			}
		}

		return $output;
	}

	public function fetchAllColumn($key = 0)
	{
		$output = [];

		while (($v = $this->fetchColumn($key)) !== false)
		{
			$output[] = $v;
		}

		return $output;
	}

	public function fetchPairs()
	{
		$output = [];
		while ($v = $this->fetchRowValues())
		{
			$output[$v[0]] = $v[1];
		}

		return $output;
	}

	public function close()
	{
		$this->closeStatement();

		$this->metaFields = null;
		$this->keys = [];
		$this->values = [];
	}

	/**
	 * @param string $message
	 * @param int $code
	 * @param null|string $sqlStateCode
	 *
	 * @return Exception
	 */
	protected function getException($message, $code = 0, $sqlStateCode = null)
	{
		$class = null;

		if ($sqlStateCode)
		{
			switch ($sqlStateCode)
			{
				case '23000':
					if (preg_match('/(^|\s)duplicate($|\s)/i', $message))
					{
						$class = DuplicateKeyException::class;
					}
					break;

				case '40001':
					$class = DeadlockException::class;
					break;

				case '42000':
					$class = InvalidQueryException::class;
					break;
			}
		}

		if (!$class)
		{
			$class = Exception::class;
		}

		$e = new $class($message, $code);
		$e->sqlStateCode = $sqlStateCode;
		$e->query = $this->query;
		$e->statement = $this;

		return $e;
	}
}
