<?php namespace SimpleSoftwareIO\SMS;

/**
 * Simple-SMS
 * Simple-SMS is a package made for Laravel to send/receive (polling/pushing) text messages.
 *
 * Part of this file is based on the Illuminate\Mail system.
 *
 * @link http://www.simplesoftware.io
 * @author SimpleSoftware support@simplesoftware.io
 *
 */

use Closure;
use Illuminate\Log\Writer;
use Illuminate\Support\Str;
use SuperClosure\Serializer;
use Illuminate\Support\Serial;
use Illuminate\Queue\QueueManager;
use Illuminate\Container\Container;
use SimpleSoftwareIO\SMS\Drivers\DriverInterface;

class SMS
{
    /**
     * The Driver Interface instance.
     *
     * @var \SimpleSoftwareIO\SMS\Drivers\DriverInterface
     */
    protected $driver;

    /**
     * The log writer instance.
     *
     * @var \Illuminate\Log\Writer
     */
    protected $logger;

    /**
     * Determines if a message should be sent or faked.
     *
     * @var boolean
     */
    protected $pretending = false;

    /**
     * The IOC Container
     *
     * @var \Illuminate\Container\Container
     */
    protected $container;

    /**
     * The global from address
     *
     * @var string
     */
    protected $from;

    /**
     * Holds the queue instance.
     *
     * @var \Illuminate\Queue\QueueManager
     */
    protected $queue;

    /**
     * Creates the SMS instance.
     *
     * @param DriverInterface $driver
     */
    public function __construct(DriverInterface $driver)
    {
        $this->driver = $driver;
    }

    /**
     * Changes the set SMS driver
     *
     * @param $driver
     */
    public function driver($driver)
    {
        $this->container['sms.sender'] = $this->container->share(function ($app) use ($driver) {
            return (new DriverManager($app))->driver($driver);
        });

        $this->driver = $this->container['sms.sender'];
    }

    /**
     * Send a SMS.
     *
     * @param string $view The desired view.
     * @param array $data The data that needs to be passed into the view.
     * @param \Closure $callback The methods that you wish to fun on the message.
     * @return \SimpleSoftwareIO\SMS\OutgoingMessage The outgoing message that was sent.
     */
    public function send($view, $data, $callback)
    {
        $data['message'] = $message = $this->createOutgoingMessage();

        //We need to set the properties so that we can later pass this onto the Illuminate Mailer class if the e-mail gateway is used.
        $message->view($view);
        $message->data($data);

        call_user_func($callback, $message);

        if (!$this->pretending)
        {
            $this->driver->send($message);
        }
        elseif (isset($this->logger))
        {
            $this->logMessage($message);
        }

        return $message;
    }

    /**
     * Logs that a message was sent.
     *
     * @param $message
     */
    protected function logMessage($message)
    {
        $numbers = implode(" , ", $message->getTo());

        $this->logger->info("Pretending to send SMS message to: $numbers");
    }

    /**
     * Creates a new Message instance.
     *
     * @return \SimpleSoftwareIO\SMS\OutgoingMessage
     */
    protected function createOutgoingMessage()
    {
        $message = new OutgoingMessage($this->container['view']);

        //If a from address is set, pass it along to the message class.
        if (isset($this->from)) {
            $message->from($this->from);
        }

        return $message;
    }

    /**
     * Returns if the message should be faked when sent or not.
     *
     * @return boolean
     */
    public function isPretending()
    {
        return $this->pretending;
    }

    /**
     * Fake sending a SMS
     *
     * @param $view The desired view
     * @param $data The data to fill the view
     * @param $callback The message callback
     */
    public function pretend($view, $data, $callback)
    {
        $this->setPretending(true);
        $this->send($view, $data, $callback);
    }

    /**
     * Sets if SMS should be fake send a SMS
     *
     * @param bool $pretend
     */
    public function setPretending($pretend = false)
    {
        $this->pretending = $pretend;
    }

    /**
     * Sets the IoC container
     *
     * @param Container $container
     */
    public function setContainer(Container $container)
    {
        $this->container = $container;
    }

