<?php
/**
 * This helper builds the request body for a /mail/send API call
 */

namespace SendGrid\Mail;

use InvalidArgumentException;
use SendGrid\Helper\Assert;

/**
 * This class is used to construct a request body for the /mail/send API call
 *
 * @package SendGrid\Mail
 */
class Mail implements \JsonSerializable
{
    /** @var $from From Email address of the sender */
    private $from;

    /** @var $subject Subject Subject of the email */
    private $subject;

    /** @var $contents Content[] Content(s) of the email */
    private $contents;

    /** @var $attachments Attachment[] Email attachments */
    private $attachments;

    /** @var $template_id TemplateId Id of a template that you would like to use */
    private $template_id;

    /** @var $sections Section[] Key/value pairs that define block sections of code to be used as substitutions */
    private $sections;

    /** @var $headers Header[] Header names and the value to substitute for them */
    private $headers;

    /** @var $categories Category[] Category names for this message */
    private $categories;

    /** @var $custom_args CustomArg[] Values that are specific to the entire send that will be carried along with the email and its activity data */
    private $custom_args;

    /** @var $substitutions Substitution[] Substitutions that will apply to the text and html content of the body of your email, in addition to the subject and reply-to parameters */
    private $substitutions;

    /** @var $send_at SendAt A unix timestamp allowing you to specify when you want your email to be delivered */
    private $send_at;

    /** @var $batch_id BatchId This ID represents a batch of emails to be sent at the same time */
    private $batch_id;

    /** @var $asm ASM Specifies how to handle unsubscribes */
    private $asm;

    /** @var $ip_pool_name IpPoolName The IP Pool that you would like to send this email from */
    private $ip_pool_name;

    /** @var $mail_settings MailSettings A collection of different mail settings that you can use to specify how you would like this email to be handled */
    private $mail_settings;

    /** @var $tracking_settings TrackingSettings Settings to determine how you would like to track the metrics of how your recipients interact with your email */
    private $tracking_settings;

    /** @var $reply_to ReplyTo Email to be use when replied to */
    private $reply_to;

    /** @var $personalization Personalization[] Messages and their metadata */
    private $personalization;

    /**
     * If passing parameters into this constructor, include $from, $to, $subject,
     * $plainTextContent, $htmlContent and $globalSubstitutions at a minimum.
     * If you don't supply any, a Personalization object will be created for you.
     *
     * @param From|null                 $from                Email address of the sender
     * @param To|To[]|null              $to                  Recipient(s) email address(es)
     * @param Subject|Subject[]|null    $subject             Subject(s)
     * @param PlainTextContent|null     $plainTextContent    Plain text version of content
     * @param HtmlContent|null          $htmlContent         Html version of content
     * @param Substitution[]|array|null $globalSubstitutions Substitutions for entire email
     *
     * @throws TypeException
     */
    public function __construct(
        $from = null,
        $to = null,
        $subject = null,
        $plainTextContent = null,
        $htmlContent = null,
        array $globalSubstitutions = null
    ) {
        if (!isset($from)
            && !isset($to)
            && !isset($subject)
            && !isset($plainTextContent)
            && !isset($htmlContent)
            && !isset($globalSubstitutions)
        ) {
            $this->personalization[] = new Personalization();
            return;
        }
        if (isset($from)) {
            $this->setFrom($from);
        }
        if (isset($to)) {
            if (!is_array($to)) {
                $to = [$to];
            }
            $subjectCount = 0;
            $personalization = new Personalization();
            foreach ($to as $email) {
                if ($email->getSubstitutions()) {
                    $personalization = new Personalization();
                }
                $personalization->addTo($email);
                if ($subs = $email->getSubstitutions()) {
                    foreach ($subs as $key => $value) {
                        $personalization->addSubstitution($key, $value);
                    }
                }
                if ($email->getSubject()) {
                    $personalization->setSubject($email->getSubject());
                }
                if (is_array($subject)) {
                    if ($subjectCount < \count($subject)) {
                        $personalization->setSubject($subject[$subjectCount]);
                    }
                    $subjectCount++;
                }
                if (is_array($globalSubstitutions)) {
                    foreach ($globalSubstitutions as $key => $value) {
                        if ($value instanceof Substitution) {
                            $personalization->addSubstitution($value);
                        } else {
                            $personalization->addSubstitution($key, $value);
                        }
                    }
                }
                if ($email->getSubstitutions()) {
                    $this->addPersonalization($personalization);
                }
            }
            if (isset($email) && !$email->getSubstitutions()) {
                $this->addPersonalization($personalization);
            }
        }
        if (isset($subject) && !is_array($subject)) {
            $this->setSubject($subject);
        }
        if (isset($plainTextContent)) {
            Assert::isInstanceOf($plainTextContent, 'plainTextContent', Content::class);

            $this->addContent($plainTextContent);
        }
        if (isset($htmlContent)) {
            Assert::isInstanceOf($htmlContent, 'htmlContent', Content::class);

            $this->addContent($htmlContent);
        }
    }

