<?php
/**
 * Form Helper
 *
 * @author Elze Kool <info@visualmedia.nl>
 */

namespace VisualM\FormBundle\Helper;

use DateTime;
use RuntimeException;
use Swift_Mailer;
use Swift_Message;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactory;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Validator\Constraints as Assert;
use VisualM\FormBundle\Doctrine\FieldType\FormFieldType;
use VisualM\FormBundle\Entity\Form as FormEntity;
use VisualM\FormBundle\Entity\FormEntry as FormEntryEntity;
use VisualM\FormBundle\Entity\FormEntryRepository;
use VisualM\FormBundle\Entity\FormField as FormFieldEntity;
use VisualM\FormBundle\Entity\FormRepository;
use VisualM\FormBundle\Form\Type\SubformGeneratorType;
use VisualM\FormBundle\Helper\Event\AfterHtmlRenderEvent;
use VisualM\FormBundle\Helper\Event\AfterPopulateBuilderEvent;
use VisualM\FormBundle\Helper\Event\FormHelperEvents;
use VisualM\FormBundle\Helper\Event\OnFieldAddEvent;
use VisualM\FormBundle\Helper\Event\OnRedirectResponseEvent;


/**
 * Form Helper
 *
 * @author Elze Kool <info@visualmedia.nl>
 */
class FormHelper
{
    /**
     * Renderered Forms
     *
     * @var array<Form, FormEntity>
     */
    private $renderedForms = [];

    /**
     * RequestStack
     * @var RequestStack
     */
    private $requestStack;

    /**
     * Form Factory
     * @var FormFactory
     */
    private $formFactory;

    /**
     * Templating Engine
     * @var EngineInterface
     */
    private $templating;

    /**
     * Mailer
     * @var Swift_Mailer
     */
    private $mailer;

    /**
     * Form Manager
     * @var FormRepository
     */
    private $formManager;

    /**
     * Form Entry Manager
     * @var FormEntryRepository
     */
    private $formEntryManager;

    /**
     * Event Dispatched
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * Sender e-mailadres
     * @var string
     */
    private $sender;


    /**
     * Status Options
     * @var string
     */
    private $defaultStatus;

    /**
     * Constructor
     *
     * @param RequestStack             $requestStack     Request Stack
     * @param FormFactory              $formFactory      Form Factory
     * @param EngineInterface          $templating       Templating Engine
     * @param Swift_Mailer             $mailer           Mailer
     * @param FormRepository           $formManager      Form Manager
     * @param FormEntryRepository      $formEntryManager Form Entry Manager
     * @param string                   $sender           Sender
     * @param EventDispatcherInterface $dispatcher       Event Dispatcher     * @param string                   $defaultStatus    Default Status
     */
    public function __construct(
        RequestStack $requestStack,
        FormFactory $formFactory,
        EngineInterface $templating,
        Swift_Mailer $mailer,
        FormRepository $formManager,
        FormEntryRepository $formEntryManager,
        $sender,
        EventDispatcherInterface $dispatcher,
        $defaultStatus    ) {
        $this->requestStack = $requestStack;
        $this->formFactory = $formFactory;
        $this->templating = $templating;
        $this->mailer = $mailer;
        $this->formManager = $formManager;
        $this->formEntryManager = $formEntryManager;
        $this->sender = $sender;
        $this->dispatcher = $dispatcher;
        $this->defaultStatus = $defaultStatus;
    }

    /**
     * Render Form
     * 
     * @param FormEntity  $formEntity        FormEntity
     * @param string|null $customRedirectUrl Custom Redirect Url
     *
     * @return string HTML Form HTML
     */
    public function renderForm(FormEntity $formEntity, $customRedirectUrl = null)
    {
        // Create form
        $form_builder = $this->getFormFactory()->createNamedBuilder('vm_form_' . $formEntity->getReferentie(), 'form');

        // Add Fields
        foreach($formEntity->getFields() as $form_field) {
            $this->addField($form_builder, $form_field);
        }
        
        $form_builder->add('spamcheck', 'hidden', array(
            'label' => false,
            'required' => false,
            'mapped' => false,
        ));
        
        $form_builder->add('accept', 'checkbox', array(
            'label' => false,
            'required' => false,
            'mapped' => false,
            'attr' =>  array(
                'style' => 'margin-left:-1234em;',
                'tabindex' => '-1',
            ),
        ));

        // Dispatch Event
        $this->dispatcher->dispatch(
            FormHelperEvents::AFTER_POPULATE_BUILDER,
            new AfterPopulateBuilderEvent($form_builder, $formEntity)
        );
        
        // Create form, update it from request and mark it as rendered
        $form = $form_builder->getForm();
        $form->handleRequest($this->requestStack->getMasterRequest());
        $this->renderedForms[] = [ $form, $formEntity, $customRedirectUrl ];

        $html = $this->templating->render(sprintf('VisualMFormEntity~%d~formHtml', $formEntity->getId()), [
            'form' => $form->createView(),
            'form_entity' => $formEntity
        ]);
       
        return $this->dispatcher->dispatch(
            FormHelperEvents::AFTER_HTML_RENDER,
            new AfterHtmlRenderEvent($formEntity, $html, AfterHtmlRenderEvent::TYPE_FORM)
        )->getHtml();

    }

