<?php

/**
 * Basic object to load a mysql table inside an object.
 */
abstract class Table
{
	public const TYPE_STRING = 1;
	public const TYPE_INTEGER = 2;
	public const TYPE_FLOAT = 3;
	public const TYPE_DATETIME = 4;
	public const TYPE_BOOL = 5;

	protected const VALUE = 'value';
	protected const TYPE = 'type';

	protected ?DatabaseInterface $database;
	protected string $tableName;
	protected array $fields;
	protected string $primaryKey;
	protected bool $isPrimKeyManual = false;

	public function __construct(string $tableName, $id, ?DatabaseInterface & $database)
	{
		$this->tableName = $tableName;
		$this->fields = [];

		$this->database = $database;

		$this->database->Query(sprintf('DESCRIBE %s', $tableName));

		$result = $this->database->getResult();

		foreach ($result as $field) {
			$sqlType = substr_count(
				$field['Type'], '(') === 0 ? $field['Type'] : strstr($field['Type'],
				'(',
				true
			);

			switch ($sqlType) {
				case 'varchar':
				case 'char':
				case 'text':
				case 'longtext':
				case 'mediumtext':
				case 'tinytext':
					$type = self::TYPE_STRING;
					break;
				case 'int':
				case 'smallint':
				case 'mediumint':
				case 'bigint':
					$type = self::TYPE_INTEGER;
					break;
				case 'float':
				case 'decimal':
				case 'double':
				case 'real':
					$type = self::TYPE_FLOAT;
					break;
				case 'datetime':
				case 'date':
					$type = self::TYPE_DATETIME;
					break;
				case 'tinyint':
					$type = self::TYPE_BOOL;
					break;
				default:
					throw new DatabaseException(
						sprintf('Type %s of field %s couldn\'t be handled', $sqlType, $field['Field']),
						ServerStatus::INTERNAL_ERROR
					);
			}

			$this->addField($field['Field'], $type);

			if ($field['Key'] === 'PRI') {
				$this->primaryKey = $field['Field'];
			}
		}

		if (!$this->isPrimKeyManual && $id !== null) {
			$this->loadById($id);
		}
	}

	public function getPrimaryKey()
	{
		if ($this->primaryKey === null) {
			return null;
		}

		return $this->getField($this->primaryKey);
	}

	protected function addField(string $name, int $type): void
	{
		if (!self::IsValidType($type)) {
			throw new DatabaseException(
				sprintf('Field %s has invalid type of %s!', $name, $type),
				ServerStatus::INTERNAL_ERROR
			);
		}

		$this->fields[$name] = [self::VALUE => null, self::TYPE => $type];
	}

	protected function loadById($id): void
	{
		$this->database->Query(
			sprintf('SELECT * FROM %s WHERE %s = :id', $this->tableName, $this->primaryKey),
			['id' => $id]
		);

		$result = $this->database->getResult();

		if (count($result) === 0) {
			throw new DatabaseException(
				sprintf('No %s with id %d found!', $this->tableName, $id),
				ServerStatus::BAD_REQUEST
			);
		}

		foreach ($result[0] as $field => $value) {
			$this->setField($field, $value);
		}
	}

	public function Flush(): void
	{
		$this->database->Delete($this->tableName, []);
	}

	public function Delete(): void
	{
		$this->database->Delete($this->tableName, [$this->primaryKey => $this->getPrimaryKey()]);

		foreach ($this->GetAllFieldNames() as $field) {
			$this->fields[$field][self::VALUE] = null;
		}
	}

	protected function getField(string $name)
	{
		if (!array_key_exists($name, $this->fields)) {
			return null;
		}

		return $this->fields[$name][self::VALUE];
	}

	/**
	 * Sets the value for the given field inside the database.
	 */
	protected function setField(string $name, $value): void
	{
		if (!$this->HasField($name)) {
			throw new DatabaseException(
				sprintf('Field %s doesn\'t exist!', $name),
				ServerStatus::INTERNAL_ERROR
			);
		}

		if ($value === null) {
			$this->fields[$name][self::VALUE] = null;
			return;
		}

		switch ($this->fields[$name][self::TYPE]) {
			case self::TYPE_STRING:
				$this->fields[$name][self::VALUE] = (string)$value;
				return;
			case self::TYPE_INTEGER:
				$this->fields[$name][self::VALUE] = (int)$value;
				return;
			case self::TYPE_FLOAT:
				$this->fields[$name][self::VALUE] = (float)$value;
				return;
			case self::TYPE_DATETIME:
				try {
					$this->fields[$name][self::VALUE] = new DateTime((string)$value);
				} catch (Throwable $e) {
					throw new DatabaseException(
						$e->getMessage(),
						ServerStatus::INTERNAL_ERROR
					);
				}
				return;
			case self::TYPE_BOOL:
				$this->fields[$name][self::VALUE] = (bool)$value;
		}
	}

	/**
	 * Checks if the table has the given column.
	 */
	public function HasField(string $name): bool
	{
		return array_key_exists($name, $this->fields);
	}

	/**
	 * Saves the whole object into the database.
	 */
	public function Save(): void
	{
		$fields = [];

		foreach ($this->GetAllFieldNames() as $fieldName) {
			$field = $this->getField($fieldName);

			if ($field instanceof DateTime) {
				$fields[$fieldName] = $field->format('Y-m-d H:i:s');
			} else if (is_bool($field)) {
				$fields[$fieldName] = (int)$field;
			} else {
				$fields[$fieldName] = $field;
			}
		}

		if ($this->isPrimKeyManual) {
			$this->saveWithManualId($fields);
		} else {
			$this->saveWithPrimaryKey($fields);
		}
	}

	/**
	 * @return string[]
	 */
	public function GetAllFieldNames(): array
	{
		$fieldNames = [];

		foreach ($this->fields as $name => $field) {
			$fieldNames[] = $name;
		}

		return $fieldNames;
	}

	/**
	 * Checks if the index is a valid backend type.
	 */
	public static function IsValidType(int $type): bool
	{
		$validTypes = [
			self::TYPE_STRING,
			self::TYPE_INTEGER,
			self::TYPE_FLOAT,
			self::TYPE_DATETIME,
			self::TYPE_BOOL,
		];

		return in_array($type, $validTypes);
	}

	protected function saveWithManualId(array $fields): void
	{
		if ($this->getField($this->primaryKey) === null) {
			throw new DatabaseException(
				'Manual primary key must not be null!',
				ServerStatus::INTERNAL_ERROR
			);
		}

		$hasKey = (bool)$this->database->Count(
			$this->tableName,
			[$this->primaryKey => $this->getField($this->primaryKey)]
		);

		if ($hasKey) {
			$this->database->Update(
				$this->tableName, $fields, [$this->primaryKey => $this->getField($this->primaryKey)]
			);
		} else {
			$this->database->Insert($this->tableName, $fields);
		}
	}

	protected function saveWithPrimaryKey(array $fields): void
	{
		if ($this->getField($this->primaryKey) !== null) {
			$this->database->Update(
				$this->tableName, $fields, [$this->primaryKey => $this->getField($this->primaryKey)]
			);
		} else {
			$this->setField($this->primaryKey, $this->database->Insert($this->tableName, $fields));
		}
	}
}