<?php


namespace App\Tables;

use App\Models\Revision;
use App\Models\Row;
use App\Models\Table;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use MightyPork\Exceptions\NotExistException;
use ReflectionClass;

/**
 * Object representing a set of table modifications
 */
class Changeset
{
    use SerializesModels;

    const object_excluded_properties = [
        'revision',
        'table',
        'note',
    ];

    /**
     * @var Revision - base revision this changeset belongs to
     */
    public $revision;

    /**
     * @var Table - table this changeset belongs to
     */
    public $table;

    /**
     * @var string - user's note attached to this changeset (future proposal)
     */
    public $note = '';

    /**
     * Rows whose content changed, identified by _id.
     * Only changed values are to be filled. Columns are identified by GCIDs
     *
     * Key'd by _id
     *
     * @var array|null - [_id -> [_id:…, cid:…, cid:…], …]
     */
    public $rowUpdates = [];

    /**
     * New rows in the full Row::data format, including GRIDs.
     * Values are identified by GCIDs from previously defined, or new columns.
     *
     * @var array|null - [[_id:…, cid:…, cid:…], …]
     */
    public $newRows = [];

    /**
     * Rows to be removed
     *
     * @var int[]|null - GRIDs
     */
    public $removedRows = [];

    /**
     * Values changed in column specifications, such as name, title, etc.
     * This does not affect the table rows in any way.
     *
     * Key'd by id
     *
     * @var array[] - column specification objects, with GCIDs, key'd by CID
     */
    public $columnUpdates = [];

    /**
     * New columns in the full format, including GCIDs
     *
     * @var array|null - [[id:…, …], …]
     */
    public $newColumns = [];

    /**
     * When reordering columns, here is the column IDs array
     * in the new order. Columns meanwhile removed from the table
     * or added to it are to be ignored or appended at the end.
     *
     * This shall not be filled if merely editing or adding columns,
     * unless the order is explicitly adjusted by the user.
     *
     * @var string[]|null - GCIDs
     */
    public $columnOrder = [];

    /**
     * Columns to be removed
     *
     * The data associated to those columns may or may not be removed from the Rows.
     * It does not matter, other than in regard to the table size.
     *
     * @var int[]|null - GCIDs
     */
    public $removedColumns = [];

    private function walkProps()
    {
        $properties = (new ReflectionClass($this))->getProperties();

        foreach ($properties as $property) {
            if (in_array($property->name, self::object_excluded_properties)) {
                continue;
            }

            yield $property->name;
        }
    }

    /**
     * Reconstruct from a object, such as one found in Proposal.
     * Note that the fromProposal() method should be used when the
     * proposal is available, as it populates additional fields.
     *
     * @param \stdClass $changes
     * @return Changeset
     */
    public static function fromObject($changes)
    {
        $changeset = new Changeset();

        foreach ($changeset->walkProps() as $prop) {
            if (isset($changes->$prop)) {
                $changeset->$prop = $changes->$prop;
            }
        }

        return $changeset;
    }

    /**
     * Serialize to an object format that may be stored in a Proposal.
     *
     * @return \stdClass
     */
    public function toObject()
    {
        $object = new \stdClass();

        foreach ($this->walkProps() as $prop) {
            $object->$prop = $this->$prop;
        }

        return $object;
    }

