<?php namespace Froiden\RestAPI;

use Carbon\Carbon;
use Closure;
use DateTimeInterface;
use Froiden\RestAPI\Exceptions\RelatedResourceNotFoundException;
use Froiden\RestAPI\Exceptions\ResourceNotFoundException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\Relation;

class ApiModel extends Model
{

    /**
     * List of fields that are visible by default. Only these fields
     * are selected and returned from database
     *
     * @var array
     */
    protected $default = ["id"];

    /**
     * List of fields that are always hidden. Unless modified, these
     * fields are not visible when object is serialised. To comply with
     * Rest architecture, its recommended to hide all relation fields
     * (like, user_id, student_id)
     *
     * @var array
     */
    protected $hidden = ["created_at", "updated_at", "pivot"];

    /**
     * List of fields on which filters are allowed to be applied. For security
     * reasons we cannot allow filters to be allowed on arbitrary fields
     * @var array
     */
    protected $filterable = ["id"];

    /**
     * List of relation attributes found during parsing of request, to be used during saving action
     * @var array
     */
    protected $relationAttributes = [];

    protected $guarded = [];

    /**
     * Raw attributes as sent in request. To be used in setters of various attributes
     * @var array
     */
    protected $raw = [];

    //region Metadata functions

    /**
     * Name of table of this model
     *
     * @return string
     */
    public static function getTableName()
    {
        return (new static)->table;
    }

    /**
     * Date fields in this model
     *
     * @return array
     */
    public static function getDateFields()
    {
        return (new static)->dates;
    }

    /**
     * List of custom fields (attributes) that are appended by default
     * ($appends array)
     *
     * @return array
     */
    public static function getAppendFields()
    {
        return (new static)->appends;
    }

    /**
     * List of fields to display by default ($defaults array)
     *
     * @return array
     */
    public static function getDefaultFields()
    {
        return (new static)->default;
    }

    /**
     * Return the $relationKeys array
     *
     * @return mixed
     */
    public static function getRelationKeyFields()
    {
        return (new static)->relationKeys;
    }

    /**
     * Returns list of fields on which filter is allowed to be applied
     *
     * @return array
     */
    public static function getFilterableFields()
    {
        return (new static)->filterable;
    }

    /**
     * Checks if given relation exists on the model
     *
     * @param $relation
     * @return bool
     */
    public static function relationExists($relation)
    {
        // Check if relation name in modal is in camel case or not
        if (config("api.relation_case", 'snakecase') === 'camelcase') {
            return (method_exists(new static(), $relation) ?? false) || (method_exists(new static(), \Str::camel($relation)) ?? false);
        }
        return method_exists(new static(), $relation);
    }

    //endregion

    /**
     * Prepare a date for array / JSON serialization. Override base method in Model to suite our needs
     *
     * @param  \DateTime  $date
     * @return string
     */
    protected function serializeDate(\DateTimeInterface $date)
    {
        return $date->format("c");
    }