    /**
     * Adds a To, Cc or Bcc object to a Personalization object
     *
     * @param string $emailType Object type name: To, Cc or Bcc
     * @param string $email Recipient email address
     * @param string|null $name Recipient name
     * @param Substitution[]|array|null $substitutions Personalized
     *                                                 substitutions
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    private function addRecipientEmail(
        $emailType,
        $email,
        $name = null,
        $substitutions = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $personalizationFunctionCall = 'add' . $emailType;
        $emailType = '\SendGrid\Mail\\' . $emailType;
        if (!($email instanceof $emailType)) {
            $email = new $emailType(
                $email,
                $name,
                $substitutions
            );
        }

        $personalization = $this->getPersonalization($personalizationIndex, $personalization);
        $personalization->$personalizationFunctionCall($email);

        if ($subs = $email->getSubstitutions()) {
            foreach ($subs as $key => $value) {
                $personalization->addSubstitution($key, $value);
            }
        }
    }

    /**
     * Adds an array of To, Cc or Bcc objects to a Personalization object
     *
     * @param string $emailType Object type name: To, Cc  or Bcc
     * @param To[]|Cc[]|Bcc[] $emails Array of email recipients
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    private function addRecipientEmails(
        $emailType,
        $emails,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $emailFunctionCall = 'add' . $emailType;

        if (current($emails) instanceof EmailAddress) {
            foreach ($emails as $email) {
                $this->$emailFunctionCall(
                    $email,
                    $name = null,
                    $substitutions = null,
                    $personalizationIndex,
                    $personalization
                );
            }
        } else {
            foreach ($emails as $email => $name) {
                $this->$emailFunctionCall(
                    $email,
                    $name,
                    $substitutions = null,
                    $personalizationIndex,
                    $personalization
                );
            }
        }
    }

    /**
     * Add a Personalization object to the Mail object
     *
     * @param Personalization $personalization A Personalization object
     *
     * @throws TypeException
     */
    public function addPersonalization($personalization)
    {
        Assert::isInstanceOf($personalization, 'personalization', Personalization::class);

        $this->personalization[] = $personalization;
    }

    /**
     * Retrieves a Personalization object, adds a pre-created Personalization
     * object, or creates and adds a Personalization object.
     *
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @return Personalization
     *
     * @throws TypeException
     */
    public function getPersonalization($personalizationIndex = null, $personalization = null)
    {
        /**
         * Approach:
         * - Append if provided personalization + return
         * - Return last added if not provided personalizationIndex (create on empty)
         * - Return existing personalizationIndex
         * - InvalidArgumentException on unexpected personalizationIndex ( > count)
         * - Create + add Personalization and return
         */

        //  If given a Personalization instance
        if (null !== $personalization) {
            //  Just append it onto Mail and return it
            $this->addPersonalization($personalization);
            return $personalization;
        }

        //  Retrieve count of existing Personalization instances
        $personalizationCount = $this->getPersonalizationCount();

        //  Not providing a personalizationIndex?
        if (null === $personalizationIndex) {
            //  Create new Personalization instance depending on current count
            if (0 === $personalizationCount) {
                $this->addPersonalization(new Personalization());
            }

            //  Return last added Personalization instance
            return end($this->personalization);
        }

        //  Existing personalizationIndex in personalization?
        if (isset($this->personalization[$personalizationIndex])) {
            //  Return referred personalization
            return $this->personalization[$personalizationIndex];
        }

        //  Non-existent personalizationIndex given
        //  Only allow creation of next Personalization if given
        //  personalizationIndex equals personalizationCount
        if (
            ($personalizationIndex < 0) ||
            ($personalizationIndex > $personalizationCount)
        ) {
            throw new InvalidArgumentException(
                'personalizationIndex ' . $personalizationIndex .
                ' must be less than ' . $personalizationCount
            );
        }

        //  Create new Personalization and return it
        $personalization = new Personalization();
        $this->addPersonalization($personalization);
        return $personalization;
    }

    /**
     * Retrieves Personalization object collection from the Mail object.
     *
     * @return Personalization[]|null
     */
    public function getPersonalizations()
    {
        return $this->personalization;
    }

    /**
     * Retrieve the number of Personalization objects associated with the Mail object
     *
     * @return int
     */
    public function getPersonalizationCount()
    {
        return isset($this->personalization) ? count($this->personalization) : 0;
    }

