<?php

namespace Hilco;

use DB;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Request;

use Illuminate\Support\Collection;

abstract class VisionwareEloquentDatatable {
    /**
     * @var Collection
     */
    protected $input;
    /**
     * @var Collection
     */
    protected $fixedFilters;
    /**
     * @var Collection
     */
    protected $filters;
    /**
     * @var array
     */
    protected $expectedFilters = [];
    /**
     * @var Builder
     */
    protected $informationQuery;
    /**
     * @var Builder
     */
    protected $filterQuery;

    protected $filteredIds;

    protected function __construct(Request $request) {
        $this->input = collect($request->all());
        $this->filters = new Collection;
        $this->fixedFilters = new Collection;
        $this->filteredIds = [];
        array_push($this->expectedFilters, 'visible');
        array_push($this->expectedFilters, 'deleted');
    }

    public static function handleRequest(Request $request, $additionalFilters = []) {
        $instance = new static($request);
        return $instance->go($additionalFilters);
    }

    abstract protected function createQuery();

    protected function go($additionalFilters = []) {
        $this->populateFilters($additionalFilters);

        $filterQuery = $this->buildFilterQuery(DB::table($this->primaryTable));
        $filterQuery = $this->applyFixedFilters($filterQuery);
        $potentialCount = $this->count($filterQuery);
        $filterQuery = $this->applyFilters($filterQuery);
        $filteredCount = $this->count($filterQuery);
        $filterQuery = $this->applyLimits($filterQuery);
        $filterQuery->addSelect($this->groupByColumn());
        $filterQuery = $this->applyOrdering($filterQuery);

        $this->filteredIds = $filterQuery->groupBy($this->groupByColumn())->pluck($this->groupByColumn());

        $informationQuery = $this->buildInformationQuery(DB::table($this->primaryTable))->whereIn($this->groupByColumn(), $this->filteredIds);
        $informationQuery = $this->addColumns($informationQuery);
        $informationQuery = $this->applyOrdering($informationQuery);

        $results = $this->getResults($informationQuery);
        $response = [
            'draw' => (int)$this->input->get('draw'),
            'recordsTotal' => $potentialCount,
            'recordsFiltered' => $filteredCount,
            'data' => $results,
        ];

        $response = array_merge($response, $this->additionalData());
        return $response;
    }

    protected function additionalData() {
        return [];
    }

    protected function getResults(Builder $query) {
        $results = $query->groupBy($this->groupByColumn())->get();
        if (!is_array($results)) $results = $results->toArray();

        $data = [];
        foreach ($results as $rowObject) {
            $row = (array)$rowObject;
            $preparedRow = $this->prepareRow($row);
            if (!\Illuminate\Support\Arr::has($preparedRow, 'isDeleted')) $preparedRow['isDeleted'] = \Illuminate\Support\Arr::get($preparedRow, 'deleted_at', 0) > 0;
            if (!\Illuminate\Support\Arr::has($preparedRow, 'DT_RowId') && \Illuminate\Support\Arr::has($preparedRow, 'id')) $preparedRow['DT_RowId'] = $preparedRow['id'];
            $data[] = $preparedRow;
        }
        return $data;
    }

    protected function prepareRow(array $row) {
        return $row;
    }

    protected function addColumns(Builder $query) {
        $columnMap = $this->baseColumnMap();
        foreach ($columnMap as $as => $column) {
            $query->addSelect(DB::raw("$column as $as"));
        }
        return $query;
    }

    protected function applyOrdering(Builder $query, $addColumns = true) {
        $columns = $this->input->get('columns');
        $columnMap = $this->baseColumnMap();

        foreach ($this->input->get('order', []) as $orderBy) {
            $orderByName = $columns[$orderBy['column']]['data'];
            $mappedName = $columnMap[$orderByName];
            if ($addColumns) {
                if (!strpos($mappedName, '(')) {
                    $orderByName = $mappedName;
                } else {
                    $query->addSelect(DB::raw("$mappedName as $orderByName"));
                }
            }
            $query->orderBy($orderByName, $orderBy['dir']);
        }
        return $query;
    }

