<?php
	namespace Juju\PDO;
	require_once(realpath(dirname(__DIR__).'/pdo.class.php'));
	use Juju\{PDO, PDO\Transaction, Settings};

	abstract class Migration {
		public abstract static function up(Transaction $pdo);
		public abstract static function down(Transaction $pdo);
		public abstract static function change(Transaction $pdo, string $direction);

		const MIGRATE_UP = 'up';
		const MIGRATE_DOWN = 'down';
		private static $pdo;
		private static $last_date;
		private static $initial_date;
		private static $current_version;
		private static $initial_version;

		final public static function version(){
			$name = get_called_class();
			return substr($name, strrpos($name, '\\') + 1);
		}
		final public static function version_table(){
			if(!class_exists("Juju\\Settings")){
				throw new \Exception("Settings not loaded");
			}
			return Settings::get('db.versions');
		}
		final public static function get_current_version(bool $force = false){
			if(is_null(self::$current_version) || $force){
				if(is_null(self::$pdo)){
					throw new \Exception("Migration is not bound to a data source");
				}
				$table_name = self::version_table();
				$query = self::$pdo->query("select max(version) version from `{$table_name}`");
				self::$current_version = $query->fetch()['version'];
				$query->closeCursor();
			}
			return self::$current_version;
		}
		final public static function get_initial_version(bool $force = false){
			if(is_null(self::$initial_version) || $force){
				if(is_null(self::$pdo)){
					throw new \Exception("Migration is not bound to a data source");
				}
				$table_name = self::version_table();
				$query = self::$pdo->query("select min(version) version from `{$table_name}`");
				self::$initial_version = $query->fetch()['version'];
				$query->closeCursor();
			}
			return self::$initial_version;
		}
		final public static function get_last_install_date(bool $force = false){
			if(is_null(self::$last_date) || $force){
				if(is_null(self::$pdo)){
					throw new \Exception("Migration is not bound to a data source");
				}
				$table_name = self::version_table();
				$query = self::$pdo->query("select max(date) date from `{$table_name}`");
				self::$last_date = $query->fetch()['date'];
				$query->closeCursor();
			}
			return self::$last_date;
		}
		final public static function get_initial_install_date(bool $force = false){
			if(is_null(self::$initial_date) || $force){
				if(is_null(self::$pdo)){
					throw new \Exception("Migration is not bound to a data source");
				}
				$table_name = self::version_table();
				$query = self::$pdo->query("select min(date) date from `{$table_name}`");
				self::$initial_date = $query->fetch()['date'];
				$query->closeCursor();
			}
			return self::$initial_date;
		}
		final public static function installed(){
			$pdo = self::$pdo;
			$count = $pdo->exec(
				"select 1 ".
				"from `".self::version_table()."` ".
				"where version = {$pdo->quote(static::version())}"
			);
			if($count === false){
				throw $pdo->getError();
			}
			return $count;
		}
		final public static function migrations(){
			$migrations = array_filter(get_declared_classes(), function($class){
				return 0 === strpos($class, "Migration\\");
			});
			sort($migrations);
			return $migrations;
		}
		final public static function migrate(string $direction, int $amount = 1){
			if($amount < 1){
				throw new \Exception("Migration amount must be a positive integer");
			}
			$pdo = self::$pdo;
			$table = self::$pdo->table(self::version_table());
			$table
				->column('version', 'varchar(100)')
				->column('date', 'datetime', DefaultValue::named('current_timestamp'))
				->primaryKey('version')
				->index('date_idx', ['date'])
				->commit();
			switch($direction){
				case self::MIGRATE_UP:
					foreach(self::migrations() as $migration){
						if(!$migration::installed()){
							$pdo->transaction(function($pdo) use($migration, $table){
								$migration::up($pdo);
								$migration::change($pdo, self::MIGRATE_UP);
							});
							$table->insert(['version'=>$migration::version()]);
							if(!$migration::installed()){
								throw new \Exception("Migration {$migration::version()} {$direction} failed");
							}
							if(--$amount == 0){
								break;
							}
						}
					}
				break;
				case self::MIGRATE_DOWN:
					foreach(array_reverse(self::migrations()) as $migration){
						if($migration::installed()){
							$pdo->transaction(function($pdo) use($migration, $table){
								$migration::change($pdo, self::MIGRATE_DOWN);
								$migration::down($pdo);
							});
							$table->delete(['version'=>$migration::version()]);
							if($migration::installed()){
								throw new \Exception("Migration {$migration::version()} {$direction} failed");
							}
							if(--$amount == 0){
								break;
							}
						}
					}
				break;
				default:
					throw new \Exception("Invalid migration direction '{$direction}'");
			}
			self::get_initial_version(true);
			self::get_initial_install_date(true);
			self::get_current_version(true);
			self::get_last_install_date(true);
		}
		final public static function migrate_all(string $direction){
			self::migrate($direction, count(self::migrations()));
		}
		final public static function bind(string $dsn){
			self::$pdo = PDO::from($dsn);
		}
		final public static function release(){
			self::$pdo = null;
		}
	}
?>