    /**
     * Return a timestamp as DateTime object.
     *
     * @param  mixed  $value
     * @return \Carbon\Carbon
     */
    protected function asDateTime($value)
    {
        // If this value is already a Carbon instance, we shall just return it as is.
        // This prevents us having to re-instantiate a Carbon instance when we know
        // it already is one, which wouldn't be fulfilled by the DateTime check.
        if ($value instanceof Carbon) {
            return $value;
        }

        // If the value is already a DateTime instance, we will just skip the rest of
        // these checks since they will be a waste of time, and hinder performance
        // when checking the field. We will just return the DateTime right away.
        if ($value instanceof DateTimeInterface) {
            return new Carbon(
                $value->format('Y-m-d H:i:s.u'), $value->getTimeZone()
            );
        }

        // If this value is an integer, we will assume it is a UNIX timestamp's value
        // and format a Carbon object from this timestamp. This allows flexibility
        // when defining your date fields as they might be UNIX timestamps here.
        if (is_numeric($value)) {
            return Carbon::createFromTimestamp($value);
        }

        // If the value is in simply year, month, day format, we will instantiate the
        // Carbon instances from that format. Again, this provides for simple date
        // fields on the database, while still supporting Carbonized conversion.
        if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value)) {
            return Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
        }

        // Parse ISO 8061 date
        if (preg_match('/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\\+(\d{2}):(\d{2})$/', $value)) {
            return Carbon::createFromFormat('Y-m-d\TH:i:s+P', $value);
        }
        elseif (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2}T(\d{2}):(\d{2}):(\d{2})\\.(\d{1,3})Z)$/', $value)) {
            return Carbon::createFromFormat('Y-m-d\TH:i:s.uZ', $value);
        }

        // Finally, we will just assume this date is in the format used by default on
        // the database connection and use that format to create the Carbon object
        // that is returned back out to the developers after we convert it here.
        return Carbon::createFromFormat($this->getDateFormat(), $value);
    }

    /**
     * Eagerly load the relationship on a set of models.
     *
     * @param  array  $models
     * @param  string  $name
     * @param  \Closure  $constraints
     * @return array
     */
    protected function loadRelation(array $models, $name, Closure $constraints)
    {
        // First we will "back up" the existing where conditions on the query so we can
        // add our eager constraints. Then we will merge the wheres that were on the
        // query back to it in order that any where conditions might be specified.
        $relation = $this->getRelation($name);

        $relation->addEagerConstraints($models);

        call_user_func($constraints, $relation);

        $models = $relation->initRelation($models, $name);

        // Once we have the results, we just match those back up to their parent models
        // using the relationship instance. Then we just return the finished arrays
        // of models which have been eagerly hydrated and are readied for return.
        $results = $relation->getEager();

        return $relation->match($models, $results, $name);
    }

    /**
     * Fill the model with an array of attributes.
     *
     * @param array $attributes
     * @param bool $relations If the attributes also contain relations
     * @return Model
     */
    public function fill(array $attributes = [])
    {
        $this->raw = $attributes;

        $excludes = config("api.excludes");

        foreach ($attributes as $key => $attribute) {
            // Guarded attributes should be removed
            if (in_array($key, $excludes)) {
                unset($attributes[$key]);
            }
            else if (method_exists($this, $key) && ((is_array($attribute) || is_null($attribute)))) {
                // Its a relation
                $this->relationAttributes[$key] = $attribute;

                // For belongs to relation, while filling, we need to set relation key.
                $relation = call_user_func([$this, $key]);

                if ($relation instanceof BelongsTo) {
                    $primaryKey = $relation->getRelated()->getKeyName();

                    if ($attribute !== null) {
                        // If key value is not set in request, we create new object
                        if (!isset($attribute[$primaryKey])) {
                            throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                        }
                        else {
                            $model = $relation->getRelated()->find($attribute[$primaryKey]);

                            if (!$model) {
                                // Resource not found
                                throw new ResourceNotFoundException();
                            }
                        }
                    }

                    $relationKey = $relation->getForeignKeyName();

                    $this->setAttribute($relationKey, ($attribute === null) ? null : $model->getKey());
                }

                unset($attributes[$key]);
            }
        }

        return parent::fill($attributes);
    }

    public function save(array $options = [])
    {
        // Belongs to relation needs to be set before, because we need the parent's Id
        foreach ($this->relationAttributes as $key => $relationAttribute) {
            /** @var Relation $relation */
            $relation = call_user_func([$this, $key]);

            if ($relation instanceof BelongsTo) {
                $primaryKey = $relation->getRelated()->getKeyName();

                if ($relationAttribute !== null) {
                    // If key value is not set in request, we create new object
                    if (!isset($relationAttribute[$primaryKey])) {
                        throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                    }
                    else {
                        $model = $relation->getRelated()->find($relationAttribute[$primaryKey]);

                        if (!$model) {
                            // Resource not found
                            throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                        }
                    }
                }

                $relationKey = $relation->getForeignKeyName();

                $this->setAttribute($relationKey, ($relationAttribute === null) ? null : $model->getKey());

                unset($this->relationAttributes[$key]);
            }
        }

        parent::save($options);

        // Fill all other relations
        foreach ($this->relationAttributes as $key => $relationAttribute) {
            /** @var Relation $relation */
            $relation = call_user_func([$this, $key]);
            $primaryKey = $relation->getRelated()->getKeyName();

            if ($relation instanceof HasOne || $relation instanceof HasMany) {

                if ($relation instanceof HasOne) {
                    $relationAttribute = [$relationAttribute];
                }

                $relationKey = explode(".", $relation->getQualifiedParentKeyName())[1];

                foreach ($relationAttribute as $val) {
                    if ($val !== null) {
                        if (!isset($val[$primaryKey])) {
                            throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                        }
                        else {
                            /** @var Model $model */
                            $model = $relation->getRelated()->find($val[$primaryKey]);

                            if (!$model) {
                                // Resource not found
                                throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                            }

                            // Only update relation key to attach $model to $this object
                            $model->{$relationKey} = $this->getKey();
                            $model->save();
                        }
                    }
                }
            }

            else if ($relation instanceof BelongsToMany) {
                $relatedIds = [];

                // Value is an array of related models
                foreach ($relationAttribute as $val) {
                    if ($val !== null) {
                        if (!isset($val[$primaryKey])) {
                            throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                        }
                        else {
                            /** @var Model $model */
                            $model = $relation->getRelated()->find($val[$primaryKey]);

                            if (!$model) {
                                // Resource not found
                                throw new RelatedResourceNotFoundException('Resource for relation "' . $key . '" not found');
                            }
                        }
                    }

                    if ($val !== null) {
                       if(isset($val['pivot'])) {
                            // We have additional fields other than primary key
                            // that need to be saved to pivot table
                            /*
                                [
                                    {
                                        "id": 12, // Primary key
                                        "pivot": {
                                            "count": 8 // Pivot table column
                                        }
                                    }
                                ]
                             */
                            $relatedIds[$model->getKey()] = $val['pivot'];
                       }
                       else {
                            // We just have ids
                            $relatedIds[] = $model->getKey();
                       }
                   }
                }

                $relation->sync($relatedIds);
            }
        }
    }
}