    /**
     * Adds an email recipient to a Personalization object
     *
     * @param string|To $to Email address or To object
     * @param string $name Recipient name
     * @param array|Substitution[] $substitutions Personalized substitutions
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    public function addTo(
        $to,
        $name = null,
        $substitutions = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if ($to instanceof To) {
            $name = $to->getName();
            $substitutions = $to->getSubstitutions();
            $to = $to->getEmailAddress();
        }
        $this->addRecipientEmail(
            'To',
            $to,
            $name,
            $substitutions,
            $personalizationIndex,
            $personalization
        );
    }

    /**
     * Adds multiple email recipients to a Personalization object
     *
     * @param To[]|array $toEmails Array of To objects or key/value pairs of
     *                             email address/recipient names
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    public function addTos(
        $toEmails,
        $personalizationIndex = null,
        $personalization = null
    ) {
        Assert::minItems($toEmails, 'toEmails', 1);
        Assert::maxItems($toEmails, 'toEmails', 1000);

        $this->addRecipientEmails(
            'To',
            $toEmails,
            $personalizationIndex,
            $personalization
        );
    }

    /**
     * Adds an email cc recipient to a Personalization object
     *
     * @param string|Cc $cc Email address or Cc object
     * @param string $name Recipient name
     * @param Substitution[]|array|null $substitutions Personalized
     *                                                 substitutions
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    public function addCc(
        $cc,
        $name = null,
        $substitutions = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if ($cc instanceof Cc) {
            $name = $cc->getName();
            $cc = $cc->getEmailAddress();
        }
        $this->addRecipientEmail(
            'Cc',
            $cc,
            $name,
            $substitutions,
            $personalizationIndex,
            $personalization
        );
    }

    /**
     * Adds multiple email cc recipients to a Personalization object
     *
     * @param Cc[]|array $ccEmails Array of Cc objects or key/value pairs of
     *                             email address/recipient names
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    public function addCcs(
        $ccEmails,
        $personalizationIndex = null,
        $personalization = null
    ) {
        Assert::minItems($ccEmails, 'ccEmails', 1);
        Assert::maxItems($ccEmails, 'ccEmails', 1000);

        $this->addRecipientEmails(
            'Cc',
            $ccEmails,
            $personalizationIndex,
            $personalization
        );
    }

    /**
     * Adds an email bcc recipient to a Personalization object
     *
     * @param string|Bcc $bcc Email address or Bcc object
     * @param string $name Recipient name
     * @param Substitution[]|array|null $substitutions Personalized
     *                                                 substitutions
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    public function addBcc(
        $bcc,
        $name = null,
        $substitutions = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if ($bcc instanceof Bcc) {
            $name = $bcc->getName();
            $bcc = $bcc->getEmailAddress();
        }
        $this->addRecipientEmail(
            'Bcc',
            $bcc,
            $name,
            $substitutions,
            $personalizationIndex,
            $personalization
        );
    }

    /**
     * Adds multiple email bcc recipients to a Personalization object
     *
     * @param Bcc[]|array $bccEmails Array of Bcc objects or key/value pairs of
     *                               email address/recipient names
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     *
     * @throws TypeException
     */
    public function addBccs(
        $bccEmails,
        $personalizationIndex = null,
        $personalization = null
    ) {
        Assert::minItems($bccEmails, 'bccEmails', 1);
        Assert::maxItems($bccEmails, 'bccEmails', 1000);

        $this->addRecipientEmails(
            'Bcc',
            $bccEmails,
            $personalizationIndex,
            $personalization
        );
    }

    /**
     * Add a subject to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * subject will be global to entire message. Note that
     * subjects added to Personalization objects override
     * global subjects.
     *
     * @param string|Subject $subject Email subject
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function setSubject(
        $subject,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if (!($subject instanceof Subject)) {
            $subject = new Subject($subject);
        }

        if ($personalization !== null) {
            $personalization->setSubject($subject);
            $this->addPersonalization($personalization);
            return;
        }
        if ($personalizationIndex !== null) {
            $this->personalization[$personalizationIndex]->setSubject($subject);
            return;
        }
        $this->setGlobalSubject($subject);
    }

    /**
     * Retrieve a subject attached to a Personalization object
     *
     * @param int $personalizationIndex Index into the array of existing
     *                                    Personalization objects
     * @return Subject
     */
    public function getSubject($personalizationIndex = 0)
    {
        return $this->personalization[$personalizationIndex]->getSubject();
    }

    /**
     * Add a header to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * header will be global to entire message. Note that
     * headers added to Personalization objects override
     * global headers.
     *
     * @param string|Header $key Key or Header object
     * @param string|null $value Value
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addHeader(
        $key,
        $value = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $header = null;
        if ($key instanceof Header) {
            $h = $key;
            $header = new Header($h->getKey(), $h->getValue());
        } else {
            $header = new Header($key, $value);
        }

        $personalization = $this->getPersonalization($personalizationIndex, $personalization);
        $personalization->addHeader($header);
    }

    /**
     * Adds multiple headers to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * header will be global to entire message. Note that
     * headers added to Personalization objects override
     * global headers.
     *
     * @param array|Header[] $headers Array of Header objects or key values
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addHeaders(
        $headers,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if (current($headers) instanceof Header) {
            foreach ($headers as $header) {
                $this->addHeader($header);
            }
        } else {
            foreach ($headers as $key => $value) {
                $this->addHeader(
                    $key,
                    $value,
                    $personalizationIndex,
                    $personalization
                );
            }
        }
    }

    /**
     * Retrieve the headers attached to a Personalization object
     *
     * @param int $personalizationIndex Index into the array of existing
     *                                    Personalization objects
     * @return Header[]
     */
    public function getHeaders($personalizationIndex = 0)
    {
        return $this->personalization[$personalizationIndex]->getHeaders();
    }

    /**
     * Add a Substitution object or key/value to a Personalization object
     *
     * @param Substitution|string $key Substitution object or the key of a
     *                                 dynamic data
     * @param string|null $value Value
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addDynamicTemplateData(
        $key,
        $value = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $this->addSubstitution($key, $value, $personalizationIndex, $personalization);
    }

    /**
     * Add a Substitution object or key/value to a Personalization object
     *
     * @param array|Substitution[] $datas Array of Substitution
     *                                    objects or key/values
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addDynamicTemplateDatas(
        $datas,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $this->addSubstitutions($datas, $personalizationIndex, $personalization);
    }

    /**
     * Retrieve dynamic template data key/value pairs from a Personalization object
     *
     * @param int|0 $personalizationIndex Index into the array of existing
     *                                    Personalization objects
     * @return array
     */
    public function getDynamicTemplateDatas($personalizationIndex = 0)
    {
        return $this->getSubstitutions($personalizationIndex);
    }