    /**
     * Check if there is any change in this changeset
     *
     * @return bool - any found
     */
    public function hasAnyChanges()
    {
        foreach ($this->walkProps() as $prop) {
            if (!empty($this->$prop)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Decorate / transform a single row for the editor view
     *
     * @param object|\DecoratedRow $row - row, must be key'd by column ids
     * @param bool $decorate - to add extra underscored info for the editor
     * @return \DecoratedRow|object|null - null if not decorating and the row was removed
     */
    public function transformRow($row, $decorate)
    {
        if ($row instanceof Row) $row = (object)$row->getAttributes();

        if ($decorate) {
            $row->_remove = false;
            $row->_changed = [];
            $row->_orig = [];
        }

        // Removed rows
        if (in_array($row->_id, $this->removedRows)) {
            if ($decorate) {
                $row->_remove = true;
            } else {
                return null;
            }
        }

        // Changed values
        if (isset($this->rowUpdates[$row->_id])) {
            $newVals = $this->rowUpdates[$row->_id];

            if ($decorate) {
                $row->_changed = array_keys($newVals);
                $row->_orig = array_only((array)$row, $row->_changed);
            }

            $row = (object)array_merge((array)$row, $newVals);
        }

        // Drop deleted columns
        if (!$decorate) {
            foreach ($this->removedColumns as $colId) {
                unset($row->$colId);
            }
        }

        unset($row->_row_pivot);

        return $row;
    }

    /**
     * Decorate / transform columns (loaded from the source revision)
     *
     * @return Column[]
     */
    public function fetchAndTransformColumns()
    {
        /** @var Column[] - loaded and transformed columns, cached from previous call to transformColumns() */
        static $cachedColumns = [];

        if ($cachedColumns) return $cachedColumns;
        $columns = Column::columnsFromJson($this->revision->columns);

        // Modify columns
        foreach ($columns as $column) {
            if (isset($this->columnUpdates[$column->id])) {
                $column->modifyByChangeset($this->columnUpdates[$column->id]);
            }

            if (in_array($column->id, $this->removedColumns)) {
                $column->markForRemoval();
            }
        }

        // Append new columns
        foreach ($this->newColumns as $newColumn) {
            $columns[] = $c =new Column($newColumn);
            $c->markAsNew();
        }

        // Reorder
        $colsById = collect($columns)->keyBy('id')->all();
        $newOrder = [];
        foreach ($this->columnOrder as $id) {
            $newOrder[] = $colsById[$id];
        }

        $leftover_keys = array_diff(array_keys($colsById), $this->columnOrder);
        foreach ($leftover_keys as $id) {
            $newOrder[] = $colsById[$id];
        }

        return $cachedColumns = $newOrder;
    }

    public function rowRemove(int $id)
    {
        $this->removedRows[] = $id;
    }

    public function rowRestore(int $id)
    {
        $this->removedRows = array_diff($this->removedRows, [$id]);
    }

    public function fetchAndTransformRow(int $id)
    {
        $r = $this->fetchRow($id);
        $transformed = $this->transformRow($r, true);
        return $transformed;
    }

    public function fetchColumns()
    {
        return Column::columnsFromJson($this->revision->columns);
    }

    public function fetchRow($id)
    {
        $r = $this->revision->rowsData($this->fetchColumns(), true, false)
            ->whereRaw("data->>'_id' = ?", $id)->first();

        if (! $r) throw new NotExistException("No such row _id = $id in this revision.");

        // remove junk
        unset($r->pivot_revision_id);
        unset($r->pivot_row_id);

        return (object)$r->getAttributes();
    }

    public function rowUpdate($newVals)
    {
        $_id = $newVals->_id;
        $origRow = $this->fetchRow($_id);

        /** @var Column[]|Collection $cols */
        $cols = collect($this->fetchAndTransformColumns())->keyBy('id');

        $updateObj = [];
        \Debugbar::addMessage(json_encode($cols));
        \Debugbar::addMessage(var_export($newVals, true));

        foreach ($newVals as $colId => $value) {
            if (starts_with($colId, '_')) continue; // internals

            if (!isset($origRow->$colId) || $value != $origRow->$colId) {
                $updateObj[$colId] = $cols[$colId]->cast($value);
            }
        }

        \Debugbar::addMessage("New: ".json_encode($newVals));
        \Debugbar::addMessage("Orig: ".json_encode($origRow));
        \Debugbar::addMessage("UpdateObj: ".json_encode($updateObj));

        if (!empty($updateObj)) {
            $this->rowUpdates[$_id] = $updateObj;
        } else {
            // remove possible old update record for this row, if nothing changes now
            unset($this->rowUpdates[$_id]);
        }
    }
}