<?php

namespace Models;

/*
	Characters are the living, breathing entities that interact with the game world. They are inextricably linked to their
	accounts, and are the primary means by which the character interacts with the game world. Separating the character from
	the account allows for multiple characters to be associated with a single account, and to prevent concurrency issues
	when performing auth checks on the database.

	When creating a character, we want to init all of the related data tables; wallets, inventory, bank, etc.

	When retrieving a character, we will get the tables as-needed, to prevent allocating more memory than we need.
*/

class Character
{
	public int $id;
	public int $user_id;
	public string $name;
	public int $title_id;
	public int $level;
	public int $xp;
	public int $xp_to_level;
	public int $hp; // Health
	public int $m_hp;
	public int $mp; // Mana
	public int $m_mp;
	public int $tp; // Travel
	public int $m_tp;
	public int $pow; // Power
	public int $acc; // Accuracy
	public int $pen; // Penetration
	public int $foc; // Focus
	public int $tou; // Toughness
	public int $arm; // Armor
	public int $res; // Resist
	public int $pre; // Precision
	public int $fer; // Ferocity
	public int $luck; // Luck

	/**
	 * Max number of slots this Character has in their inventory.
	 */
	public int $inv_slots;

	/**
	 * Number of points available to spend on attributes.
	 */
	public int $att_points;
	public string $bio;

	/**
	 * The owning User of this Character. Is populated the first time user() is called.
	 */
	private User $user;

	/**
	 * The current title of the Character.
	 */
	private array $title;

	public function __construct(array $data)
	{
		foreach ($data as $k => $v) {
			if (property_exists($this, $k)) $this->$k = $v;
		}
	}

	/**
	 * Find a Character by their name or ID.
	 */
	public static function find(int|string $id): Character|false
	{
		$q = live_db()->query(
			"SELECT * FROM characters WHERE id = :id OR name = :id COLLATE NOCASE",
			[':id' => $id]
		);
		if ($q === false) throw new \Exception('Failed to query character. (C::f)'); // badly formed query
		return ($c = $q->fetchArray(SQLITE3_ASSOC)) === false ? false : new Character($c);
	}

	/**
	 * Create a Character in the database using a User ID and a name. Pass optional overrides to adjust the
	 * defaults for other fields.
	 */
	public static function create(int $user_id, string $name, array $overrides = []): Character|false
	{
		// Prep the data and merge in any overrides
		$data = ['user_id' => $user_id, 'name' => $name];
		if (env('debug')) {
			$overrides['bio'] = "Hello! [i]My[/i] name is [b]{$name}[/b].";
		}
		if (!empty($overrides)) $data = array_merge($data, $overrides);

		// Prep the fields for the query
		$k = array_keys($data);
		$f = implode(', ', $k);
		$v = implode(', ', array_map(fn($x) => ":$x", $k));

		// Create the character!
		if (live_db()->query("INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
			// @TODO: Log this error
			throw new \Exception('Failed to create character. (cc)');
		}

		// Get the character ID
		return Character::find(live_db()->lastInsertRowID());
	}

	/**
	 * Create an associated location row for this character.
	 */
	public function create_location(int $x = 0, int $y = 0, int $currently = 0): bool
	{
		$l = live_db()->query(
			"INSERT INTO char_locations (char_id, x, y, currently) VALUES (:i, :x, :y, :c)",
			[':i' => $this->id, ':x' => $x, ':y' => $y, ':c' => $currently]
		);
		return $l !== false;
	}

	/**
	 * Create an associated gear row for this character.
	 */
	public function create_gear(array $initialGear = []): bool
	{
		// @TODO implement initial gear
		$g = live_db()->query("INSERT INTO char_gear (char_id) VALUES (:i)", [':i' => $this->id]);
		return $g !== false;
	}

	/**
	 * Return whether a given character name exists already.
	 */
	public static function name_exists(string $name): bool
	{
		return live_db()->exists('characters', 'name', $name);
	}

	/**
	 * Return whether a character exists with the given ID.
	 */
	public static function exists(int $id): bool
	{
		return live_db()->exists('characters', 'id', $id);
	}

	/**
	 * Return whether a character belongs to the given user ID.
	 */
	public static function belongs_to(int $id, int $user_id): bool
	{
		$q = live_db()->query(
			"SELECT 1 FROM characters WHERE id = :i AND user_id = :u LIMIT 1",
			[':i' => $id, ':u' => $user_id]
		);
		if ($q === false) throw new \Exception('Failed to query char ownership. (C::bt)');
		return $q->fetchArray(SQLITE3_ASSOC) !== false;
	}

	/**
	 * Get the character's user.
	 */
	public function user(): User|false
	{
		if (!empty($this->user)) return $this->user;
		$u = User::find($this->user_id);
		return $u !== false ? $this->user = $u : false;
	}

	/**
	 * Award this character a title.
	 */
	public function award_title(int $title_id): bool
	{
		$r = live_db()->query(
			'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :i)',
			[':t' => $title_id, ':i' => $this->id]
		);
		return $r !== false;
	}

	/**
	 * Delete a character by the ID. Deletes many rows of data from many tables; items, bank, PvE and PvP w/ logs, etc
	 */
	public static function delete(int $id)
	{
		// Delete the character
		if (live_db()->query("DELETE FROM characters WHERE id = :p", [':p' => $id]) === false) {
			throw new \Exception('Failed to delete character. (C::d)');
		}
	}

	/**
	 * Get the character's current title.
	 */
	public function title(): array|false
	{
		if (isset($this->title)) return $this->title;

		$t = title($this->title_id);

		$q = live_db()->query(
			'SELECT awarded FROM owned_titles WHERE char_id = ? AND title_id = ? LIMIT 1',
			[$this->id, $this->title_id]
		);
		if ($q === false) throw new \Exception('Failed to query title. (C::t)');

		$a = $q->fetchArray(SQLITE3_ASSOC);
		if ($a === false) return false;

		$t['awarded'] = $a['awarded']; // add the awarded date to the title info
		return $this->title = $t;
	}
}