    /**
     * Add a substitution to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * substitution will be global to entire message. Note that
     * substitutions added to Personalization objects override
     * global substitutions.
     *
     * @param string|Substitution $key Key or Substitution object
     * @param string|null $value Value
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addSubstitution(
        $key,
        $value = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $substitution = null;
        if ($key instanceof Substitution) {
            $s = $key;
            $substitution = new Substitution($s->getKey(), $s->getValue());
        } else {
            $substitution = new Substitution($key, $value);
        }

        $personalization = $this->getPersonalization($personalizationIndex, $personalization);
        $personalization->addSubstitution($substitution);
    }

    /**
     * Adds multiple substitutions to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * substitution will be global to entire message. Note that
     * substitutions added to Personalization objects override
     * global headers.
     *
     * @param array|Substitution[] $substitutions Array of Substitution
     *                                            objects or key/values
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addSubstitutions(
        $substitutions,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if (current($substitutions) instanceof Substitution) {
            foreach ($substitutions as $substitution) {
                $this->addSubstitution($substitution);
            }
        } else {
            foreach ($substitutions as $key => $value) {
                $this->addSubstitution(
                    $key,
                    $value,
                    $personalizationIndex,
                    $personalization
                );
            }
        }
    }

    /**
     * Retrieve the substitutions attached to a Personalization object
     *
     * @param int|0 $personalizationIndex Index into the array of existing
     *                                    Personalization objects
     * @return Substitution[]
     */
    public function getSubstitutions($personalizationIndex = 0)
    {
        return $this->personalization[$personalizationIndex]->getSubstitutions();
    }

    /**
     * Add a custom arg to a Personalization or Mail object
     *
     * Note that custom args added to Personalization objects
     * override global custom args.
     *
     * @param string|CustomArg $key Key or CustomArg object
     * @param string|null $value Value
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addCustomArg(
        $key,
        $value = null,
        $personalizationIndex = null,
        $personalization = null
    ) {
        $custom_arg = null;
        if ($key instanceof CustomArg) {
            $ca = $key;
            $custom_arg = new CustomArg($ca->getKey(), $ca->getValue());
        } else {
            $custom_arg = new CustomArg($key, $value);
        }

        $personalization = $this->getPersonalization($personalizationIndex, $personalization);
        $personalization->addCustomArg($custom_arg);
    }

    /**
     * Adds multiple custom args to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * custom arg will be global to entire message. Note that
     * custom args added to Personalization objects override
     * global custom args.
     *
     * @param array|CustomArg[] $custom_args Array of CustomArg objects or
     *                                       key/values
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function addCustomArgs(
        $custom_args,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if (current($custom_args) instanceof CustomArg) {
            foreach ($custom_args as $custom_arg) {
                $this->addCustomArg($custom_arg);
            }
        } else {
            foreach ($custom_args as $key => $value) {
                $this->addCustomArg(
                    $key,
                    $value,
                    $personalizationIndex,
                    $personalization
                );
            }
        }
    }

    /**
     * Retrieve the custom args attached to a Personalization object
     *
     * @param int|0 $personalizationIndex Index into the array of existing
     *                                    Personalization objects
     * @return CustomArg[]
     */
    public function getCustomArgs($personalizationIndex = 0)
    {
        return $this->personalization[$personalizationIndex]->getCustomArgs();
    }

    /**
     * Add a unix timestamp allowing you to specify when you want your
     * email to be delivered to a Personalization or Mail object
     *
     * If you don't provide a Personalization object or index, the
     * send at timestamp will be global to entire message. Note that
     * timestamps added to Personalization objects override
     * global timestamps.
     *
     * @param int|SendAt $send_at A unix timestamp
     * @param int|null $personalizationIndex Index into the array of existing
     *                                       Personalization objects
     * @param Personalization|null $personalization A pre-created
     *                                              Personalization object
     * @throws TypeException
     */
    public function setSendAt(
        $send_at,
        $personalizationIndex = null,
        $personalization = null
    ) {
        if (!($send_at instanceof SendAt)) {
            $send_at = new SendAt($send_at);
        }

        $personalization = $this->getPersonalization($personalizationIndex, $personalization);
        $personalization->setSendAt($send_at);
    }

    /**
     * Retrieve the unix timestamp attached to a Personalization object
     *
     * @param int|0 $personalizationIndex Index into the array of existing
     *                                    Personalization objects
     * @return SendAt|null
     */
    public function getSendAt($personalizationIndex = 0)
    {
        return $this->personalization[$personalizationIndex]->getSendAt();
    }

    /**
     * Add the sender email address to a Mail object
     *
     * @param string|From $email Email address or From object
     * @param string|null $name  Sender name
     *
     * @throws TypeException
     */
    public function setFrom($email, $name = null)
    {
        if ($email instanceof From) {
            $this->from = $email;
        } else {
            Assert::email(
                $email,
                'email',
                '"$email" must be an instance of SendGrid\Mail\From or a valid email address'
            );
            $this->from = new From($email, $name);
        }
    }

    /**
     * Retrieve the sender attached to a Mail object
     *
     * @return From
     */
    public function getFrom()
    {
        return $this->from;
    }

    /**
     * Add the reply to email address to a Mail object
     *
     * @param string|ReplyTo $email Email address or From object
     * @param string|null $name Reply to name
     *
     * @throws TypeException
     */
    public function setReplyTo($email, $name = null)
    {
        if ($email instanceof ReplyTo) {
            $this->reply_to = $email;
        } else {
            $this->reply_to = new ReplyTo($email, $name);
        }
    }

    /**
     * Retrieve the reply to information attached to a Mail object
     *
     * @return ReplyTo
     */
    public function getReplyTo()
    {
        return $this->reply_to;
    }

    /**
     * Add a subject to a Mail object
     *
     * Note that
     * subjects added to Personalization objects override
     * global subjects.
     *
     * @param string|Subject $subject Email subject
     *
     * @throws TypeException
     */
    public function setGlobalSubject($subject)
    {
        if (!($subject instanceof Subject)) {
            $subject = new Subject($subject);
        }
        $this->subject = $subject;
    }

