<?php

namespace Ignite\Core\Models\Form;

use Ignite\Theme\Manager;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Kris\LaravelFormBuilder\Fields\FormField;
use Closure;

class Form extends \Kris\LaravelFormBuilder\Form
{
    /**
     * @var Manager
     */
    protected $themeManager;

    /**
     * The fields that will be grouped separately.
     * @var array
     */
    protected $groups = [];

    /**
     * Should the values be grouped by table.
     * @var bool
     */
    protected $groupByTable = false;

    /**
     * Form constructor.
     *
     * @param Manager $themeManager
     */
    public function __construct(Manager $themeManager)
    {
        $this->themeManager = $themeManager;

        $this->setFormOption('class', 'form-horizontal');
    }

    /**
     * Build the form from the JSON schema file.
     *
     * @param  string $form
     * @param bool $renderGroups
     *
     * @throws \Exception
     */
    protected function buildFromSchema($form, $renderGroups = true)
    {
        $schema = $this->getSchema($form);

        foreach ($schema['fields'] as $field) {
            if (array_has($field, 'group')) {
                $this->extractGroup($field);
                continue;
            }

            $this->renderField($field);
        }

        if ($renderGroups) {
            $this->renderGroups($schema);
        }
    }

    /**
     * Get the schema from the JSON file.
     *
     * @param  string $form
     * @return array
     * @throws \Exception
     */
    protected function getSchema($form)
    {
        $themePath = $this->getActiveTheme()->path();
        $path = $themePath . DIRECTORY_SEPARATOR . 'forms' . DIRECTORY_SEPARATOR . $form . '.json';
        $json = file_get_contents($path);
        $data = json_decode($json, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \Exception("Unable to load schema for `$form` form. Error: " . json_last_error_msg());
        }

        return $data;
    }

    /**
     * The active theme.
     *
     * @return \Ignite\Theme\Theme
     */
    protected function getActiveTheme()
    {
        return $this->themeManager->current();
    }

    /**
     * Prepare the field data for the form.
     *
     * @param  array $field
     * @return array
     * @throws \Exception
     */
    protected function prepareField($field)
    {
        $name = Arr::get($field, 'name');

        if (array_has($field, 'choices')) {
            $field['choices'] = $this->convertChoices($field['choices']);

            if (! is_null($this->model) && isset($this->model[$name])) {
                $field['selected'] = $this->model[$name];
            }
        }

        $field['label_attr'] = ['class' => 'control-label col-sm-3'];
        $field['form_horizontal'] = true;

        if (array_has($field, 'sensitive')) {
            $field['value'] = $this->getEncryptValueClosure();
        }

        return $field;
    }

    /**
     * The closure to pass when encrypting form value for a sensitive field.
     *
     * @return Closure
     */
    public function getEncryptValueClosure()
    {
        return Closure::fromCallable([$this, 'encryptValue']);
    }

    /**
     * Encrypt a value before displaying it in the form.
     *
     * @param string $value
     * @param string $default
     * @param int $characterCount
     * @return string
     */
    public function encryptValue($value, $default = '', $characterCount = 8)
    {
        $value = trim($value);

        if (empty($value)) {
            return $default;
        }

        try {
            $value = decrypt($value);
            return str_repeat('*', strlen($value));
        } catch (DecryptException $e) {
            return str_repeat('*', $characterCount);
        }
    }

    /**
     * Render the field.
     *
     * @param  array       $field
     * @param  string|null $after
     * @return self
     * @throws \Exception
     */
    protected function renderField($field, $after = null)
    {
        if (! empty($after)) {
            $this->addAfter($after, $field['name'], $field['frontend_type'], $this->prepareField($field));
        } else {
            $this->add($field['name'], $field['frontend_type'], $this->prepareField($field));
        }

        return $this;
    }

    /**
     * Convert the provided choice sources.
     *
     * @param  array|string $options
     * @return array
     * @throws \Exception
     */
    protected function convertChoices($options)
    {
        if (is_array($options)) {
            return $options;
        }

        $options = trim($options);

        if (! preg_match('/^{([0-9a-zA-Z_]+)}$/', $options)) {
            $split = preg_split("/[|,\n]/", $options, -1, PREG_SPLIT_NO_EMPTY);

            if (! is_array($split)) {
                return [$split];
            }

            return $split;
        }

        $variable = str_replace(['{', '}'], '', $options);
        $sources = config('sources');

        if (! isset($sources[$variable])) {
            throw new \Exception("Unknown data source `{$variable}`. Please update the sources.php config file.");
        }

        $source = app($sources[$variable]);

        return $source->toDropdown();
    }

    /**
     * Extract the group and add it to the local cache.
     *
     * @param array $field
     */
    protected function extractGroup(array $field)
    {
        if (! array_key_exists('name', $field)) {
            return;
        }

        $name = $field['name'];
        $group = Arr::pull($field, 'group');

        if (! isset($this->groups[$group])) {
            $this->groups[$group] = [];
        }

        $this->groups[$group][$name] = $field;
    }

