<?php defined('SYSPATH') or die('No direct access allowed.');

/**
 * This class provides email template handling in the same manor as views.
 *
 * @package    lnApp
 * @category   Helpers
 * @author     Deon George
 * @copyright  (c) 2014 Deon George
 * @license    http://dev.leenooks.net/license.html
 */
abstract class lnApp_Email extends Kohana_Email {
	// Array of global variables
	protected static $_global_data = array();

	// View filename
	protected $_file = array();

	// Array of local variables
	protected $_data = array();

	// Email Details
	protected $_email = array();

	// Our email subject key
	private $_key;

	private $_mime = array(
		'txt'=>'text/plain',
		'htm'=>'text/html',
	);

	/**
	 * Sets the initial view filename and local data. Views should almost
	 * always only be created using [Email::factory].
	 *
	 *     $email = new Email($file);
	 *
	 * @param   string  $file   view filename
	 * @param   array   $data   array of values
	 * @return  void
	 * @uses    Email::set_filename
	 */
	public function __construct($file=NULL,array $data=NULL) {
		$this->_key = $file;

		if ($file !== NULL)
			$this->set_filename($file);

		// Add the values to the current data
		if ($data !== NULL)
			$this->_data = $data + $this->_data;
	}

	/**
	 * Magic method, searches for the given variable and returns its value.
	 * Local variables will be returned before global variables.
	 *
	 *     $value = $email->foo;
	 *
	 * [!!] If the variable has not yet been set, an exception will be thrown.
	 *
	 * @param   string  $key    variable name
	 * @return  mixed
	 * @throws  Kohana_Exception
	 */
	public function & __get($key) {
		if (array_key_exists($key,$this->_data))
			return $this->_data[$key];

		elseif (array_key_exists($key,$this->_email))
			return $this->_email[$key];

		elseif (array_key_exists($key,Email::$_global_data))
			return Email::$_global_data[$key];

		else
			throw new Kohana_Exception('View variable is not set: :var',array(':var'=>$key));
	}

	/**
	 * Magic method, determines if a variable is set.
	 *
	 *     isset($email->foo);
	 *
	 * [!!] `NULL` variables are not considered to be set by [isset](http://php.net/isset).
	 *
	 * @param   string  $key    variable name
	 * @return  boolean
	 */
	public function __isset($key) {
		return (isset($this->_data[$key]) OR isset($this->_email[$key]) OR isset(Email::$_global_data[$key]));
	}

	/**
	 * Magic method, calls [Email::set] with the same parameters.
	 *
	 *     $email->foo = 'something';
	 *
	 * @param   string  $key    variable name
	 * @param   mixed   $value  value
	 * @return  void
	 */
	public function __set($key,$value) {
		switch ($key) {
			case 'from':
			case 'bcc':
			case 'to':
				if (! is_array($value) OR ! array_intersect(array('email','account'),array_keys($value)))
					throw new Email_Exception('Values for to should be an array of either "mail" or "account", however :value was given',
						array(':value'=>serialize($value)));

			case 'subject':
				$this->_email[$key] = $value;
				break;

			default:
				$this->set($key,$value);
		}
	}

	/**
	 * Magic method, returns the output of [Email::render].
	 *
	 * @return  string
	 * @uses    Email::render
	 */
	public function __toString() {
		$e = Email::connect();

		try {
			return (string)$this->render();

		} catch (Exception $e) {
			/**
			 * Display the exception message.
			 *
			 * We use this method here because it's impossible to throw an
			 * exception from __toString().
			 */
			$error_response = Kohana_Exception::_handler($e);

			return $error_response->body();
		}
	}

	/**
	 * Magic method, unsets a given variable.
	 *
	 *     unset($email->foo);
	 *
	 * @param   string  $key    variable name
	 * @return  void
	 */
	public function __unset($key) {
		unset($this->_data[$key],$this->_email[$key],Email::$_global_data[$key]);
	}

	/**
	 * Assigns a value by reference. The benefit of binding is that values can
	 * be altered without re-setting them. It is also possible to bind variables
	 * before they have values. Assigned values will be available as a
	 * variable within the view file:
	 *
	 *     // This reference can be accessed as $ref within the view
	 *     $email->bind('ref', $bar);
	 *
	 * @param   string  $key    variable name
	 * @param   mixed   $value  referenced variable
	 * @return  $this
	 */
	public function bind($key,&$value) {
		$this->_data[$key] =& $value;

		return $this;
	}

	/**
	 * Assigns a global variable by reference, similar to [Email::bind], except
	 * that the variable will be accessible to all views.
	 *
	 *     Email::bind_global($key,$value);
	 *
	 * @param   string  $key    variable name
	 * @param   mixed   $value  referenced variable
	 * @return  void
	 */
	public static function bind_global($key,&$value) {
		Email::$_global_data[$key] =& $value;
	}

	/**
	 * Captures the output that is generated when a view is included.
	 * The view data will be extracted to make local variables. This method
	 * is static to prevent object scope resolution.
	 *
	 *     $output = Email::capture($file,$data);
	 *
	 * @param   string  $kohana_view_filename   filename
	 * @param   array   $kohana_view_data       variables
	 * @return  string
	 */
	protected static function capture($kohana_view_filename,array $kohana_view_data) {
		return Email::complete(file_get_contents($kohana_view_filename),Arr::merge(Email::$_global_data,$kohana_view_data));
	}

