<?php

namespace App;

use Auth;
use DateTime;
use DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\RedirectResponse;
use Lang;
use Illuminate\Http\Request;
use Session;

/**
 * Class Entity
 * @package App
 */
class Entity extends Model
{

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'parent_entity_id',
        'entity_type_id',
        'entity_class_id',
        'location_id',
        'manufacturer_id',
        'supplier_id',
        'state_id'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        //
    ];

    /**
     * Returns prepared query, with which you can specify where clause, orderBy, or even another join if you wish,
     * but that should not be necessary.
     *
     * This method returns [$columns, $query] list.
     *
     * The query has already build up all the needed stuff to retrieve:
     *  - all entities' parameters, acting as table column
     *  - all entity type's parameters, acting as table column
     *  - all parent's id & class_id & class_name, acting as table column, each
     *  - all children's id & class_id & class_name (this is in JSON array string, you should perform json_decode)
     *  - manufacturer, supplier & location values if entity class told us this class have any of those
     *   - if the entity itself does not have a manufacturer, then it is entity type's manufacturer, who is returned
     *
     * To know which columns are about to be returned by that query, is specified in $columns array.
     *
     * @param $entityClass
     * @param $entityClassType
     * @param $hasParameters
     * @return array [$columns, $query]
     */
    public static function getEntitiesQuery($entityClass = null, $entityClassType = null, $hasParameters = true)
    {
        // Prepare common query
        $query = DB::table('entities as d')
            ->join('entity_classes as dc', 'd.entity_class_id', '=', 'dc.id')
            ->join('states as s', 'd.state_id', '=', 's.id')
            ->leftJoin('state_transitions as st', 'd.state_id', '=', 'st.from_state_id')
            ->leftJoin('entities as dch', 'd.id', '=', ($entityClass !== null && $entityClass->is_type ? 'dch.entity_type_id' : 'dch.parent_entity_id'))
            ->leftJoin('entity_classes as dchc', 'dch.entity_class_id', '=', 'dchc.id')
            ->groupBy('d.id');

        if ($entityClassType !== null) {
            // Add possible type
            $query = $query->leftJoin('entities as dt', 'd.entity_type_id', '=', 'dt.id');
        }

        // With common selected columns
        $selectArray = [
            'd.id as id',
            's.name as state',
            'd.state_id as state_id',
            'd.entity_type_id as type_id',
            'd.parent_entity_id as parent_entity_id',
            'd.location_id as location_id',
            'd.supplier_id as supplier_id',
            'd.updated_at as updated_at',
            DB::raw("CONCAT( '[',
                    GROUP_CONCAT( DISTINCT
                        '{',
                            '\"id\":\"',
                                `dch`.`id`, '\",',
                            '\"class_id\":\"',
                                `dchc`.`id`, '\",',
                            '\"class\":\"',
                                `dchc`.`name`, '\"',
                        '}'
                    SEPARATOR ', ') ,
                ']') as children_ids"),
            DB::raw("CONCAT( '[',
                    GROUP_CONCAT( DISTINCT
                        '{',
                            '\"to_state_id\":',
                                `st`.`to_state_id`, ',',
                            '\"permission_id\":',
                                `st`.`permission_id`,
                        '}'
                    SEPARATOR ', ') ,
                ']') as possible_states")
        ];

        // And start building columns list for view
        $columns = [
            'id',
            'state'
        ];

        if ($entityClassType !== null && $entityClass !== null) {
            $selectArray[] = DB::raw('COALESCE(`d`.`manufacturer_id`, `dt`.`manufacturer_id`) as `manufacturer_id`');
        } else {
            $selectArray[] = 'd.manufacturer_id as manufacturer_id';
        }

        if ($entityClass !== null) {

            if ($hasParameters) {
                if ($entityClassType !== null)
                    $allParams = array_merge($entityClass->parameters, $entityClassType->parameters);
                else
                    $allParams = $entityClass->parameters;

                foreach ($allParams as $parameter) {
                    $newParam = $parameter['name'];

                    // Skip duplicates
                    if (array_search($newParam, $columns) === false) {
                        $columns[] = $newParam;
                    }
                }
            }

            // Non-type manufacturer has precedence ..
            if ($entityClass->has_manufacturer) {
                $query = $query->leftJoin('manufacturers as m', 'd.manufacturer_id', '=', 'm.id');

                $columns[] = 'manufacturer';
                $selectArray[] = 'm.name as manufacturer';
            } else if ($entityClassType !== null && $entityClassType->has_manufacturer) {
                $query = $query->leftJoin('manufacturers as m', 'dt.manufacturer_id', '=', 'm.id');

                $columns[] = 'manufacturer';
                $selectArray[] = 'm.name as manufacturer';
            }

            if ($entityClass->has_location) {
                $query = $query->leftJoin('locations as l', 'd.location_id', '=', 'l.id');

                $columns[] = 'location';
                $selectArray[] = 'l.name as location';
            }

            if ($entityClass->has_supplier) {
                $query = $query->leftJoin('suppliers as sup', 'd.supplier_id', '=', 'sup.id');

                $columns[] = 'supplier';
                $selectArray[] = 'sup.name as supplier';
            }

            if ($hasParameters) {

                if (count($entityClass->parameters) > 0) {
                    $query = $query->leftJoin('parameters as p', 'd.id', '=', 'p.entity_id')
                        ->leftJoin('class_parameters as dcp', 'p.entity_class_parameter_id', '=', 'dcp.id');


                    foreach ($entityClass->parameters as $parameter) {
                        $paramName = $parameter['name'];
                        $paramType = $parameter['type'];
                        $groupConcat = "GROUP_CONCAT( DISTINCT CASE WHEN `dcp`.`parameter_name` = '$paramName' THEN `p`.`$paramType` END)";

                        // For proper sorting we need to cast group_concat output to integer
                        if ($paramType === EntityParameter::INT || $paramType === EntityParameter::TINY_INT)
                            $selectArray[] =
                                DB::raw("CAST($groupConcat AS signed) as `$paramName`");
                        else
                            $selectArray[] =
                                DB::raw("$groupConcat as `$paramName`");
                    }
                }

                $typeHasParams = $entityClassType !== null && count($entityClassType->parameters) > 0;

                if ($typeHasParams) {
                    $query = $query->leftJoin('parameters as dtp', 'dt.id', '=', 'dtp.entity_id')
                        ->leftJoin('class_parameters as dtcp', 'dtp.entity_class_parameter_id', '=', 'dtcp.id');

                    foreach ($entityClassType->parameters as $parameter) {
                        $paramName = $parameter['name'];
                        $paramType = $parameter['type'];

                        $groupConcat = "GROUP_CONCAT( DISTINCT CASE WHEN `dtcp`.`parameter_name` = '$paramName' THEN `dtp`.`$paramType` END)";

                        // For proper sorting we need to cast group_concat output to integer (except big int - MySQL cannot cast to it)
                        if ($paramType === EntityParameter::INT || $paramType === EntityParameter::TINY_INT)
                            $selectArray[] =
                                DB::raw("CAST($groupConcat AS signed) as `$paramName`");
                        else
                            $selectArray[] =
                                DB::raw("$groupConcat as `$paramName`");
                    }
                }
            }
            // Types cannot have parents ..
            if (!$entityClass->is_type) {
                $query = $query->leftJoin('entities as dp', 'd.parent_entity_id', '=', 'dp.id')
                    ->leftJoin('entity_classes as dpc', 'dp.entity_class_id', '=', 'dpc.id');

                $selectArray[] = 'dpc.name as parent_class';
                $selectArray[] = 'dpc.id as parent_class_id';
                $selectArray[] = 'dp.id as parent_id';

                $columns[] = 'parent_id';
            }
        }

        $columns[] = 'children_ids';

        $query = $query->select($selectArray);

        return [$columns, $query];
    }

    /**
     * @param $entityClass
     * @param $entityClassType
     * @param $request
     * @return array
     */
    private static function parseFilters($entityClass, $entityClassType, Request $request)
    {
        $filters = [];
        foreach ($entityClass->parameters as $parameter) {
            $filterThis = $request->get($parameter['name'], null);

            if ($filterThis !== null) {
                $filters[$parameter['name']] = $filterThis;
            }
        }

        if ($entityClassType !== null) {
            foreach ($entityClassType->parameters as $parameter) {
                $filterThis = $request->get($parameter['name'], null);

                if ($filterThis !== null) {
                    $filters[$parameter['name']] = $filterThis;
                }
            }
        }

        $checkAlsoThese = ['state', 'manufacturer', 'location', 'supplier', 'parent_id', 'type_id'];

        foreach ($checkAlsoThese as $checkAlsoThis) {
            $filterThis = $request->get($checkAlsoThis, null);

            if ($filterThis !== null) {
                $filters[$checkAlsoThis] = $filterThis;
            }
        }
        return array_map(function ($value) {
            return str_replace('*', '%', $value);
        }, $filters);
    }

    /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function parameters()
    {
        return $this->hasMany('App\EntityParameter');
    }

    /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function clazz()
    {
        return $this->belongsTo('App\EntityClassInitialSetup', 'entity_class_id');
    }

    /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function manufacturer()
    {
        return $this->belongsTo('App\Manufacturer');
    }

    /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function supplier()
    {
        return $this->belongsTo('App\Supplier');
    }

    /**
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function location()
    {
        return $this->belongsTo('App\Location');
    }

    /**
     * @param $entityClass
     * @param $entityClassType
     * @param $sort
     * @param $ordering
     * @param Request $request
     * @return array [ $columns, $entities ]
     */
    public static function getEntitiesWithEntityClass($entityClass, $entityClassType, $sort, $ordering, Request $request)
    {
        // Prepare common query
        list($columns, $query) = self::getEntitiesQuery($entityClass, $entityClassType);

        $filters = self::parseFilters($entityClass, $entityClassType, $request);

        $simplePaginate = false;
        foreach ($filters as $key => $value) {
            $query = $query->having($key, 'LIKE', $value);
            $simplePaginate = true;
        }

        $where = [
            ['dc.id', '=', $entityClass->id]
        ];


        // Finally, query the database with limiting $count
        $query = $query
            ->where($where)
            ->orderBy($sort, $ordering)
            ->orderBy('id', 'desc');

        $count = intval($request->get('count', 10));

        DB::statement('SET group_concat_max_len = 10000000');

        if ($count === null)
            $entities = $query->get();
        elseif ($simplePaginate)
            $entities = $query->simplePaginate($count);
        else
            $entities = $query->paginate($count);

        foreach ($entities as $entity) {
            if ($entity->children_ids !== null) {
                $entity->children_ids = json_decode($entity->children_ids, true);
            } else {
                $entity->children_ids = [];
            }

            $classCounter = [];
            foreach ($entity->children_ids as $children_id) {
                if (!isset($classCounter[$children_id['class_id']])) {
                    $classCounter[$children_id['class_id']] = [
                        'entities_count' => 1,
                        'class_name' => $children_id['class'],
                        'class_id' => $children_id['class_id']
                    ];
                } else {
                    ++$classCounter[$children_id['class_id']]['entities_count'];
                }
            }
            $entity->children_ids = $classCounter;
        }

        // Last column will be updated_at ..
        $columns[] = 'updated_at';

        return [$columns, $entities];
    }

    /**
     * @param $entityClass
     * @param $entityClassType
     * @param $entityId
     * @return array [ $columns, $entity ]
     */
    public static function getEntityWithEntityClass($entityClass, $entityClassType, $entityId)
    {
        // Prepare common query
        list($columns, $query) = self::getEntitiesQuery($entityClass, $entityClassType);

        DB::statement('SET group_concat_max_len = 10000000');

        $entity = $query
            ->where([
                ['dc.id', '=', $entityClass->id],
                ['d.id', '=', $entityId]
            ])
            ->first();

        if (!$entity)
            return [$columns, null];

        if ($entity->children_ids !== null) {
            $entity->children_ids = json_decode($entity->children_ids, true);
        } else {
            $entity->children_ids = [];
        }

        /*
         *  Parse possible state transitions respecting user's permissions
         */
        $possibleStates = [$entity->state_id];

        if ($entity->possible_states !== null) {
            $entity->possible_states = json_decode($entity->possible_states, true);

            foreach ($entity->possible_states as $stateTransition) {
                if (Auth::getUser()->canById($stateTransition['permission_id'])) {
                    $possibleStates[] = $stateTransition['to_state_id'];
                }
            }
        }

        $possibleTranslatedStates = [];
        foreach ($possibleStates as $possibleState) {
            $possibleTranslatedStates[$possibleState] = State::VALID_STATES[$possibleState - 1];
        }

        $entity->possible_states = $possibleTranslatedStates;

        /*
         * Parse children entities
         */

        $classCounter = [];
        foreach ($entity->children_ids as $children_id) {
            if (!isset($classCounter[$children_id['class_id']])) {
                $classCounter[$children_id['class_id']] = [
                    'entities_count' => 1,
                    'class_name' => $children_id['class'],
                    'class_id' => $children_id['class_id']
                ];
            } else {
                ++$classCounter[$children_id['class_id']]['entities_count'];
            }
        }
        $entity->children_ids = $classCounter;

        // Last column will be updated_at ..
        $columns[] = 'updated_at';

        return [$columns, $entity];
    }

    /**
     * @param $entityClass
     * @param Request $request
     * @param $entityId
     * @param $isType
     * @return bool $redirectBack
     */
    public static function validateEntityChange(&$entityClass, Request $request, $entityId, $isType)
    {
        if ($entityClass instanceof RedirectResponse)
            return $entityClass;

        $redirectBack = false;

        if ($request->has('id')) {
            Session::flash('error', Lang::get('validation.id_change_forbidden'));
            $redirectBack = true;
        }

        /**
         * Validate state change
         */
        $desiredStateId = $request->get('state_id', false);
        if ($desiredStateId !== false && $entityId !== null) {
            $desiredStateId = intval($desiredStateId);

            if (!$desiredStateId) {
                Session::flash('error', Lang::get('validation.required', ['attribute' => 'State']));
                $redirectBack = true;
            } else {
                $validationPassed = false;
                $possibleStates = DB::table('entities as d')
                    ->join('states as s', 'd.state_id', '=', 's.id')
                    ->leftJoin('state_transitions as st', 's.id', '=', 'st.from_state_id')
                    ->select([
                        'd.state_id as from_state_id',
                        'st.to_state_id as to_state_id',
                        'st.permission_id as permission_id',
                    ])->where([
                        ['d.id', '=', $entityId]
                    ])->get();

                if ($possibleStates !== null) {

                    // Find the final state from all possible final states
                    foreach ($possibleStates as $possibleState) {

                        if ($possibleState->from_state_id === $desiredStateId) {
                            // Stop validating, no change made ..
                            $validationPassed = true;
                            break;
                        }
                        $requiredPermissionId = $possibleState->permission_id;
                        $possibleState = $possibleState->to_state_id;

                        if ($possibleState === $desiredStateId) {
                            // Found the one we were looking for!

                            $validationPassed = Auth::getUser()->canById($requiredPermissionId);
                            break;
                        }
                    }
                    if (!$validationPassed) {
                        Session::flash('error', Lang::get('You are not allowed to perform this state transition! Stop hacking this system!'));
                        $redirectBack = true;
                    }
                } else {
                    Session::flash('error', Lang::get('Unknown entity id !'));
                    $redirectBack = true;

                    return $redirectBack;
                }
            }
        }


        // Check that type is specified
        if (!$isType && $entityClass->type_id !== null) {
            $specifiedEntityTypeId = $request->get('type_id', null);
            if ($specifiedEntityTypeId === null) {
                Session::flash('error', Lang::getFromJson('No entity type id given. Stop hacking this system!'));
                $redirectBack = true;
            } elseif (!is_numeric($specifiedEntityTypeId)) {
                Session::flash('error', 'Id must be numeric! Stop hacking this system!');
                $redirectBack = true;
            } else {

                list ($_, $query) = self::getEntitiesQuery();

                $entityType = $query
                    ->where([
                        ['dc.id', '=', $entityClass->type_id],
                        ['d.id', '=', $specifiedEntityTypeId]
                    ])
                    ->first();

                if (!$entityType) {
                    Session::flash('error', Lang::getFromJson('Unknown Id for this Entity Type'));
                    $redirectBack = true;
                }
            }
        }

        $specifiedParentId = $request->get('parent_entity_id', null);

        // Check that type is specified
        if (!$isType && ($specifiedParentId !== null || $entityClass->require_parent)) {
            $specifiedParentEntityClassId = $request->get('parent_entity_class_id', null);
            if ($specifiedParentEntityClassId === null) {
                Session::flash('error', Lang::getFromJson('No parent entity class id given. Stop hacking this system!'));
                $redirectBack = true;
            } elseif ($specifiedParentId === null) {
                Session::flash('error', Lang::getFromJson('No parent entity id given. Stop hacking this system!'));
                $redirectBack = true;
            } elseif (!is_numeric($specifiedParentId) || !is_numeric($specifiedParentEntityClassId)) {
                Session::flash('error', 'Id must be numeric! Stop hacking this system!');
                $redirectBack = true;
            } else {

                $specifiedParentEntityClassId = intval($specifiedParentEntityClassId);
                if (!isset($entityClass->parents[$specifiedParentEntityClassId])) {
                    Session::flash('error', 'This entity does not accept this entity class id! Stop hacking this system!');
                    $redirectBack = true;
                } else {

                    list ($_, $query) = self::getEntitiesQuery();

                    $parentEntity = $query
                        ->where([
                            ['dc.id', '=', $specifiedParentEntityClassId],
                            ['d.id', '=', $specifiedParentId]
                        ])
                        ->first();

                    if (!$parentEntity) {
                        Session::flash('error', Lang::getFromJson('Unknown Id for specified Parent Entity Class'));
                        $redirectBack = true;
                    }
                }
            }
        }

        // Check for validity of each parameter
        foreach ($entityClass->parameters as $parameter) {
            $providedParamValue = $request->get('param_' . $parameter['name'], null);

            if ($parameter['required'] && ($providedParamValue === null || trim($providedParamValue) === '')) {
                Session::flash('error', Lang::get('validation.required', ['attribute' => $parameter['name']]));
                $redirectBack = true;
            }
            if ($providedParamValue !== null)
                switch ($parameter['type']) {
                    case EntityParameter::STRING:
                        $providedParamValue = trim($providedParamValue);
                        $valueLength = strlen($providedParamValue);

                        $max = ($parameter['max'] === null || $parameter['max'] > EntityParameter::STRING_DB_LEN) ?
                            EntityParameter::STRING_DB_LEN :
                            $parameter['max'];

                        if ($valueLength > $max) {
                            Session::flash('error', Lang::get('validation.max.string', [
                                'max' => $max,
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }

                        if ($parameter['unique'] && !EntityParameter::isUnique($entityId, $parameter['id'], $parameter['type'], $providedParamValue)) {
                            Session::flash('error', Lang::get('validation.unique', [
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }
                        break;
                    /** @noinspection PhpMissingBreakStatementInspection */
                    case EntityParameter::TINY_INT:
                        $max = ($parameter['max'] === null || $parameter['max'] > 127) ? 127 : $parameter['max'];

                    case EntityParameter::INT:
                        $max = ($parameter['max'] === null || $parameter['max'] > 2147483647) ? 2147483647 : $parameter['max'];

                        if (!is_numeric($providedParamValue)) {
                            Session::flash('error', Lang::get('validation.numeric', [
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }

                        $providedParamValue = intval($providedParamValue);

                        if ($providedParamValue > $max) {
                            Session::flash('error', Lang::get('validation.max.numeric', [
                                'max' => $max,
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }

                        if ($parameter['unique'] && !EntityParameter::isUnique($entityId, $parameter['id'], $parameter['type'], $providedParamValue)) {
                            Session::flash('error', Lang::get('validation.unique', [
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }

                        break;
                    case EntityParameter::DATETIME:

                        $pattern = '/^\\d{4}-\\d{2}-\\d{2}$/';
                        if (!preg_match($pattern, $providedParamValue)) {
                            Session::flash('error', Lang::get('validation.date_format', [
                                'format' => 'YYYY-MM-DD',
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        } elseif (DateTime::createFromFormat('Y-m-d', $providedParamValue) === false) {
                            Session::flash('error', Lang::get('validation.date', [
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }

                        if ($parameter['unique'] && !EntityParameter::isUnique($entityId, $parameter['id'], $parameter['type'], $providedParamValue)) {
                            Session::flash('error', Lang::get('validation.unique', [
                                'attribute' => $parameter['name']
                            ]));
                            $redirectBack = true;
                        }

                        break;
                    case EntityParameter::BLOB:

                        // Doesn't have any validation just yet ..
                        break;
                }

        }

        // Both entity type & entity can have manufacturer ..
        $entityClass->new_manufacturer_id = null;
        $entityClass->new_supplier_id = null;
        $entityClass->new_location_id = null;

        if ($entityClass->has_manufacturer) {
            $manufacturerSpecified = $request->get('manufacturer', null);
            if ($manufacturerSpecified !== null) {
                $found = DB::table('manufacturers')
                    ->select('id')
                    ->where('name', '=', $manufacturerSpecified)
                    ->first();

                if (!$found) {
                    Session::flash('error', Lang::get('manufacturer.not_found', ['name' => $manufacturerSpecified]));
                    $redirectBack = true;
                } else {
                    $entityClass->new_manufacturer_id = $found->id;
                }
            }
        }

        // Only entity can have supplier & location ..
        if (!$isType) {

            if ($entityClass->has_supplier) {
                $supplierSpecified = $request->get('supplier', null);
                if ($supplierSpecified !== null) {
                    $found = DB::table('suppliers')
                        ->select('id')
                        ->where('name', '=', $supplierSpecified)
                        ->first();

                    if (!$found) {
                        Session::flash('error', Lang::get('supplier.not_found', ['name' => $supplierSpecified]));
                        $redirectBack = true;
                    } else {
                        $entityClass->new_supplier_id = $found->id;
                    }
                }
            }

            if ($entityClass->has_location) {
                $locationSpecified = $request->get('location', null);
                if ($locationSpecified !== null) {
                    $found = DB::table('locations')
                        ->select('id')
                        ->where('name', '=', $locationSpecified)
                        ->first();

                    if (!$found) {
                        Session::flash('error', Lang::get('location.not_found', ['name' => $locationSpecified]));
                        $redirectBack = true;
                    } else {
                        $entityClass->new_location_id = $found->id;
                    }
                }
            }

        }
        return $redirectBack;
    }
}