    /**
     * Render groups.
     *
     * @param  array $schema
     * @throws \Exception
     */
    protected function renderGroups($schema)
    {
        $backendGroups = ['internal', 'admin', 'account'];

        $groups = Arr::get($schema, 'groups', []);

        foreach ($this->groups as $group => $fields) {
            if (in_array($group, $backendGroups)) {
                if ($this->isViewingBackend()) {
                    $this->renderGroup($group, $fields, $groups);
                    continue;
                }
            } else {
                $this->renderGroup($group, $fields, $groups);
            }
        }
    }

    /**
     * Render a group.
     *
     * @param string $group
     * @param array  $fields
     * @throws \Exception
     */
    protected function renderGroup($group, $fields, $groups)
    {
        $groupName = "group_$group";
        $meta = Arr::get($groups, $group, []);

        $this->addBefore('submit', $groupName, 'static', [
            'label_attr' => ['class' => 'group-heading'],
            'wrapper' => ['class' => 'group-container'],
            'tag' => 'div',
            'attr' => [
                'class' => 'form-control-static' . array_has($meta, 'value') ? '' : ' hidden'
            ],
            'label' => Arr::get($meta, 'label', ucwords($group)),
            'value' => Arr::get($meta, 'value', '')
        ]);

        $last = $groupName;

        foreach ($fields as $name => $field) {
            $this->renderField($field, $last);
            $last = $name;
        }
    }

    /**
     * Determine if the user is viewing the form via the backend.
     *
     * @return bool
     */
    protected function isViewingBackend()
    {
        return Str::contains(url()->current(), 'admin');
    }

    /**
     * Validate the form.
     *
     * @param array $validationRules
     * @param array $messages
     * @return \Illuminate\Contracts\Validation\Validator
     */
    public function validate($validationRules = [], $messages = [])
    {
        /** @var FormField $field */
        foreach ($this->fields as $name => $field) {
            if ($rules = $field->getValidationRules()->getRules()) {
                if (is_array($rules) && is_array($rules[$name])) {
                    foreach ($rules[$name] as $index => $rule) {
                        $validationRules[$name][$index] = $this->convertRule($name, $field, $rule);
                    }
                }
            }
        }

        return parent::validate($validationRules, $messages);
    }

    /**
     * Optionally mess with this form's $values before it's returned from getFieldValues().
     *
     * @param array $values
     * @return void
     */
    public function alterFieldValues(array &$values)
    {
        $values = $this->removeConfirmationFields($values);

        foreach ($values as $name => $value) {
            $field = $this->getField($name);

            if ($sensitive = $field->getOption('sensitive')) {
                if (Str::contains($value, '*')) {
                    unset($values[$name]);
                    continue;
                }

                $value = encrypt($value);
            }

            if ($readonly = $field->getOption('readonly')) {
                unset($values[$name]);
                continue;
            }

            $values[$name] = with(new Backend($value))->apply($field);
        }

        if (property_exists($this, 'groupByTable') && $this->groupByTable) {
            $this->groupByTable($values);
        }
    }

    /**
     * Group the values by their associated tables.
     *
     * @param  array $values
     * @return array
     */
    protected function groupByTable($values)
    {
        $grouped = [];
        foreach ($values as $name => $value) {
            $field = $this->getField($name);
            $table = $field->getOption('table');

            if (! isset($grouped[$table])) {
                $grouped[$table] = [];
            }

            $grouped[$table][$name] = $value;
        }

        return $grouped;
    }

    /**
     * Remove fields that only exist in the form to confirm other fields.
     *
     * @param  array $values
     * @return array
     */
    protected function removeConfirmationFields(array $values)
    {
        return collect($values)->filter(function ($value, $key) {
            return ! Str::contains($key, '_confirmation');
        })->toArray();
    }

    /**
     * Convert the rules.
     *
     * @param  string    $name
     * @param  FormField $field
     * @param  string    $rule
     * @return mixed
     */
    protected function convertRule($name, FormField $field, $rule)
    {
        if (Str::contains($rule, 'unique') && ($this->model instanceof \Illuminate\Database\Eloquent\Model)) {
            return $this->convertRuleUnique($name, $field);
        }

        return $rule;
    }

    /**
     * Convert 'unique' rule when a model exists.
     *
     * @param  string    $name
     * @param  FormField $field
     * @return \Illuminate\Validation\Rules\Unique
     */
    protected function convertRuleUnique($name, $field)
    {
        $table = $field->getOption('table');
        $rule = Rule::unique("core_$table", $name);
        $rule->ignore($this->model->getKey(), $this->model->getKeyName());

        return $rule;
    }
}
