<?php
/**
 * Has Many 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;
}

/**
 * Has Many Relationship Class
 */
class HasMany extends Relation
{
    /**
     * The foreign key of the parent model
     *
     * @var string
     */
    protected $foreignKey;

    /**
     * The local key of the parent model
     *
     * @var string
     */
    protected $localKey;

    /**
     * Create a new has many relationship instance
     *
     * @param Model $parent
     * @param Model $related
     * @param string $foreignKey
     * @param string $localKey
     */
    public function __construct(Model $parent, Model $related, $foreignKey, $localKey)
    {
        $this->foreignKey = $foreignKey;
        $this->localKey = $localKey;

        parent::__construct($parent, $related);

        $this->addConstraints();
    }

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

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

        $this->query->whereIn($this->foreignKey, $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, new ModelCollection());
        }

        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->localKey);

            if (isset($dictionary[$key])) {
                $model->setRelation($relation, new ModelCollection($dictionary[$key]));
            }
        }

        return $models;
    }

    /**
     * Get the results of the relationship
     *
     * @return ModelCollection
     */
    public function getResults()
    {
        return $this->get();
    }

    /**
     * Save a new model and return the instance
     *
     * @param Model $model
     * @return Model
     */
    public function save(Model $model)
    {
        $this->setForeignAttributesForCreate($model);

        return $model->save() ? $model : false;
    }

    /**
     * Save an array of new models and return the instances
     *
     * @param array $models
     * @return array
     */
    public function saveMany(array $models)
    {
        foreach ($models as $model) {
            $this->save($model);
        }

        return $models;
    }

    /**
     * Create a new instance of the related model
     *
     * @param array $attributes
     * @return Model
     */
    public function create(array $attributes = [])
    {
        $instance = $this->related->newInstance($attributes);
        $this->setForeignAttributesForCreate($instance);
        $instance->save();

        return $instance;
    }

    /**
     * Create an array of new instances of the related model
     *
     * @param array $records
     * @return array
     */
    public function createMany(array $records)
    {
        $instances = [];

        foreach ($records as $record) {
            $instances[] = $this->create($record);
        }

        return $instances;
    }

    /**
     * Update related models
     *
     * @param array $attributes
     * @return int
     */
    public function update(array $attributes)
    {
        return $this->query->update($attributes);
    }

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

    /**
     * Get the parent key value
     *
     * @return mixed
     */
    protected function getParentKey()
    {
        return $this->parent->getAttribute($this->localKey);
    }

    /**
     * Get an array of keys from the given models
     *
     * @param array $models
     * @param string $key
     * @return array
     */
    protected function getKeys(array $models, $key)
    {
        $keys = [];

        foreach ($models as $model) {
            $value = $model->getAttribute($key);
            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->foreignKey);

            if (!isset($dictionary[$key])) {
                $dictionary[$key] = [];
            }

            $dictionary[$key][] = $result;
        }

        return $dictionary;
    }

    /**
     * Set the foreign key attributes for creating a related model
     *
     * @param Model $model
     * @return void
     */
    protected function setForeignAttributesForCreate(Model $model)
    {
        $model->setAttribute($this->foreignKey, $this->getParentKey());
    }

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

    /**
     * Get the local key for the relationship
     *
     * @return string
     */
    public function getLocalKeyName()
    {
        return $this->localKey;
    }

    /**
     * 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;
        }
    }
}