    protected function formDataConvert($form, $formEntity)
    {
        // Extract data from fields
        $data = [];
        
        foreach($formEntity->getFields() as $field) {
            if ($form->has($field->getReferentie())) {
                
                if (strpos($field->getType(), 'form#') === 0) {
                    $subformEntityId = substr($field->getType(), 5, -2);
                    $subFormEntity = $this->formManager->getFirst([ 'id' => $subformEntityId ]);
                    $data[$field->getReferentie()] = [];
                    foreach($form->get($field->getReferentie()) as $subForm) {
                        $data[$field->getReferentie()][] = $this->formDataConvert($subForm, $subFormEntity);
                    }
                    continue;
                }
                
                $value = $form->get($field->getReferentie())->getData();

                if (is_array($value)) {
                    $data[$field->getReferentie()] = join(', ', $value);
                } else if($value instanceof DateTime) {
                    $data[$field->getReferentie()] = $value->format('d-m-Y');
                } else {
                    $data[$field->getReferentie()] = $value;
                }
            }
        }
        
        $data['spamcheck'] = $form->get('spamcheck')->getData();
        $data['accept'] = $form->get('accept')->getData();

        return $data;
    }


    /**
     * Handle the submit of the form
     *
     * @return null|Response Response to send or null if no response
     */
    public function handleSubmit()
    {
        foreach($this->renderedForms as $form_and_entity) {

            list($form, $form_entity, $custom_redirect_url) = $form_and_entity;
            /* @var $form Form */
            /* @var $form_entity FormEntity */

            if ($form->isValid()) {

                $data = $this->formDataConvert($form, $form_entity);
                
                if (false === $data['accept'] && $data['spamcheck'] === 'NO_SPAM') {
                    $entry = $this->saveEntry($data, $form_entity);
                    $this->sendEmails($data, $form_entity);
                } else {
                    $entry = null;
                }

                if (null !== ($response = $this->dispatcher->dispatch(
                    FormHelperEvents::ON_REDIRECT_RESPONSE,
                    new OnRedirectResponseEvent(new RedirectResponse(
                        $custom_redirect_url !== null ? $custom_redirect_url : $form_entity->getRedirectPath()
                    ), $entry))->getResponse())
                ) {
                    return $response;
                }
            }
            
        }

        return null;
    }

    /**
     * Save Entry to database
     *
     * @param array      $data       Data
     * @param FormEntity $formEntity Form Entity
     *
     * @return FormEntryEntity
     */
    protected function saveEntry(array $data, FormEntity $formEntity)
    {
        $request = $this->requestStack->getMasterRequest();

        // Create message HTML
        $email_html = $this->templating->render(sprintf('VisualMFormEntity~%d~emailHtml', $formEntity->getId()), [
            'form' => $formEntity,
            'data' => $data
        ]);

        $email_html = $this->dispatcher->dispatch(
            FormHelperEvents::AFTER_HTML_RENDER,
            new AfterHtmlRenderEvent($formEntity, $email_html, AfterHtmlRenderEvent::TYPE_EMAIL)
        )->getHtml();

        $form_entry = new FormEntryEntity();
        $form_entry->setStatus($this->defaultStatus);
        $form_entry->setForm($formEntity);
        $form_entry->setRemoteAddr($request->getClientIp());
        $form_entry->setBrowser($request->headers->get('User-Agent', ''));

        $form_entry->setLandingUrl($request->getSession()->get('userinfo.landing_page', null));
        $form_entry->setLandingReferer($request->getSession()->get('userinfo.landing_referer', null));
        $form_entry->setTimeOnSiteInSec($request->getSession()->has('userinfo.landing_time') ? (time() - $request->getSession()->get('userinfo.landing_time')) : null);
        $form_entry->setLandingUrl($request->getSession()->get('userinfo.landing_page'));
        $form_entry->setUrl($request->getSchemeAndHttpHost() . $request->getRequestUri());
        $form_entry->setRawdata($data);
        $form_entry->setEmailHtml($email_html);

        $this->formEntryManager->saveItem($form_entry);

        return $form_entry;
    }

