<?php
/**
 * Belongs To Relationship
 * 
 * @package ZenoChatbot\Framework\Database\Relations
 */

namespace ZenoChatbot\Framework\Database\Relations;

use ZenoChatbot\Framework\Database\Model;
use ZenoChatbot\Framework\Database\ModelCollection;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Belongs To Relationship Class
 */
class BelongsTo extends Relation
{
    /**
     * The foreign key of the parent model
     *
     * @var string
     */
    protected $foreignKey;

    /**
     * The associated key on the parent model
     *
     * @var string
     */
    protected $ownerKey;

    /**
     * The name of the relationship
     *
     * @var string
     */
    protected $relationName;

    /**
     * Create a new belongs to relationship instance
     *
     * @param Model $parent
     * @param Model $related
     * @param string $foreignKey
     * @param string $ownerKey
     * @param string $relationName
     */
    public function __construct(Model $parent, Model $related, $foreignKey, $ownerKey, $relationName)
    {
        $this->foreignKey = $foreignKey;
        $this->ownerKey = $ownerKey;
        $this->relationName = $relationName;

        parent::__construct($parent, $related);
        
        $this->addConstraints();
    }

    /**
     * Add constraints to the relationship query
     *
     * @return void
     */
    public function addConstraints()
    {
        if (static::$constraints) {
            $this->query->where($this->ownerKey, '=', $this->getForeignKeyValue());
        }
    }

    /**
     * Add eager loading constraints to the relationship query
     *
     * @param array $models
     * @return void
     */
    public function addEagerConstraints(array $models)
    {
        $keys = $this->getEagerModelKeys($models);
        
        $this->query->whereIn($this->ownerKey, $keys);
    }

    /**
     * Initialize the relation on a set of models
     *
     * @param array $models
     * @param string $relation
     * @return array
     */
    public function initRelation(array $models, $relation)
    {
        foreach ($models as $model) {
            $model->setRelation($relation, null);
        }

        return $models;
    }

    /**
     * Match the eagerly loaded results to their parents
     *
     * @param array $models
     * @param ModelCollection $results
     * @param string $relation
     * @return array
     */
    public function match(array $models, ModelCollection $results, $relation)
    {
        $dictionary = $this->buildDictionary($results);

        foreach ($models as $model) {
            $key = $model->getAttribute($this->foreignKey);
            
            if (isset($dictionary[$key])) {
                $model->setRelation($relation, $dictionary[$key]);
            }
        }

        return $models;
    }

    /**
     * Get the results of the relationship
     *
     * @return Model|null
     */
    public function getResults()
    {
        return $this->first();
    }

    /**
     * Associate the model instance to the given parent
     *
     * @param Model $model
     * @return Model
     */
    public function associate(Model $model)
    {
        $this->parent->setAttribute($this->foreignKey, $model->getAttribute($this->ownerKey));
        
        return $this->parent->setRelation($this->relationName, $model);
    }

    /**
     * Dissociate previously associated model from the given parent
     *
     * @return Model
     */
    public function dissociate()
    {
        $this->parent->setAttribute($this->foreignKey, null);
        
        return $this->parent->setRelation($this->relationName, null);
    }

    /**
     * Update the parent model on the relationship
     *
     * @param array $attributes
     * @return mixed
     */
    public function update(array $attributes)
    {
        return $this->getResults()->fill($attributes)->save();
    }

    /**
     * Delete the related model
     *
     * @return int
     */
    public function delete()
    {
        return $this->query->delete();
    }

    /**
     * Get the foreign key value for the relationship
     *
     * @return mixed
     */
    protected function getForeignKeyValue()
    {
        return $this->parent->getAttribute($this->foreignKey);
    }

    /**
     * Get the keys for an eager load of the relation
     *
     * @param array $models
     * @return array
     */
    protected function getEagerModelKeys(array $models)
    {
        $keys = [];

        foreach ($models as $model) {
            $value = $model->getAttribute($this->foreignKey);
            if ($value !== null) {
                $keys[] = $value;
            }
        }

        return array_unique($keys);
    }

    /**
     * Build model dictionary keyed by the relation's foreign key
     *
     * @param ModelCollection $results
     * @return array
     */
    protected function buildDictionary(ModelCollection $results)
    {
        $dictionary = [];

        foreach ($results as $result) {
            $key = $result->getAttribute($this->ownerKey);
            $dictionary[$key] = $result;
        }

        return $dictionary;
    }

    /**
     * Get the foreign key for the relationship
     *
     * @return string
     */
    public function getForeignKeyName()
    {
        return $this->foreignKey;
    }

    /**
     * Get the owner key for the relationship
     *
     * @return string
     */
    public function getOwnerKeyName()
    {
        return $this->ownerKey;
    }

    /**
     * Get the relationship name
     *
     * @return string
     */
    public function getRelationName()
    {
        return $this->relationName;
    }

    /**
     * Static constraints flag
     *
     * @var bool
     */
    protected static $constraints = true;

    /**
     * Run a callback with constraints disabled
     *
     * @param callable $callback
     * @return mixed
     */
    public static function noConstraints(callable $callback)
    {
        $previous = static::$constraints;
        static::$constraints = false;

        try {
            return $callback();
        } finally {
            static::$constraints = $previous;
        }
    }
}
