<?php
/**
 * Belongs To 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;
}

/**
 * Belongs To Many Relationship Class
 */
class BelongsToMany extends Relation
{
    /**
     * The intermediate table for the relation
     *
     * @var string
     */
    protected $table;

    /**
     * The foreign key of the parent model
     *
     * @var string
     */
    protected $foreignPivotKey;

    /**
     * The associated key of the relation
     *
     * @var string
     */
    protected $relatedPivotKey;

    /**
     * The parent key for the relationship
     *
     * @var string
     */
    protected $parentKey;

    /**
     * The related key for the relationship
     *
     * @var string
     */
    protected $relatedKey;

    /**
     * The pivot columns to retrieve
     *
     * @var array
     */
    protected $pivotColumns = [];

    /**
     * Create a new belongs to many relationship instance
     *
     * @param Model $parent
     * @param Model $related
     * @param string $table
     * @param string $foreignPivotKey
     * @param string $relatedPivotKey
     * @param string $parentKey
     * @param string $relatedKey
     */
    public function __construct(
        Model $parent,
        Model $related,
        $table,
        $foreignPivotKey,
        $relatedPivotKey,
        $parentKey,
        $relatedKey
    ) {
        $this->table = $table;
        $this->foreignPivotKey = $foreignPivotKey;
        $this->relatedPivotKey = $relatedPivotKey;
        $this->parentKey = $parentKey;
        $this->relatedKey = $relatedKey;

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

    /**
     * Add constraints to the relationship query
     *
     * @return void
     */
    public function addConstraints()
    {
        $this->performJoin();

        if (static::$constraints) {
            $this->addWhereConstraints();
        }
    }

    /**
     * Add eager loading constraints to the relationship query
     *
     * @param array $models
     * @return void
     */
    public function addEagerConstraints(array $models)
    {
        $keys = $this->getKeys($models, $this->parentKey);
        
        $this->query->whereIn($this->getQualifiedForeignPivotKeyName(), $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->parentKey);
            
            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();
    }

    /**
     * Attach a model to the parent
     *
     * @param mixed $id
     * @param array $attributes
     * @return void
     */
    public function attach($id, array $attributes = [])
    {
        $records = $this->formatAttachRecords(
            $this->parseIds($id), $attributes
        );

        foreach ($records as $record) {
            $this->parent->getConnection()->insert($this->table, $record);
        }
    }

    /**
     * Detach models from the relationship
     *
     * @param mixed $ids
     * @return int
     */
    public function detach($ids = null)
    {
        $query = $this->newPivotQuery();

        if (!is_null($ids)) {
            $ids = $this->parseIds($ids);
            $query->whereIn($this->relatedPivotKey, $ids);
        }

        return $query->delete();
    }

    /**
     * Sync the intermediate tables with a list of IDs or collection of models
     *
     * @param mixed $ids
     * @param bool $detaching
     * @return array
     */
    public function sync($ids, $detaching = true)
    {
        $changes = [
            'attached' => [],
            'detached' => [],
            'updated' => [],
        ];

        $current = $this->getCurrentlyAttachedPivots();
        $records = $this->formatSyncRecords($this->parseIds($ids));

        $detach = array_diff($current, array_keys($records));

        if ($detaching && count($detach) > 0) {
            $this->detach($detach);
            $changes['detached'] = $detach;
        }

        $changes = array_merge(
            $changes, $this->attachNew($records, $current)
        );

        return $changes;
    }

    /**
     * Get the pivot columns for the relation
     *
     * @param array $columns
     * @return $this
     */
    public function withPivot($columns)
    {
        $this->pivotColumns = array_merge(
            $this->pivotColumns, is_array($columns) ? $columns : func_get_args()
        );

        return $this;
    }

    /**
     * Perform the join for the relationship query
     *
     * @return void
     */
    protected function performJoin()
    {
        $baseTable = $this->related->getTable();
        
        $this->query->join(
            $this->table,
            $this->getQualifiedRelatedKeyName(),
            '=',
            $this->getQualifiedRelatedPivotKeyName()
        );

        $this->query->select([
            $baseTable . '.*',
            $this->table . '.' . $this->foreignPivotKey . ' as pivot_' . $this->foreignPivotKey,
            $this->table . '.' . $this->relatedPivotKey . ' as pivot_' . $this->relatedPivotKey,
        ]);

        // Add pivot columns
        foreach ($this->pivotColumns as $column) {
            $this->query->select($this->table . '.' . $column . ' as pivot_' . $column);
        }
    }

    /**
     * Add the where constraints for the relationship query
     *
     * @return void
     */
    protected function addWhereConstraints()
    {
        $this->query->where(
            $this->getQualifiedForeignPivotKeyName(),
            '=',
            $this->parent->getAttribute($this->parentKey)
        );
    }

    /**
     * Create a new pivot query builder
     *
     * @return \ZenoChatbot\Framework\Database\QueryBuilder
     */
    protected function newPivotQuery()
    {
        $query = $this->parent->newQuery()->table($this->table);
        
        return $query->where($this->foreignPivotKey, $this->parent->getAttribute($this->parentKey));
    }

    /**
     * Get the currently attached pivot records
     *
     * @return array
     */
    protected function getCurrentlyAttachedPivots()
    {
        $results = $this->newPivotQuery()->get();
        
        return $results->pluck($this->relatedPivotKey)->toArray();
    }

    /**
     * Format the sync records for attachment
     *
     * @param array $ids
     * @return array
     */
    protected function formatSyncRecords(array $ids)
    {
        $records = [];

        foreach ($ids as $id => $attributes) {
            if (!is_array($attributes)) {
                list($id, $attributes) = [$attributes, []];
            }

            $records[$id] = $attributes;
        }

        return $records;
    }

    /**
     * Format the attach record
     *
     * @param array $ids
     * @param array $attributes
     * @return array
     */
    protected function formatAttachRecords(array $ids, array $attributes)
    {
        $records = [];

        foreach ($ids as $id) {
            $records[] = array_merge([
                $this->foreignPivotKey => $this->parent->getAttribute($this->parentKey),
                $this->relatedPivotKey => $id,
            ], $attributes);
        }

        return $records;
    }

    /**
     * Attach new records that aren't already attached
     *
     * @param array $records
     * @param array $current
     * @return array
     */
    protected function attachNew(array $records, array $current)
    {
        $changes = ['attached' => [], 'updated' => []];

        foreach ($records as $id => $attributes) {
            if (!in_array($id, $current)) {
                $this->attach($id, $attributes);
                $changes['attached'][] = $id;
            }
        }

        return $changes;
    }

    /**
     * Parse the given IDs
     *
     * @param mixed $ids
     * @return array
     */
    protected function parseIds($ids)
    {
        if ($ids instanceof Model) {
            return [$ids->getAttribute($this->relatedKey)];
        }

        if ($ids instanceof ModelCollection) {
            return $ids->pluck($this->relatedKey)->toArray();
        }

        return (array) $ids;
    }

    /**
     * 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('pivot_' . $this->foreignPivotKey);
            
            if (!isset($dictionary[$key])) {
                $dictionary[$key] = [];
            }
            
            $dictionary[$key][] = $result;
        }

        return $dictionary;
    }

    /**
     * Get the qualified foreign pivot key name
     *
     * @return string
     */
    protected function getQualifiedForeignPivotKeyName()
    {
        return $this->table . '.' . $this->foreignPivotKey;
    }

    /**
     * Get the qualified related pivot key name
     *
     * @return string
     */
    protected function getQualifiedRelatedPivotKeyName()
    {
        return $this->table . '.' . $this->relatedPivotKey;
    }

    /**
     * Get the qualified related key name
     *
     * @return string
     */
    protected function getQualifiedRelatedKeyName()
    {
        return $this->related->getTable() . '.' . $this->relatedKey;
    }

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