    /**
     * Send E-mails for an succesfully validated form
     *
     * @param array      $data       Data
     * @param FormEntity $formEntity Form Entity
     */
    protected function sendEmails(array $data, FormEntity $formEntity)
    {
        // Create message HTML
        $email_html = $this->templating->render(sprintf('VisualMFormEntity~%d~emailHtml', $formEntity->getId()), [
            'form' => $formEntity,
            'data' => $data
        ]);

        $email_html = $this->dispatcher->dispatch(
            FormHelperEvents::AFTER_HTML_RENDER,
            new AfterHtmlRenderEvent($formEntity, $email_html, AfterHtmlRenderEvent::TYPE_EMAIL)
        )->getHtml();

        // Create Message
        $message = $this->mailer->createMessage();
        /* @var $message Swift_Message */

        $message
            ->setSender($this->sender)
            ->setSubject($this->replaceControlValues($formEntity->getEmailOnderwerp(), $data))
            ->addTo($this->replaceControlValues($formEntity->getOntvangerEmail(), $data), $this->replaceControlValues($formEntity->getOntvangerNaam(), $data))
            ->setFrom($this->replaceControlValues($formEntity->getAfzenderEmail(), $data), $this->replaceControlValues($formEntity->getAfzenderNaam(), $data))
            ->setBody($email_html, 'text/html');

        if ($formEntity->getBccEmail() != '' AND $formEntity->getBccEmail() != null) {
            foreach(explode(';', $formEntity->getBccEmail()) as $bcc) {
                $message->addBcc(trim($bcc));
            }
        }

        $this->mailer->send($message);

        // Check if we have to send a kopie
        if ($formEntity->getKopieNaarAfzender()) {

            // Create message HTML
            $kopie_email_html = $this->templating->render(sprintf('VisualMFormEntity~%d~kopieEmailHtml', $formEntity->getId()), [
                'form' => $formEntity,
                'data' => $data
            ]);

            $kopie_email_html = $this->dispatcher->dispatch(
                FormHelperEvents::AFTER_HTML_RENDER,
                new AfterHtmlRenderEvent($formEntity, $kopie_email_html, AfterHtmlRenderEvent::TYPE_KOPIE_EMAIL)
            )->getHtml();

            // Create Message
            $kopie_message = $this->mailer->createMessage();
            /* @var $kopie_message Swift_Message */

            $kopie_message
                ->setSender($this->sender)
                ->setSubject($this->replaceControlValues(($formEntity->getKopieEmailOnderwerp() != '' ? $formEntity->getKopieEmailOnderwerp() : $formEntity->getEmailOnderwerp()), $data))
                ->addTo($this->replaceControlValues($formEntity->getAfzenderEmail(), $data), $this->replaceControlValues($formEntity->getAfzenderNaam(), $data))
                ->setFrom($this->replaceControlValues($formEntity->getOntvangerEmail(), $data), $this->replaceControlValues($formEntity->getOntvangerNaam(), $data))
                ->setBody($kopie_email_html, 'text/html');

            $this->mailer->send($kopie_message);
            
        }
        
    }