	/**
	 * Exchange the variables for values
	 */
	private static function complete($output,$data) {
		foreach (Email::variables($output) as $v)
			$output = str_replace('$'.$v.'$',$data[$v],$output);

		return $output;
	}

	/**
	 * Deliver the email
	 */
	public function deliver(array $admin=array()) {
		// @todo - Setup queue mode
		return Email::connect()->send($this->render(NULL,$admin));
	}

	/**
	 * Get email details
	 */
	private function email($key) {
		if (is_array($this->_email[$key]) AND isset($this->_email[$key]['email']))
			return $this->_email[$key]['email'];
		else
			return (is_array($this->_email[$key]) AND isset($this->_email[$key]['account'])) ? $this->_email[$key]['account'] : $this->_email[$key];
	}

	/**
	 * Returns a new Email object. If you do not define the "file" parameter,
	 * you must call [Email::set_filename].
	 *
	 *     $email = Email::factory($file);
	 *
	 * @param   string  $file   email filename
	 * @param   array   $data   array of values
	 * @return  Email
	 */
	public static function factory($file=NULL,array $data=NULL) {
		return new Email($file,$data);
	}

	/**
	 * Renders the view object to a string. Global and local data are merged
	 * and extracted to create local variables within the view file.
	 *
	 *     $output = $email->render();
	 *
	 * [!!] Global variables with the same key name as local variables will be
	 * overwritten by the local variable.
	 *
	 * @param   string  $file   view filename
	 * @return  string
	 * @throws  Email_Exception
	 * @uses    Email::capture
	 */
	public function render($file=NULL,array $admin=array()) {
		if ($file !== NULL)
			$this->set_filename($file);

		if (empty($this->_file))
			throw new Email_Exception('You must set the file to use within your email before rendering');

		if ($x=array_diff(array('to','from','subject'),array_keys($this->_email)))
			throw new Email_Exception('You are missing :missing from the Email',array(':missing'=>join('|',$x)));

		$sm = Swift_Message::newInstance()
			->setFrom($this->email('from'))
			->setSubject($this->email('subject'));

		foreach ($this->_file as $file) {
			$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
			$sm->addPart(Email::capture($file,$this->_data),array_key_exists($ext,$this->_mime) ? $this->_mime[$ext] : File::mime($file));
		}

		// Our list of BCC recipients
		if (Kohana::$config->load('debug')->email_bcc_admin AND $x=Arr::merge($this->email('bcc'),Kohana::$config->load('debug')->email_bcc_admin))
			$sm->setBcc($x);

		$sm->setTo(($admin OR ($admin = Config::testmail($this->_key))) ? $admin : $this->email('to'));

		return $sm;
	}

	/**
	 * Assigns a variable by name. Assigned values will be available as a
	 * variable within the view file:
	 *
	 *     // This value can be accessed as $foo within the view
	 *     $email->set('foo','my value');
	 *
	 * You can also use an array to set several values at once:
	 *
	 *     // Create the values $food and $beverage in the view
	 *     $email->set(array('food' => 'bread', 'beverage' => 'water'));
	 *
	 * @param   string  $key    variable name or an array of variables
	 * @param   mixed   $value  value
	 * @return  $this
	 */
	public function set($key,$value=NULL) {
		if (is_array($key))
			foreach ($key as $name => $value)
				$this->_data[$name] = $value;

		else
			$this->_data[$key] = $value;

		return $this;
	}

	/**
	 * Sets the view filename.
	 *
	 *     $email->set_filename($file);
	 *
	 * @param   string  $file   view filename
	 * @return  Email
	 * @throws  Email_Exception
	 */
	public function set_filename($file) {
		foreach (array_keys($this->_mime) as $ext)
			if ($path=Kohana::find_file('email',$file,$ext))
				// Store the file path locally
				array_push($this->_file,$path);

		if (! $this->_file)
			throw new Email_Exception('The requested email :file could not be found',array(':file'=>$file));

		return $this;
	}

	/**
	 * Sets a global variable, similar to [Email::set], except that the
	 * variable will be accessible to all views.
	 *
	 *     Email::set_global($name, $value);
	 *
	 * @param   string  $key    variable name or an array of variables
	 * @param   mixed   $value  value
	 * @return  void
	 */
	public static function set_global($key,$value=NULL) {
		if (is_array($key))
			foreach ($key as $key2 => $value)
				Email::$_global_data[$key2] = $value;

		else
			Email::$_global_data[$key] = $value;
	}

	/**
	 * Extract the variables in the text
	 */
	public static function variables($output) {
		$results = array();
		$matches = array();

		preg_match_all('/\$([A-Z0-9_]+)\$/U',$output,$matches,PREG_OFFSET_CAPTURE);

		foreach ($matches[1] as $k => $v)
			if (! in_array($v[0],$results))
				array_push($results,$v[0]);

		return $results;
	}
}
?>