    /**
     * Retrieve a subject attached to a Mail object
     *
     * @return Subject
     */
    public function getGlobalSubject()
    {
        return $this->subject;
    }

    /**
     * Add content to a Mail object
     *
     * For a list of pre-configured mime types, please see
     * MimeType.php
     *
     * @param string|Content $type Mime type or Content object
     * @param string|null $value Contents (e.g. text or html)
     *
     * @throws TypeException
     */
    public function addContent($type, $value = null)
    {
        if ($type instanceof Content) {
            $content = $type;
        } else {
            $content = new Content($type, $value);
        }
        $this->contents[] = $content;
    }

    /**
     * Adds multiple Content objects to a Mail object
     *
     * @param array|Content[] $contents Array of Content objects
     *                                  or key value pairs
     *
     * @throws TypeException
     */
    public function addContents($contents)
    {
        if (current($contents) instanceof Content) {
            foreach ($contents as $content) {
                $this->addContent($content);
            }
        } else {
            foreach ($contents as $key => $value) {
                $this->addContent($key, $value);
            }
        }
    }

    /**
     * Retrieve the contents attached to a Mail object
     *
     * Will return array of Content Objects with text/plain MimeType first
     * Array re-ordered before return where this is not already the case
     *
     * @return Content[]
     */
    public function getContents()
    {
        if ($this->contents) {
            if ($this->contents[0]->getType() !== MimeType::TEXT && \count($this->contents) > 1) {
                foreach ($this->contents as $key => $value) {
                    if ($value->getType() === MimeType::TEXT) {
                        $plain_content = $value;
                        unset($this->contents[$key]);
                        break;
                    }
                }
                if (isset($plain_content)) {
                    array_unshift($this->contents, $plain_content);
                }
            }
        }

        return $this->contents;
    }

    /**
     * Add an attachment to a Mail object
     *
     * @param string|Attachment $attachment Attachment object or
     *                                       Base64 encoded content
     * @param string|null $type Mime type of the attachment
     * @param string|null $filename File name of the attachment
     * @param string|null $disposition How the attachment should be
     *                                       displayed: inline or attachment
     *                                       default is attachment
     * @param string|null $content_id Used when disposition is inline
     *                                       to display the file within the
     *                                       body of the email
     * @throws TypeException
     */
    public function addAttachment(
        $attachment,
        $type = null,
        $filename = null,
        $disposition = null,
        $content_id = null
    ) {
        if (is_array($attachment)) {
            $attachment = new Attachment(
                $attachment[0],
                $attachment[1],
                $attachment[2],
                $attachment[3],
                $attachment[4]
            );
        } else if (!($attachment instanceof Attachment)) {
            $attachment = new Attachment(
                $attachment,
                $type,
                $filename,
                $disposition,
                $content_id
            );
        }
        $this->attachments[] = $attachment;
    }

    /**
     * Adds multiple attachments to a Mail object
     *
     * @param array|Attachment[] $attachments Array of Attachment objects or
     *                                        arrays
     * @throws TypeException
     */
    public function addAttachments($attachments)
    {
        foreach ($attachments as $attachment) {
            $this->addAttachment($attachment);
        }
    }

    /**
     * Retrieve the attachments attached to a Mail object
     *
     * @return Attachment[]
     */
    public function getAttachments()
    {
        return $this->attachments;
    }

    /**
     * Add a template id to a Mail object
     *
     * @param TemplateId|string $template_id The id of the template to be
     *                                       applied to this email
     * @throws TypeException
     */
    public function setTemplateId($template_id)
    {
        if (!($template_id instanceof TemplateId)) {
            $template_id = new TemplateId($template_id);
        }

        $this->template_id = $template_id;
    }

    /**
     * Retrieve a template id attached to a Mail object
     *
     * @return TemplateId
     */
    public function getTemplateId()
    {
        return $this->template_id;
    }

    /**
     * Add a section to a Mail object
     *
     * @param string|Section $key Key or Section object
     * @param string|null $value Value
     */
    public function addSection($key, $value = null)
    {
        if ($key instanceof Section) {
            $section = $key;
            $this->sections[$section->getKey()] = $section->getValue();
            return;
        }
        $this->sections[$key] = (string)$value;
    }

    /**
     * Adds multiple sections to a Mail object
     *
     * @param array|Section[] $sections Array of CustomArg objects
     *                                  or key/values
     */
    public function addSections($sections)
    {
        if (current($sections) instanceof Section) {
            foreach ($sections as $section) {
                $this->addSection($section);
            }
        } else {
            foreach ($sections as $key => $value) {
                $this->addSection($key, $value);
            }
        }
    }

    /**
     * Retrieve the section(s) attached to a Mail object
     *
     * @return Section[]
     */
    public function getSections()
    {
        return $this->sections;
    }

    /**
     * Add a header to a Mail object
     *
     * Note that headers added to Personalization objects override
     * global headers.
     *
     * @param string|Header $key Key or Header object
     * @param string|null $value Value
     */
    public function addGlobalHeader($key, $value = null)
    {
        if ($key instanceof Header) {
            $header = $key;
            $this->headers[$header->getKey()] = $header->getValue();
            return;
        }
        $this->headers[$key] = (string)$value;
    }