    /**
     * Replace {Control:name} values in string
     *
     * @param string        $string Input string
     * @param array<string> $data   Data from Form
     *
     * @return string String with replacements
     */
    protected function replaceControlValues($string, $data)
    {
        $replacements = [];
        foreach($data as $key => $value) {
            if (!is_array($value)) {
                $replacements[sprintf('{control:%s}', $key)] = $value;
            }
        }
        return str_replace(array_keys($replacements), array_values($replacements), $string);
    }

    
    /**
     * Add Field to FormBuilder
     *
     * @param FormBuilderInterface $formBuilder FormBuilder
     * @param FormFieldEntity      $formField   Form Field
     * @param bool                 $mapped      Mapped
     */
    protected function addField(FormBuilderInterface $formBuilder, FormFieldEntity $formField, $mapped = false)
    {
        $constraints = [];
        
        switch($formField->getType()) {
            case FormFieldType::HIDDEN:
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'hidden', [  'mapped' => $mapped, 'constraints' => $constraints ]);
                return;

            case FormFieldType::TEXT:
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'text', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints ]);
                return;

            case FormFieldType::NUMBER:
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }
                $constraints[] = new Assert\Type([ 'type' => 'int']);

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'integer', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints ]);
                return;
                
            case FormFieldType::PHONE:
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'text', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints, 'attr' => [ 'type' => 'tel' ] ]);
                return;

            case FormFieldType::EMAIL:
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $constraints[] = new Assert\Email();
                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'email', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints ]);
                return;

            case FormFieldType::DATE:
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'date', [  'widget' => 'single_text',  'mapped' => false, 'required' => $formField->getVerplicht(), 'constraints' => $constraints ]);
                return;

            case FormFieldType::TEXTAREA:
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'textarea', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints, 'attr' => [ 'rows' => $formField->getRijen() ] ]);
                return;

            case FormFieldType::SELECT:
                $choices = $this->createChoices($formField->getWaarden());
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }

                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'choice', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints, 'choices' => $choices ]);
                return;

            case FormFieldType::SELECTLIST:
                $choices = $this->createChoices($formField->getWaarden());
                if ($formField->getVerplicht()) { $constraints[] = new Assert\Count([ 'min' => 1, 'max' => count($choices) ]); }
                
                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'choice', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints, 'choices' => $choices, 'multiple' => true, 'expanded' => false, 'attr' => [ 'rows' => $formField->getRijen() ]  ]);
                return;

            case FormFieldType::CHECKBOXLIST:
                $choices = $this->createChoices($formField->getWaarden());
                if ($formField->getVerplicht()) { $constraints[] = new Assert\Count([ 'min' => 1, 'max' => count($choices) ]); }
                
                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'choice', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints, 'choices' => $choices, 'multiple' => true, 'expanded' => true  ]);
                return;

            case FormFieldType::OPTIONLIST:
                $choices = $this->createChoices($formField->getWaarden());
                if ($formField->getRegex() != '' AND $formField->getRegex() != null) { $constraints[] = new Assert\Regex([ 'pattern' => $formField->getRegex() ]); }
                if ($formField->getVerplicht()) { $constraints[] = new Assert\NotBlank(); }
                
                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'choice', [  'mapped' => $mapped, 'required' => $formField->getVerplicht(), 'constraints' => $constraints, 'choices' => $choices, 'multiple' => false, 'expanded' => true  ]);
                return;
        }

        // Kijk of we een subform moeten includen
        foreach($this->formManager->getIndex() as $formEntity) {
            if ($formField->getType() == sprintf('form#%d[]', $formEntity->getId())) {
                
                $collectionType = new SubformGeneratorType(
                    'vm_form_' . $formField->getReferentie() . '_' . $formEntity->getReferentie(),
                    function(FormBuilderInterface $subFormBuilder) use ($formEntity) {
                        foreach($formEntity->getFields() as $subFormField) {
                            $this->addField($subFormBuilder, $subFormField);
                        }
                    }
                );
                $this->addFieldToBuilder($formBuilder, $formField, $formField->getReferentie(), 'collection', [
                    'label' => false,
                    'allow_add' => true,
                    'allow_delete' => true,
                    'type' => $collectionType,
                    'mapped' => false
                ]);

                return;
            }
        }

        throw new RuntimeException(sprintf('Unkown form field type "%s" in %s', $formField->getType(), get_class($this)));

    }

    /**
     * Get Form Factory
     * 
     * @return FormFactory FormFactory
     */
    protected function getFormFactory()
    {
        return $this->formFactory;
    }

    /**
     * Add Field to Builder
     * 
     * @param FormBuilderInterface            $formBuilder Form Builder
     * @param FormFieldEntity                 $formField   Form Field
     * @param string|int|FormBuilderInterface $child           Child
     * @param string|FormTypeInterface        $type            Type
     * @param array                           $options         Options
     */
    protected function addFieldToBuilder(FormBuilderInterface $formBuilder, FormFieldEntity $formField, $child, $type, array $options = [])
    {
        // Dispatch Event
        $event = $this->dispatcher->dispatch(
            FormHelperEvents::ON_FIELD_ADD,
            new OnFieldAddEvent($child, $type, $options, $formField)
        );

        if (!$event->getDoNotAdd()) {
            $formBuilder->add($event->getChild(), $event->getType(), $event->getOptions());
        }        
    }

    /**
     * Create Choices from Waarden
     *
     * @param string $waarden Waarden
     *
     * @return array<string> Choices
     */
    private function createChoices($waarden)
    {
        $choices = [];
        foreach(explode("\n", $waarden) as $waarde) {
            $choices[trim($waarde)] = trim($waarde);
        }
        return $choices;
    }

    
}