<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\CustomFieldsBundle\CustomFields;

use Chill\CustomFieldsBundle\Entity\CustomField;
use Chill\CustomFieldsBundle\Form\DataTransformer\CustomFieldDataTransformer;
use Chill\CustomFieldsBundle\Form\Type\ChoicesListType;
use Chill\CustomFieldsBundle\Form\Type\ChoicesType;
use Chill\CustomFieldsBundle\Form\Type\ChoiceWithOtherType;
use Chill\MainBundle\Form\Type\TranslatableStringFormType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Twig\Environment;

class CustomFieldChoice extends AbstractCustomField
{
    final public const ALLOW_OTHER = 'other';

    final public const CHOICES = 'choices';

    final public const EXPANDED = 'expanded';

    final public const MULTIPLE = 'multiple';

    final public const OTHER_VALUE_LABEL = 'otherValueLabel';

    /**
     * CustomFieldChoice constructor.
     */
    public function __construct(
        private readonly Environment $templating,
        /**
         * @var TranslatableStringHelper Helper that find the string in current locale from an array of translation
         */
        private readonly TranslatableStringHelper $translatableStringHelper,
    ) {}

    public function allowOtherChoice(CustomField $cf)
    {
        return $cf->getOptions()[self::ALLOW_OTHER];
    }

    public function buildForm(FormBuilderInterface $builder, CustomField $customField)
    {
        // prepare choices
        $choices = [];
        $customFieldOptions = $customField->getOptions();

        foreach ($customFieldOptions[self::CHOICES] as $persistedChoices) {
            if ($persistedChoices['active']) {
                $choices[$persistedChoices['slug']] = $this->translatableStringHelper->localize($persistedChoices['name']);
            }
        }

        // prepare $options
        $options = [
            'multiple' => $customFieldOptions[self::MULTIPLE],
            'choices' => array_combine(array_values($choices), array_keys($choices)),
            'required' => $customField->isRequired(),
            'label' => $this->translatableStringHelper->localize($customField->getName()),
        ];

        // if allow_other = true
        if (true === $customFieldOptions[self::ALLOW_OTHER]) {
            $otherValueLabel = null;

            if (\array_key_exists(self::OTHER_VALUE_LABEL, $customFieldOptions)) {
                $otherValueLabel = $this->translatableStringHelper->localize(
                    $customFieldOptions[self::OTHER_VALUE_LABEL]
                );
            }

            $builder->add(
                $builder
                    ->create(
                        $customField->getSlug(),
                        ChoiceWithOtherType::class,
                        $options,
                        ['other_value_label' => $otherValueLabel]
                    )
                    ->addModelTransformer(new CustomFieldDataTransformer($this, $customField))
            );
        } else { // if allow_other = false
            // we add the 'expanded' to options
            $options['expanded'] = $customFieldOptions[self::EXPANDED];

            $builder->add(
                $builder->create($customField->getSlug(), ChoiceType::class, $options)
                    ->addModelTransformer(new CustomFieldDataTransformer($this, $customField))
            );
        }
    }

    public function buildOptionsForm(FormBuilderInterface $builder)
    {
        $builder
            ->add(self::MULTIPLE, ChoiceType::class, [
                'expanded' => true,
                'multiple' => false,
                'choices' => [
                    'Multiple' => '1',
                    'Unique' => '0', ],
                'empty_data' => '0',
                'label' => 'Multiplicity',
            ])
            ->add(self::EXPANDED, ChoiceType::class, [
                'expanded' => true,
                'multiple' => false,
                'choices' => [
                    'Expanded' => '1',
                    'Non expanded' => '0', ],
                'empty_data' => '0',
                'label' => 'Choice display',
            ])
            ->add(self::ALLOW_OTHER, ChoiceType::class, [
                'label' => 'Allow other',
                'choices' => [
                    'No' => '0',
                    'Yes' => '1', ],
                'empty_data' => '0',
                'expanded' => true,
                'multiple' => false,
            ])
            ->add(self::OTHER_VALUE_LABEL, TranslatableStringFormType::class, [
                'label' => 'Other value label (empty if use by default)', ])
            ->add(self::CHOICES, ChoicesType::class, [
                'entry_type' => ChoicesListType::class,
                'allow_add' => true,
            ]);

        return $builder;
    }

    public function deserialize($serialized, CustomField $customField)
    {
        // we always have to adapt to what the current data should be
        $options = $customField->getOptions();

        if ($options[self::MULTIPLE]) {
            return $this->deserializeToMultiple($serialized, $options[self::ALLOW_OTHER]);
        }

        return $this->deserializeToUnique($serialized, $options[self::ALLOW_OTHER]);

        return $serialized;
    }

    public function extractOtherValue(CustomField $cf, ?array $data = null)
    {
        return $data['_other'];
    }