    /**
     * Adds multiple headers to a Mail object
     *
     * Note that headers added to Personalization objects override
     * global headers.
     *
     * @param array|Header[] $headers Array of Header objects
     *                                or key values
     */
    public function addGlobalHeaders($headers)
    {
        if (current($headers) instanceof Header) {
            foreach ($headers as $header) {
                $this->addGlobalHeader($header);
            }
        } else {
            foreach ($headers as $key => $value) {
                $this->addGlobalHeader($key, $value);
            }
        }
    }

    /**
     * Retrieve the headers attached to a Mail object
     *
     * @return Header[]
     */
    public function getGlobalHeaders()
    {
        return $this->headers;
    }

    /**
     * Add a substitution to a Mail object
     *
     * Note that substitutions added to Personalization objects override
     * global substitutions.
     *
     * @param string|Substitution $key Key or Substitution object
     * @param string|null $value Value
     */
    public function addGlobalSubstitution($key, $value = null)
    {
        if ($key instanceof Substitution) {
            $substitution = $key;
            $this->substitutions[$substitution->getKey()] = $substitution->getValue();
            return;
        }
        $this->substitutions[$key] = $value;
    }

    /**
     * Adds multiple substitutions to a Mail object
     *
     * Note that substitutions added to Personalization objects override
     * global headers.
     *
     * @param array|Substitution[] $substitutions Array of Substitution
     *                                            objects or key/values
     */
    public function addGlobalSubstitutions($substitutions)
    {
        if (current($substitutions) instanceof Substitution) {
            foreach ($substitutions as $substitution) {
                $this->addGlobalSubstitution($substitution);
            }
        } else {
            foreach ($substitutions as $key => $value) {
                $this->addGlobalSubstitution($key, $value);
            }
        }
    }

    /**
     * Retrieve the substitutions attached to a Mail object
     *
     * @return Substitution[]
     */
    public function getGlobalSubstitutions()
    {
        return $this->substitutions;
    }

    /**
     * Add a category to a Mail object
     *
     * @param string|Category $category Category object or category name
     * @throws TypeException
     */
    public function addCategory($category)
    {
        if (!($category instanceof Category)) {
            $category = new Category($category);
        }

        Assert::accept($category, 'category', function () {
            $categories = $this->categories;
            if (!\is_array($categories)) {
                $categories = [];
            }
            return \count($categories) < 10;
        }, 'Number of elements in "$categories" can not exceed 10.');

        $this->categories[] = $category;
    }

    /**
     * Adds multiple categories to a Mail object
     *
     * @param array|Category[] $categories Array of Category objects or arrays
     * @throws TypeException
     */
    public function addCategories($categories)
    {
        foreach ($categories as $category) {
            $this->addCategory($category);
        }
    }

    /**
     * Retrieve the categories attached to a Mail object
     *
     * @return Category[]
     */
    public function getCategories()
    {
        return $this->categories;
    }

    /**
     * Add a custom arg to a Mail object
     *
     * Note that custom args added to Personalization objects override
     * global custom args.
     *
     * @param string|CustomArg $key Key or CustomArg object
     * @param string|null $value Value
     */
    public function addGlobalCustomArg($key, $value = null)
    {
        if ($key instanceof CustomArg) {
            $custom_arg = $key;
            $this->custom_args[$custom_arg->getKey()] = $custom_arg->getValue();
            return;
        }
        $this->custom_args[$key] = (string)$value;
    }

    /**
     * Adds multiple custom args to a Mail object
     *
     * Note that custom args added to Personalization objects override
     * global custom args.
     *
     * @param array|CustomArg[] $custom_args Array of CustomArg objects
     *                                       or key/values
     */
    public function addGlobalCustomArgs($custom_args)
    {
        if (current($custom_args) instanceof CustomArg) {
            foreach ($custom_args as $custom_arg) {
                $this->addGlobalCustomArg($custom_arg);
            }
        } else {
            foreach ($custom_args as $key => $value) {
                $this->addGlobalCustomArg($key, $value);
            }
        }
    }

    /**
     * Retrieve the custom args attached to a Mail object
     *
     * @return CustomArg[]
     */
    public function getGlobalCustomArgs()
    {
        return $this->custom_args;
    }

    /**
     * Add a unix timestamp allowing you to specify when you want your
     * email to be delivered to a Mail object
     *
     * Note that timestamps added to Personalization objects override
     * global timestamps.
     *
     * @param int|SendAt $send_at A unix timestamp
     * @throws TypeException
     */
    public function setGlobalSendAt($send_at)
    {
        if (!($send_at instanceof SendAt)) {
            $send_at = new SendAt($send_at);
        }
        $this->send_at = $send_at;
    }

    /**
     * Retrieve the unix timestamp attached to a Mail object
     *
     * @return SendAt
     */
    public function getGlobalSendAt()
    {
        return $this->send_at;
    }

    /**
     * Add a batch id to a Mail object
     *
     * @param string|BatchId $batch_id Id for a batch of emails
     *                                 to be sent at the same time
     * @throws TypeException
     */
    public function setBatchId($batch_id)
    {
        if (!($batch_id instanceof BatchId)) {
            $batch_id = new BatchId($batch_id);
        }
        $this->batch_id = $batch_id;
    }

    /**
     * Retrieve the batch id attached to a Mail object
     *
     * @return BatchId
     */
    public function getBatchId()
    {
        return $this->batch_id;
    }

    /**
     * Add a Asm describing how to handle unsubscribes to a Mail object
     *
     * @param int|Asm $group_id Asm object or unsubscribe group id
     *                                   to associate this email with
     * @param array $groups_to_display Array of integer ids of unsubscribe
     *                                   groups to be displayed on the
     *                                   unsubscribe preferences page
     * @throws TypeException
     */
    public function setAsm($group_id, $groups_to_display = null)
    {
        if ($group_id instanceof Asm) {
            $asm = $group_id;
            $this->asm = $asm;
        } else {
            $this->asm = new Asm($group_id, $groups_to_display);
        }
    }

