<?php
/**
 * Created by PhpStorm.
 * User: jirislav
 * Date: 6.5.17
 * Time: 19:00
 */

namespace App\Http\ViewHelpers;

use Auth;

use App\Exceptions\ViewFeederException;
use App\Http\Controllers\ViewFeederAwareController;

use DB;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Input;
use Lang;

/**
 * Class ViewFeeder
 * @package App\Http\ViewHelpers
 */
class ViewFeeder
{
    const LIST = ' List';
    const CREATE = 'Create a New ';
    const SHOW = 'Details of the ';
    const EDIT = 'Edit a ';

    const VIEW_FEEDER_TYPES = [
        self::LIST,
        self::CREATE,
        self::SHOW,
        self::EDIT
    ];

    protected $type;

    protected $plural = null;
    protected $singular = null;
    protected $heading = null;
    protected $customHeading = false;

    protected $nothingFound;

    /**
     * @var array|LengthAwarePaginator
     */
    protected $results = [];
    protected $columns = [];

    /**
     * @var Model
     */
    protected $result = null;

    protected $sort;
    protected $ordering;

    protected $permissions = [];
    protected $router = null;

    protected $size = 8;
    protected $viewOffset = 2;

    protected $controller;

    protected $manipulatingHimself = null;
    protected $classId = null;

    protected $allRoles = [];

    protected $autoComplete = [];

    protected $fluid = false;

    /**
     * ViewFeeder constructor.
     *
     * @param ViewFeederAwareController $controller
     * @param string $viewFeederType
     * @throws ViewFeederException
     */
    public function __construct(ViewFeederAwareController $controller, string $viewFeederType)
    {
        if (!in_array($viewFeederType, self::VIEW_FEEDER_TYPES)) {
            throw new ViewFeederException("Unrecognized type '$viewFeederType'");
        }

        $ctrlPathFullName = explode('\\', get_class($controller));
        $this->controller = $ctrlPathFullName[count($ctrlPathFullName) - 1];

        $this->singular = $controller->getSingular();
        $this->plural = $controller->getPlural();
        $this->router = $controller::getRouterName();
        $this->columns = $controller->getColumns();

        switch ($viewFeederType) {
            case self::LIST:
                $this->nothingFound = 'No ' . $this->plural . ' found';
                $this->heading = $this->plural . $viewFeederType;
                break;
            case self::CREATE:
            case self::SHOW:
            case self::EDIT:
                $this->nothingFound = 'This ' . $this->singular . ' was not found';
                $this->heading = $viewFeederType . $this->singular;
                break;
        }

        $this->type = $viewFeederType;
    }

    /**
     * @param LengthAwarePaginator|Collection $results
     * @return $this
     */
    public function results($results)
    {
        $this->results = $results;

        return $this;
    }

    /**
     * @param Model|\StdClass $result
     * @return $this
     */
    public function result($result)
    {
        $this->result = $result;

        return $this;
    }

    /**
     * @param array $columns
     * @return $this
     */
    public function columns(array $columns)
    {
        $this->columns = $columns;

        return $this;
    }

    /**
     * @param string $sort
     * @param string $ordering
     * @return $this
     */
    public function sorting(string $sort, string $ordering)
    {
        $this->sort = $sort;
        $this->ordering = $ordering;

        return $this;
    }

    /**
     * Sets up permissions required for actions.
     *
     * Example:
     * $feeder->permissions([
     *      'edit' => Permissions::EDIT_USER,
     *      'create' => Permissions::CREATE_USER,
     *      'delete' => Permissions::DELETE_USER
     * ])
     *
     * @param array $permissions
     * @return $this
     */
    public function permissions(array $permissions)
    {
        $this->permissions = $permissions;

        return $this;
    }

    /**
     * Changes router provided at the constructor
     *
     * @param string $router
     * @return $this
     */
    public function router(string $router)
    {
        $this->router = $router;

        return $this;
    }

    /**
     * Sets classId for nested list views
     *
     * @param string $classId
     * @return $this
     */
    public function classId(string $classId)
    {
        $this->classId = $classId;

        return $this;
    }

    /**
     * Changes singular provided at the constructor
     *
     * @param string $singular
     * @return $this
     */
    public function singular(string $singular)
    {
        $this->singular = $singular;

        return $this;
    }

    /**
     * Changes heading deducted from the c'tor
     *
     * @param $heading
     * @return $this
     */
    public function heading($heading)
    {
        $this->heading = $heading;
        $this->customHeading = true;

        return $this;
    }

    /**
     * Changes plural deducted from the c'tor
     *
     * @param $plural
     * @return $this
     */
    public function plural($plural)
    {
        $this->plural = $plural;

        return $this;
    }

    /**
     * Sets up final view container size
     *
     * @param int $size
     * @return $this
     * @throws ViewFeederException
     */
    public function size(int $size)
    {
        if ($size < 4 || $size > 12)
            throw new ViewFeederException('Only sizes of interval <6, 12> are supported!');

        $this->size = $size;

        $this->viewOffset = round((12 - $size) / 2, 0, PHP_ROUND_HALF_DOWN);

        return $this;
    }

    /**
     * Fetches all roles & sets 'allRoles' global variable in the view.
     *
     * @return $this
     */
    public function allRoles()
    {
        $allRoles = DB::table('roles')
            ->get(['id', 'name']);

        $roles = [];
        foreach ($allRoles as $role) {
            $roles[$role->id] = $role->name;
        }

        $this->allRoles = $roles;

        return $this;
    }