    /**
     * Sets the number that message should always be sent from.
     *
     * @param $number
     */
    public function alwaysFrom($number)
    {
        $this->from = $number;
    }

    /**
     * Set the log writer instance.
     *
     * @param  \Illuminate\Log\Writer $logger
     * @return $this
     */
    public function setLogger(Writer $logger)
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * Queues a SMS message.
     *
     * @param string $view The desired view.
     * @param array $data An array of data to fill the view.
     * @param  \Closure|string $callback The callback to run on the Message class.
     * @param null|string $queue The desired queue to push the message to.
     * @return void
     */
    public function queue($view, $data, $callback, $queue = null)
    {
        $callback = $this->buildQueueCallable($callback);

        $this->queue->push('sms@handleQueuedMessage', compact('view', 'data', 'callback'), $queue);
    }

    /**
     * Queues a SMS message to a given queue.
     *
     * @param null|string $queue The desired queue to push the message to.
     * @param string $view The desired view.
     * @param array $data An array of data to fill the view.
     * @param  \Closure|string $callback The callback to run on the Message class.
     * @return void
     */
    public function queueOn($queue, $view, array $data, $callback)
    {
        $this->queue($view, $data, $callback, $queue);
    }

    /**
     * Queues a message to be sent a later time.
     *
     * @param int $delay The desired delay in seconds
     * @param string $view The desired view.
     * @param array $data An array of data to fill the view.
     * @param  \Closure|string $callback The callback to run on the Message class.
     * @param null|string $queue The desired queue to push the message to.
     * @return void
     */
    public function later($delay, $view, array $data, $callback, $queue = null)
    {
        $callback = $this->buildQueueCallable($callback);

        $this->queue->later($delay, 'mailer@handleQueuedMessage', compact('view', 'data', 'callback'), $queue);
    }

    /**
     * Queues a message to be sent a later time on a given queue.
     *
     * @param null|string $queue The desired queue to push the message to.
     * @param int $delay The desired delay in seconds
     * @param string $view The desired view.
     * @param array $data An array of data to fill the view.
     * @param \Closure|string $callback The callback to run on the Message class.
     * @return void
     */
    public function laterOn($queue, $delay, $view, array $data, $callback)
    {
        $this->later($delay, $view, $data, $callback, $queue);
    }

    /**
     * Builds the callable for a queue.
     *
     * @param \Closure|string $callback The callback to be serialized
     * @return string
     */
    protected function buildQueueCallable($callback)
    {
        if (! $callback instanceof Closure) {
            return $callback;
        }
        return (new Serializer)->serialize($callback);
    }

    /**
     * Handles a queue message.
     *
     * @param \Illuminate\Queue\Jobs\Job $job
     * @param array $data
     * @return void
     */
    public function handleQueuedMessage($job, $data)
    {
        $this->send($data['view'], $data['data'], $this->getQueuedCallable($data));

        $job->delete();
    }

    /**
     * Gets the callable for a queued message.
     *
     * @param array $data
     * @return mixed
     */
    protected function getQueuedCallable(array $data)
    {
        if (Str::contains($data['callback'], 'SerializableClosure')) {
            return unserialize($data['callback'])->getClosure();
        }
        return $data['callback'];
    }

    /**
     * Set the queue manager instance.
     *
     * @param  \Illuminate\Queue\QueueManager  $queue
     * @return $this
     */
    public function setQueue(QueueManager $queue)
    {
        $this->queue = $queue;
    }

    /**
     * Receives a SMS via a push request.
     *
     * @return IncomingMessage
     */
    public function receive()
    {
        //Passes all of the request onto the driver.
        $raw = $this->container['Input'];
        return $this->driver->receive($raw);
    }

    /**
     * Queries the provider for a list of messages.
     *
     * @param array $options The options to pass onto a provider.  See each provider for a list of options.
     * @return array Returns an array of IncomingMessage objects.
     */
    public function checkMessages(Array $options = array())
    {
        return $this->driver->checkMessages($options);
    }

    /**
     * Gets a message by it's ID.
     *
     * @param $messageId The requested messageId.
     * @return IncomingMessage
     */
    public function getMessage($messageId)
    {
        return $this->driver->getMessage($messageId);
    }
}