    /**
     * Retrieve the Asm object describing how to handle unsubscribes attached
     * to a Mail object
     *
     * @return Asm
     */
    public function getAsm()
    {
        return $this->asm;
    }

    /**
     * Add the IP pool name to a Mail object
     *
     * @param string|IpPoolName $ip_pool_name The IP Pool that you would
     *                                        like to send this email from
     * @throws TypeException
     */
    public function setIpPoolName($ip_pool_name)
    {
        if ($ip_pool_name instanceof IpPoolName) {
            $this->ip_pool_name = $ip_pool_name->getIpPoolName();
        } else {
            $this->ip_pool_name = new IpPoolName($ip_pool_name);
        }
    }

    /**
     * Retrieve the IP pool name attached to a Mail object
     *
     * @return IpPoolName
     */
    public function getIpPoolName()
    {
        return $this->ip_pool_name;
    }

    /**
     * Add a MailSettings object to a Mail object
     *
     * @param MailSettings $mail_settings A collection of different
     *                                    mail settings that you can
     *                                    use to specify how you would
     *                                    like this email to be handled
     */
    public function setMailSettings($mail_settings)
    {
        $this->mail_settings = $mail_settings;
    }

    /**
     * Retrieve the MailSettings object attached to a Mail object
     *
     * @return MailSettings
     */
    public function getMailSettings()
    {
        return $this->mail_settings;
    }

    /**
     * Set the Bcc settings on a MailSettings object
     *
     * @param bool|BccSettings $enable A BccSettings object or a boolean
     *                                 to determine if this setting is active
     * @param string|null      $email  The email address to be bcc'ed
     * @throws TypeException
     */
    public function setBccSettings($enable, $email = null)
    {
        if (!($this->mail_settings instanceof MailSettings)) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setBccSettings($enable, $email);
    }

    /**
     * Enable bypass list management on a MailSettings object
     *
     * Allows you to bypass all unsubscribe groups and suppressions to ensure
     * that the email is delivered to every single recipient. This should only
     * be used in emergencies when it is absolutely necessary that every
     * recipient receives your email.
     *
     * @throws TypeException
     */
    public function enableBypassListManagement()
    {
        if (!$this->mail_settings instanceof MailSettings) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setBypassListManagement(true);
    }

    /**
     * Disable bypass list management on a MailSettings object
     *
     * Allows you to bypass all unsubscribe groups and suppressions to ensure
     * that the email is delivered to every single recipient. This should only
     * be used in emergencies when it is absolutely necessary that every
     * recipient receives your email.
     *
     * @throws TypeException
     */
    public function disableBypassListManagement()
    {
        if (!($this->mail_settings instanceof MailSettings)) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setBypassListManagement(false);
    }

    /**
     * Set the Footer settings on a MailSettings object
     *
     * @param bool|Footer $enable A Footer object or a boolean
     *                            to determine if this setting is active
     * @param string|null $text The plain text content of the footer
     * @param string|null $html The HTML content of the footer
     *
     * @throws TypeException
     */
    public function setFooter($enable = null, $text = null, $html = null)
    {
        if (!$this->mail_settings instanceof MailSettings) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setFooter($enable, $text, $html);
    }

    /**
     * Enable sandbox mode on a MailSettings object
     *
     * This allows you to send a test email to ensure that your request
     * body is valid and formatted correctly.
     *
     * @throws TypeException
     */
    public function enableSandBoxMode()
    {
        if (!($this->mail_settings instanceof MailSettings)) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setSandBoxMode(true);
    }

    /**
     * Disable sandbox mode on a MailSettings object
     *
     * This allows you to send a test email to ensure that your request
     * body is valid and formatted correctly.
     *
     * @throws TypeException
     */
    public function disableSandBoxMode()
    {
        if (!($this->mail_settings instanceof MailSettings)) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setSandBoxMode(false);
    }

    /**
     * Set the spam check settings on a MailSettings object
     *
     * @param bool|SpamCheck $enable A SpamCheck object or a boolean
     *                                    to determine if this setting is active
     * @param int|null $threshold The threshold used to determine if your
     *                                    content qualifies as spam on a scale from
     *                                    1 to 10, with 10 being most strict, or
     *                                    most likely to be considered as spam
     * @param string|null $post_to_url An Inbound Parse URL that you would like
     *                                    a copy of your email along with the spam
     *                                    report to be sent to
     *
     * @throws TypeException
     */
    public function setSpamCheck($enable = null, $threshold = null, $post_to_url = null)
    {
        if (!$this->mail_settings instanceof MailSettings) {
            $this->mail_settings = new MailSettings();
        }
        $this->mail_settings->setSpamCheck($enable, $threshold, $post_to_url);
    }

    /**
     * Add a TrackingSettings object to a Mail object
     *
     * @param TrackingSettings $tracking_settings Settings to determine how you
     *                                            would like to track the metrics
     *                                            of how your recipients interact
     *                                            with your email
     */
    public function setTrackingSettings($tracking_settings)
    {
        $this->tracking_settings = $tracking_settings;
    }

    /**
     * Retrieve the TrackingSettings object attached to a Mail object
     *
     * @return TrackingSettings
     */
    public function getTrackingSettings()
    {
        return $this->tracking_settings;
    }