    /**
     * @param $autoCompleteList
     * @return $this
     * @throws ViewFeederException
     */
    public function autoComplete($autoCompleteList)
    {
        if (!$this->result && $this->type === self::EDIT) {
            throw new ViewFeederException("You must set result first before setting up autoComplete! (Only for EDIT view)");
        }

        $isEdit = ($this->type === self::EDIT);

        foreach ($autoCompleteList as $key => $willDo) {

            if ($willDo)
                if ($isEdit)
                    $value = $this->result->$key;
                else
                    $value = Input::old($key);

            $this->autoComplete[$key] = $willDo ? "<typeahead nothing-string=\"" .
                Lang::getFromJson('Nothing found') .
                "\" placeholder=\"" . Lang::getFromJson('Start typing, values will appear') .
                "\" query=\"" . $value .
                "\" src=\"/api/${key}s\" name='$key'></typeahead>" : null;
        }

        return $this;
    }

    /**
     * @return $this
     */
    public function fluid()
    {
        $this->fluid = true;

        return $this;
    }

    /**
     * Returns associative array to feed a view.
     *
     * @return array
     * @throws ViewFeederException
     */
    public function feed()
    {
        $this->assertNotNull('router', 'singular', 'plural', 'columns');

        switch ($this->type) {
            case self::LIST:
                $this->assertNotNull('nothingFound');
                return $this->feedListView();
                break;
            case self::CREATE:
                return $this->feedCreateView();
                break;
            case self::SHOW:
                $this->assertNotNull('result');
                return $this->feedShowView();
                break;
            case self::EDIT:
                $this->assertNotNull('result');
                return $this->feedEditView();
                break;
            default:
                throw new ViewFeederException("Unimplemented ViewFeederType '$this->type'");
                break;
        }
    }

    /**
     * Returns CSS3 Bootstrap 3 classes for setting up the final container size
     *
     * @return string
     */
    protected function containerSize()
    {
        $offsetClass = $this->viewOffset === 0 ? '' : " col-md-offset-$this->viewOffset";
        $sizeClass = "col-md-$this->size";
        return "$sizeClass$offsetClass";
    }

    /**
     * Returns base feed, common for all types of feeds
     *
     * @return array
     */
    protected function baseFeed()
    {
        return array_merge([
            'fluid' => $this->fluid,
            'manipulatingHimself' => $this->manipulatingHimself(),
            'results' => $this->results,
            'result' => $this->result,
            'heading' => $this->heading,
            'customHeading' => $this->customHeading,
            'plural' => $this->plural,
            'singular' => $this->singular,
            'columns' => $this->columns,
            'containerSize' => $this->containerSize(),
            'nothingFound' => $this->nothingFound,
            'router' => $this->router,
            'controller' => $this->controller,
            'canEdit' => false,
            'canDelete' => false,
            'classId' => $this->classId,
            'objectId' => $this->result !== null ? $this->result->id : null,
            'allRoles' => $this->allRoles,
        ], $this->autoComplete);
    }

    /**
     * Like feed(), but for listings view.
     *
     * @return array
     * @throws ViewFeederException
     */
    protected function feedListView()
    {
        $sort = $this->sort;
        $ordering = $this->ordering;

        if (count($this->results) === 0) {
            return $this->nothingFoundFeed();
        }

        $feed = $this->baseFeed();

        $feed['orderingClass'] = function ($what) use ($sort, $ordering) {

            if ($what !== $sort)
                return '';

            return $ordering;
        };

        $feed['nextOrderingJs'] = function ($what) use ($sort, $ordering) {

            if ($what !== $sort || $ordering === 'asc')
                $ordering = 'desc';
            else
                $ordering = 'asc';

            return "location.href='?sort=$what&ordering=$ordering'";

        };


        $user = Auth::user();

        if (count($this->permissions) !== 0) {

            if (isset($this->results[0])) {
                $hasId = isset($this->results[0]->id);
            } else
                $hasId = false;

            if (!$hasId)
                throw new ViewFeederException('Id must be included in viewed data when dealing with permissions !');
        }

        foreach ($this->permissions as $task => $permission) {
            $feed[$task] = $user->can($permission);
        }

        return $feed;
    }

    /**
     * Returns feed when there is no result to show in view.
     *
     * @return array
     */
    protected function nothingFoundFeed()
    {
        $feed = $this->baseFeed();

        $feed['results'] = [];
        $feed['result'] = null;

        return $feed;
    }

    /**
     * Like feed(), but for create view.
     *
     * @return array
     */
    protected function feedCreateView()
    {

        return $this->baseFeed();
    }

    /**
     * Like feed(), but for show view.
     *
     * @return array
     */
    protected function feedShowView()
    {
        $feed = $this->baseFeed();

        $user = Auth::user();

        foreach ($this->permissions as $task => $permission) {
            $feed[$task] = $user->can($permission);
        }

        return $feed;
    }

    protected function feedEditView()
    {
        $feed = $this->baseFeed();

        $user = Auth::user();

        foreach ($this->permissions as $task => $permission) {
            $feed[$task] = $user->can($permission);
        }

        return $feed;

    }

    /**
     * @return bool|null
     */
    protected function manipulatingHimself()
    {
        if ($this->result === null)
            return false;

        if ($this->manipulatingHimself === null) {
            $resultIsUser = get_class($this->result) === 'App\User';
            $this->manipulatingHimself = $resultIsUser && Auth::user()->id == $this->result->id;

            if ($this->manipulatingHimself)
                $this->heading = 'My Account';
        }
        return $this->manipulatingHimself;
    }

    /**
     * Throws an exception if any of provided arguments is null as an attribute of this class.
     *
     * @param \string[] $varNames
     * @throws ViewFeederException
     */
    protected function assertNotNull(string ...$varNames)
    {
        foreach ($varNames as $varName) {
            if ($this->$varName === null) {
                throw new ViewFeederException("Variable '$varName' must be set!");
            }
        }
    }
}