    public function getChoices(CustomField $cf)
    {
        if ($cf->getOptions()[self::MULTIPLE]) {
            $choices = [];

            foreach ($cf->getOptions()[self::CHOICES] as $choice) {
                if (false === $choice['active']) {
                    continue;
                }
                $choices[$choice['slug']] = $this->translatableStringHelper
                    ->localize($choice['name']);
            }

            if ($this->allowOtherChoice($cf)) {
                $labels = $cf->getOptions()[self::OTHER_VALUE_LABEL];

                if (!\is_array($labels) || 0 === \count($labels)) {
                    $labels['back'] = 'other value';
                }
                $choices['_other'] = $this->translatableStringHelper
                    ->localize($labels);
            }

            return $choices;
        }

        return [
            $cf->getSlug() => $this->translatableStringHelper->localize($cf->getName()),
        ];
    }

    public function getName()
    {
        return 'Choices';
    }

    /**
     * Return true if the choice given in $choiceSlug is checked inside $data.
     *
     * Used in list exports.
     *
     * @param string       $choiceSlug the slug of the choice we want to know if it was checked
     * @param array|string $data       the data of the field
     *
     * @return bool
     */
    public function isChecked(CustomField $cf, $choiceSlug, array|string $data)
    {
        if (null === $data) {
            return false;
        }

        if ($cf->getOptions()[self::MULTIPLE]) {
            if ($cf->getOptions()[self::ALLOW_OTHER]) {
                return \in_array($choiceSlug, $this->deserialize($data, $cf)['_choices'], true);
            }

            return \in_array($choiceSlug, $this->deserialize($data, $cf), true);
        }

        if ($cf->getOptions()[self::ALLOW_OTHER]) {
            return $this->deserialize($data, $cf)['_choices'] === $choiceSlug;
        }

        return $this->deserialize($data, $cf) === $choiceSlug;
    }

    public function isEmptyValue($value, CustomField $customField)
    {
        if (null === $value) {
            return true;
        }

        // if multiple choice OR multiple/single choice with other
        if (\is_array($value)) {
            // if allow other
            if (\array_key_exists('_choices', $value)) {
                if (null === $value['_choices']) {
                    return true;
                }

                return empty($value['_choices']);
            }   // we do not have 'allow other'

            if (1 === \count($value)) {
                return empty($value[0]);
            }

            return empty($value);
        }

        return empty($value);

        throw \LogicException('This case is not expected.');
    }

    public function isMultiple(CustomField $cf)
    {
        return $cf->getOptions()[self::MULTIPLE];
    }

    /**
     * @internal this function is able to receive data whichever is the value of "other", "multiple"
     *
     * @return string html representation
     */
    public function render($value, CustomField $customField, $documentType = 'html')
    {
        // extract the data. They are under a _choice key if they are stored with allow_other
        $data = $value['_choices'] ?? $value;
        $selected = (\is_array($data)) ? $data : [$data];
        $choices = $customField->getOptions()[self::CHOICES];

        if (\in_array('_other', $selected, true)) {
            $choices[] = ['name' => $value['_other'], 'slug' => '_other'];
        }

        $template = '@ChillCustomFields/CustomFieldsRendering/choice.html.twig';

        if ('csv' === $documentType) {
            $template = '@ChillCustomFields/CustomFieldsRendering/choice.csv.twig';
        }

        return $this->templating
            ->render(
                $template,
                [
                    'choices' => $choices,
                    'selected' => $selected,
                    'multiple' => $customField->getOptions()[self::MULTIPLE],
                    'expanded' => $customField->getOptions()[self::EXPANDED],
                ]
            );
    }

    public function serialize($value, CustomField $customField)
    {
        return $value;
    }

    /**
     * deserialized the data from the database to a multiple
     * field.
     *
     * @param bool $allowOther
     */
    private function deserializeToMultiple(mixed $serialized, $allowOther)
    {
        $value = $this->guessValue($serialized);

        // set in an array : we want a multiple
        $fixedValue = \is_array($value) ? $value : [$value];

        if ($allowOther) {
            return $this->deserializeWithAllowOther($serialized, $fixedValue);
        }

        return $fixedValue;
    }

    private function deserializeToUnique($serialized, $allowOther)
    {
        $value = $this->guessValue($serialized);

        // set in a single value. We must have a single string
        $fixedValue = \is_array($value) ?
                // check if the array has an element, if not replace by empty string
                \count($value) > 0 ? end($value) : ''
                :
                $value;

        if ($allowOther) {
            return $this->deserializeWithAllowOther($serialized, $fixedValue);
        }

        return $fixedValue;
    }

    private function deserializeWithAllowOther($serialized, $value)
    {
        $existingOther = $serialized['_other'] ?? '';

        return [
            '_other' => $existingOther,
            '_choices' => $value,
        ];
    }

    /**
     * Guess the value from the representation of it.
     *
     * If the value had an 'allow_other' = true option, the returned value
     * **is not** the content of the _other field, but the `_other` string.
     */
    private function guessValue(array|string|null $value)
    {
        if (null === $value) {
            return null;
        }

        if (!\is_array($value)) {
            return $value;
        }
        // we have a field with "allow other"
        if (\array_key_exists('_choices', $value)) {
            return $value['_choices'];
        }

        // we have a field with "multiple"
        return $value;

        throw \LogicException('This case is not expected.');
    }
}
