<?php

/**
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
 * @copyright Aimeos (aimeos.org), 2021
 */


namespace Aimeos\Upscheme\Schema;

use Doctrine\DBAL\Schema\Table as DbalTable;


/**
 * Foreign schema class
 *
 * @property string $onDelete
 * @property string $onUpdate
 */
class Foreign
{
	use \Aimeos\Macro\Macroable;


	/**
	 * @var \Aimeos\Upscheme\Schema\DB
	 */
	private $db;

	/**
	 * @var \Aimeos\Upscheme\Schema\Table
	 */
	private $table;

	/**
	 * @var \Doctrine\DBAL\Schema\Table
	 */
	private $dbaltable;

	/**
	 * @var array<string>
	 */
	private $localcol;

	/**
	 * @var string
	 */
	private $fktable;

	/**
	 * @var array<string>
	 */
	private $fkcol;

	/**
	 * @var string|null
	 */
	private $name;

	/**
	 * @var array<string>
	 */
	private $opts;


	/**
	 * Initializes the foreign key object
	 *
	 * @param \Aimeos\Upscheme\Schema\DB $db DB schema object
	 * @param \Aimeos\Upscheme\Schema\Table $table Table schema object
	 * @param \Doctrine\DBAL\Schema\Table $dbaltable Doctrine table object
	 * @param array<string> $localcol List of columns from the current table spawning the foreign key constraint
	 * @param string $fktable Name of the referenced table
	 * @param array<string> $fkcol List of columns from the referenced table spawning the foreign key constraint
	 * @param string|null $name $name Name of the foreign key constraint and index or NULL for autogenerated name
	 */
	public function __construct( DB $db, Table $table, DbalTable $dbaltable, array $localcol, string $fktable, array $fkcol, ?string $name = null )
	{
		$this->db = $db;
		$this->table = $table;
		$this->dbaltable = $dbaltable;
		$this->localcol = $localcol;
		$this->fktable = $fktable;
		$this->fkcol = $fkcol;
		$this->name = $name;
		$this->opts = [
			'onDelete' => 'CASCADE',
			'onUpdate' => 'CASCADE',
		];

		if( !$table->hasIndex( (string) $name ) ) {
			$table->index( $localcol, $name );
		}

		if( !$table->hasForeign( (string) $name ) )
		{
			$lcol = $fcol = [];

			foreach( (array) $localcol as $key => $column ) {
				$lcol[$key] = $db->qi( $column );
			}

			foreach( (array) $fkcol as $key => $column ) {
				$fcol[$key] = $db->qi( $column );
			}

			$dbaltable->addForeignKeyConstraint( $db->qi( $fktable ), $lcol, $fcol, $this->opts, $name ? $db->qi( $name ) : null );
		}
	}


	/**
	 * Calls custom methods or passes unknown method calls to the Doctrine table object
	 *
	 * @param string $method Name of the method
	 * @param array<mixed> $args Method parameters
	 * @return mixed Return value of the called method
	 */
	public function __call( string $method, array $args )
	{
		if( self::macro( $method ) ) {
			return $this->call( $method, ...$args );
		}

		throw new \BadMethodCallException( sprintf( 'Unknown method "%1$s" in %2$s', $method, __CLASS__ ) );
	}


	/**
	 * Returns the value for the given sequence option
	 *
	 * @param string $name Sequence option name
	 * @return mixed Sequence option value
	 */
	public function __get( string $name )
	{
		return $this->{$name}();
	}


	/**
	 * Sets the new value for the given sequence option
	 *
	 * @param string $name Sequence option name
	 * @param mixed $value Sequence option value
	 */
	public function __set( string $name, $value )
	{
		$this->{$name}( $value );
	}


	/**
	 * Sets the action if referenced rows are deleted or updated
	 *
	 * Available actions are:
	 * - CASCADE : Delete or update referenced value
	 * - NO ACTION : No change in referenced value
	 * - RESTRICT : Forbid changing values
	 * - SET DEFAULT : Set referenced value to the default value
	 * - SET NULL : Set referenced value to NULL
	 *
	 * @param string $action Performed action
	 * @return self Same object for fluid method calls
	 */
	public function do( string $action ) : self
	{
		if( $this->opts['onDelete'] !== $action || $this->opts['onUpdate'] !== $action )
		{
			$this->opts['onDelete'] = $action;
			$this->opts['onUpdate'] = $action;

			return $this->replace();
		}

		return $this;
	}


	/**
	 * Returns the current name of the foreign key constraint
	 *
	 * @return string|null Name of the constraint or NULL if no name is available
	 */
	public function name()
	{
		return $this->name;
	}


	/**
	 * Sets the action if the referenced row is deleted or returns the current value
	 *
	 * Available actions are:
	 * - CASCADE : Delete referenced value
	 * - NO ACTION : No change in referenced value
	 * - RESTRICT : Forbid changing values
	 * - SET DEFAULT : Set referenced value to the default value
	 * - SET NULL : Set referenced value to NULL
	 *
	 * @param string|null $value Performed action or NULL to return current value
	 * @return self|string Same object for setting the value, current value without parameter
	 */
	public function onDelete( ?string $value = null )
	{
		if( $value === null ) {
			return $this->opts['onDelete'];
		}

		if( $this->opts['onDelete'] !== $value )
		{
			$this->opts['onDelete'] = $value;
			return $this->replace();
		}

		return $this;
	}


	/**
	 * Sets the action if the referenced row is updated or returns the current value
	 *
	 * Available actions are:
	 * - CASCADE : Update referenced value
	 * - NO ACTION : No change in referenced value
	 * - RESTRICT : Forbid changing values
	 * - SET DEFAULT : Set referenced value to the default value
	 * - SET NULL : Set referenced value to NULL
	 *
	 * @param string|null $value Performed action or NULL to return current value
	 * @return self|string Same object for setting the value, current value without parameter
	 */
	public function onUpdate( ?string $value = null )
	{
		if( $value === null ) {
			return $this->opts['onUpdate'];
		}

		if( $this->opts['onUpdate'] !== $value )
		{
			$this->opts['onUpdate'] = $value;
			return $this->replace();
		}

		return $this;
	}


	/**
	 * Applies the changes to the database schema
	 *
	 * @return self Same object for fluid method calls
	 */
	public function up() : self
	{
		$this->table->up();
		return $this;
	}


	/**
	 * Deletes the current constraint and creates a new one
	 *
	 * @param string|null $newname Name of the new constraint or same name if NULL
	 * @return self Same object for fluid method calls
	 */
	protected function replace( ?string $newname = null ) : self
	{
		$newname = $newname ?: $this->name;

		if( $this->name )
		{
			$this->dbaltable->removeForeignKey( $this->db->qi( $this->name ) );
			$this->table->dropIndex( $this->name );
		}

		$lcol = $fcol = [];

		foreach( $this->localcol as $key => $column ) {
			$lcol[$key] = $this->db->qi( $column );
		}

		foreach( $this->fkcol as $key => $column ) {
			$fcol[$key] = $this->db->qi( $column );
		}

		$this->table->index( $this->localcol, $newname )->up();
		$this->dbaltable->addForeignKeyConstraint(
			$this->db->qi( $this->fktable ),
			$lcol, $fcol, $this->opts,
			$newname ? $this->db->qi( $newname ) : null
		);

		$this->name = $newname;
		return $this;
	}
}