<?php

namespace Ignite\Core\Models\Form;

use Exception;
use Illuminate\Support\Arr;

class CoreForm extends Form
{
    use GroupByTableTrait;

    /**
     * The schema resolver.
     *
     * @var ThemeSchemaResolver
     */
    protected $schemaResolver;

    /**
     * The mapping of field names to their index in the schema.
     *
     * @var array
     */
    protected $schemaFieldIndexes = [];

    /**
     * Form constructor.
     *
     * @param ThemeSchemaResolver $schemaResolver
     */
    public function __construct(ThemeSchemaResolver $schemaResolver)
    {
        $this->schemaResolver = $schemaResolver;
    }

    /**
     * Build the form from the JSON schema file.
     *
     * @param string $type
     * @param bool $renderGroups
     *
     * @throws Exception
     */
    protected function buildFromSchema($type, bool $renderGroups = false)
    {
        $this->setFormOption('class', 'form-horizontal');

        $schema = $this->getSchema($type);

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

            $this->renderField($field);
        }

        if (method_exists($this, 'renderGroups') && $renderGroups) {
            $this->renderGroups($schema);
        }
    }

    /**
     * Get the schema from the JSON file.
     *
     * Note you can overwrite schemaAlter() if you want to change the schema based on certain conditions.
     *
     * @param string $type
     * @return array
     * @throws Exception
     */
    protected function getSchema(string $type)
    {
        $schema = $this->schemaResolver->resolve($type);
        $this->schemaIndexFields($schema);

        return $this->schemaAlter($schema);
    }

    /**
     * @param  string $name
     * @param  array  $options
     * @param  array  &$schema
     */
    protected function schemaAddField(
        string $name,
        array $options,
        array &$schema
    ): CoreForm
    {
        $options['name'] = $name;
        $schema['fields'][] = $options;
        $this->schemaIndexFields($schema);

        return $this;
    }

    /**
     * Alters the schema before building with it.
     *
     * You can overwrite this to change the schema on conditions before returning
     * from getSchema().
     *
     * Note the helper functions you can use:
     *   $this->schemaAddField()
     *   $this->schemaGetFieldOptions()
     *   $this->schemaOnlyFields()
     *   $this->schemaRemoveFields()
     *   $this->schemaSetFieldOption()
     *   $this->schemaSetFieldOptions()
     *
     * @param  array  $schema
     */
    protected function schemaAlter(array $schema): array
    {
        if ($this->isViewingBackend()) {
            $this->schemsReplaceWithBackendRules($schema);
        }

        return $schema;
    }

    /**
     * @param  string $field
     * @param  array  $schema
     * @return null|array
     */
    protected function schemaGetFieldOptions(string $field, array $schema): ?array
    {
        $index = $this->schemaFieldIndexes[$field];

        return $schema['fields'][$index];
    }

    /**
     * @param  array  $schema
     */
    protected function schemaIndexFields(array $schema): CoreForm
    {
        $this->schemaFieldIndexes = [];
        foreach ($schema['fields'] as $key => $field) {
            $this->schemaFieldIndexes[$field['name']] = $key;
        }

        return $this;
    }

    /**
     * @param  array  $fields
     * @param  array  &$schema
     * @param  array  $map
     */
    protected function schemaOnlyFields(
        array $fields,
        array &$schema
    ): CoreForm
    {
        $newFields = [];

        foreach ($fields as $field) {
            $index = $this->schemaFieldIndexes[$field];
            $newFields[] = $schema['fields'][$index];
        }

        $schema['fields'] = $newFields;
        $this->schemaIndexFields($schema);

        return $this;
    }

    /**
     * @param  array  $fields
     * @param  array  &$schema
     * @param  array  $map
     */
    protected function schemaRemoveFields(
        array $fields,
        array &$schema
    ): CoreForm
    {
        foreach ($fields as $field) {
            $index = $this->schemaFieldIndexes[$field];
            unset($schema['fields'][$index]);
            unset($this->schemaFieldIndexes[$field]);
        }

        return $this;
    }

    /**
     * @param string $field
     * @param string $options
     * @param array  $schema
     */
    protected function schemaSetFieldOption(
        string $field,
        string $option,
        $value,
        array &$schema
    ): CoreForm
    {
        $index = $this->schemaFieldIndexes[$field];
        Arr::set($schema['fields'][$index], $option, $value);

        return $this;
    }

    /**
     * @param string $field
     * @param string $options
     * @param array  $schema
     */
    protected function schemaSetFieldOptions(
        string $field,
        array $options,
        array &$schema
    ): CoreForm
    {
        if (!isset($this->schemaFieldIndexes[$field])) {
            return null;
        }

        $index = $this->schemaFieldIndexes[$field];
        $schema['fields'][$index] = $options;

        return $this;
    }

    /**
     * Replace 'rules' with 'rules_backend' if it exists.
     * @param array  $schema
     */
    protected function schemsReplaceWithBackendRules(array &$schema): CoreForm
    {
        foreach ($schema['fields'] as $fieldInfo) {
            $field = $fieldInfo['name'];
            $options = $this->schemaGetFieldOptions($field, $schema);
            if (isset($options['rules_backend'])) {
                $options['rules'] = $options['rules_backend'];
                unset($options['rules_backend']);
                $this->schemaSetFieldOptions($field, $options, $schema);
            }
        }

        return $this;
    }

    /**
     * Determine whether the given form schema should group fields by their assigned table.
     *
     * @param array $field
     *
     * @return bool
     */
    protected function shouldGroupByTable(array $field)
    {
        return Arr::has($field, 'group') &&
            method_exists($this, 'extractGroup') &&
            property_exists($this, 'groupByTable') &&
            $this->groupByTable;
    }
}