    protected function applyLimits(Builder $query) {
        if ($this->input->get('length')) $query->take($this->input->get('length'));
        if ($this->input->get('start')) $query->offset($this->input->get('start'));
        return $query;
    }

    protected function buildFilterQuery(Builder $query) {
        return $this->buildInformationQuery($query);
    }

    protected function count(Builder $query) {
        return $query->distinct($this->groupByColumn())->count($this->groupByColumn());
    }

    protected function filterSearch(Builder $query) {
        $searchArray = $this->filters['search'];
        $query->where(function ($wheres) use ($searchArray) {
            foreach ($searchArray as $column => $value) {
                $wheres->orWhere($column, 'LIKE', $value);
            }
        });
        return $query;
    }

    protected function applyFilters(Builder $query) {
        foreach ($this->filters as $filterKey => $filterValue) {
            $filterMethod = "filter" . \Illuminate\Support\Str::studly($filterKey);
            if (method_exists($this, $filterMethod)) {
                $query = call_user_func_array([$this, $filterMethod], [$query, $filterValue]);
            } else {
                $query->where($filterKey, $filterValue);
            }
        }
        return $query;
    }

    protected function filterDeleted(Builder $query, $value) {
        if ($value == 'include') {

        } else if ($value == 'only') {
            $query->where("{$this->primaryTable}.deleted_at", '!=', DB::raw(0));
        } else {
            $query->where("{$this->primaryTable}.deleted_at", '=', DB::raw(0));
        }
        return $query;
    }

    protected function filterVisible(Builder $query, $value) {
        if ($value == 'visible') {
            $query->where("{$this->primaryTable}.is_visible", '=', 1);
        } else if ($value == 'hidden') {
            $query->where("{$this->primaryTable}.is_visible", '=', 0);
        }
        return $query;
    }

    protected function applyFixedFilters(Builder $query) {
        foreach ($this->fixedFilters as $filterKey => $filterValue) {
            $filterMethod = "filter" . \Illuminate\Support\Str::studly($filterKey);
            if (method_exists($this, $filterMethod)) {
                $query = call_user_func_array([$this, $filterMethod], [$query, $filterValue]);
            } else {
                $query->where($filterKey, $filterValue);
            }
        }
        return $query;
    }

    protected function buildInformationQuery(Builder $query) {
        return $query;
    }

    protected function populateFilters($additionalFilters = []) {
        if (!(count($this->expectedFilters))) return;
        $columnMap = $this->baseColumnMap();
        $search = $this->input->get('search', false);
        $this->filters = collect($this->input->only($this->expectedFilters))->filter(function ($value) {
            return strlen(trim($value));
        });
        if (!$this->filters->has('deleted')) $this->filters->put('deleted', 'no');
        foreach ($this->input as $key => $value) {
//            if ($key == 'deleted') $key = 'fixed_deleted';
            if (!\Illuminate\Support\Str::startsWith($key, 'fixed_')) continue;
            $actualKey = str_replace('fixed_', '', $key);
            if (!in_array($actualKey, $this->expectedFilters)) continue;
            if (is_array($value)) {
                $array = [];
                foreach ($value as $index => $item) {
                    $item = trim($item);
                    if (!strlen($item)) continue;
                    $array[$index] = $item;
                    $this->fixedFilters->put($actualKey, $array);
                }
            } else {
                if (!strlen(trim($value))) continue;
                $this->fixedFilters->put($actualKey, $value);
            }
        }
        foreach ($additionalFilters as $key=>$value) $this->filters->put($key, $value);
        if (strlen($search['value'])) {
            $searchArray = [];
            foreach ($this->input->get('columns', []) as $column) {
                if ($column['searchable'] === 'true') {
                    if (\Illuminate\Support\Arr::has($columnMap, $column['data'])) {
                        $searchArray[$columnMap[$column['data']]] = "%$search[value]%";
                    }
                }
            }
            $this->filters->put('search', $searchArray);
        }
    }

    private function groupByColumn() {
        return $this->primaryTable . '.' . $this->groupByColumn;
    }

    private function baseColumnMap() {
        $map = $this->columnMap();
        $map['deleted_at'] = "{$this->primaryTable}.deleted_at";
        return $map;
    }

    protected function columnMap() {
        return [];
    }

}