    /**
     * Set the click tracking settings on a TrackingSettings object
     *
     * @param bool|ClickTracking $enable A ClickTracking object or a boolean
     *                                        to determine if this setting is active
     * @param bool|null $enable_text Indicates if this setting should be
     *                                        included in the text/plain portion of
     *                                        your email
     *
     * @throws TypeException
     */
    public function setClickTracking($enable = null, $enable_text = null)
    {
        if (!($this->tracking_settings instanceof TrackingSettings)) {
            $this->tracking_settings = new TrackingSettings();
        }
        $this->tracking_settings->setClickTracking($enable, $enable_text);
    }

    /**
     * Set the open tracking settings on a TrackingSettings object
     *
     * @param bool|OpenTracking $enable A OpenTracking object or a boolean
     *                                            to determine if this setting is
     *                                            active
     * @param string|null $substitution_tag Allows you to specify a
     *                                            substitution tag that you can
     *                                            insert in the body of your email
     *                                            at a location that you desire.
     *                                            This tag will be replaced by the
     *                                            open tracking pixel
     *
     * @throws TypeException
     */
    public function setOpenTracking($enable = null, $substitution_tag = null)
    {
        if (!($this->tracking_settings instanceof TrackingSettings)) {
            $this->tracking_settings = new TrackingSettings();
        }
        $this->tracking_settings->setOpenTracking($enable, $substitution_tag);
    }

    /**
     * Set the subscription tracking settings on a TrackingSettings object
     *
     * @param bool|SubscriptionTracking $enable A SubscriptionTracking
     *                                          object or a boolean to
     *                                          determine if this setting
     *                                          is active
     * @param string|null $text Text to be appended to the
     *                          email, with the
     *                          subscription tracking
     *                          link. You may control
     *                          where the link is by using
     *                          the tag <% %>
     * @param string|null $html HTML to be appended to the
     *                          email, with the
     *                          subscription tracking
     *                          link. You may control
     *                          where the link is by using
     *                          the tag <% %>
     * @param string|null $substitution_tag A tag that will be
     *                                      replaced with the
     *                                      unsubscribe URL. for
     *                                      example:
     *                                      [unsubscribe_url]. If this
     *                                      parameter is used, it will
     *                                      override both the text and
     *                                      html parameters. The URL
     *                                      of the link will be placed
     *                                      at the substitution tag’s
     *                                      location, with no
     *                                      additional formatting
     */
    public function setSubscriptionTracking(
        $enable = null,
        $text = null,
        $html = null,
        $substitution_tag = null
    ) {
        if (!($this->tracking_settings instanceof TrackingSettings)) {
            $this->tracking_settings = new TrackingSettings();
        }
        $this->tracking_settings->setSubscriptionTracking(
            $enable,
            $text,
            $html,
            $substitution_tag
        );
    }

    /**
     * Set the Google anatlyics settings on a TrackingSettings object
     *
     * @param bool|Ganalytics $enable A Ganalytics object or a boolean to
     *                                determine if this setting
     *                                is active
     * @param string|null $utm_source Name of the referrer source. (e.g.
     *                                Google, SomeDomain.com, or
     *                                Marketing Email)
     * @param string|null $utm_medium Name of the marketing medium.
     *                                (e.g. Email)
     * @param string|null $utm_term Used to identify any paid keywords.
     * @param string|null $utm_content Used to differentiate your campaign
     *                                 from advertisements
     * @param string|null $utm_campaign The name of the campaign
     *
     * @throws TypeException
     */
    public function setGanalytics(
        $enable = null,
        $utm_source = null,
        $utm_medium = null,
        $utm_term = null,
        $utm_content = null,
        $utm_campaign = null
    ) {
        if (!($this->tracking_settings instanceof TrackingSettings)) {
            $this->tracking_settings = new TrackingSettings();
        }
        $this->tracking_settings->setGanalytics(
            $enable,
            $utm_source,
            $utm_medium,
            $utm_term,
            $utm_content,
            $utm_campaign
        );
    }

    /**
     * Return an array representing a request object for the Twilio SendGrid API
     *
     * @return null|array
     * @throws TypeException
     */
    public function jsonSerialize()
    {
        // Detect if we are using the new dynamic templates
        if ($this->getTemplateId() !== null && strpos($this->getTemplateId()->getTemplateId(), 'd-') === 0) {
            foreach ($this->personalization as $personalization) {
                $personalization->setHasDynamicTemplate(true);
            }
        }

        return array_filter(
            [
                'personalizations' => array_values(array_filter(
                    $this->getPersonalizations(),
                    static function ($value) {
                        return null !== $value && null !== $value->jsonSerialize();
                    }
                )),
                'from' => $this->getFrom(),
                'reply_to' => $this->getReplyTo(),
                'subject' => $this->getGlobalSubject(),
                'content' => $this->getContents(),
                'attachments' => $this->getAttachments(),
                'template_id' => $this->getTemplateId(),
                'sections' => $this->getSections(),
                'headers' => $this->getGlobalHeaders(),
                'categories' => $this->getCategories(),
                'custom_args' => $this->getGlobalCustomArgs(),
                'send_at' => $this->getGlobalSendAt(),
                'batch_id' => $this->getBatchId(),
                'asm' => $this->getASM(),
                'ip_pool_name' => $this->getIpPoolName(),
                'substitutions' => $this->getGlobalSubstitutions(),
                'mail_settings' => $this->getMailSettings(),
                'tracking_settings' => $this->getTrackingSettings()
            ],
            static function ($value) {
                return $value !== null;
            }
        ) ?: null;
    }
}
