<?php namespace ProcessWire;

/**
 * An individual notification item to be part of a NotificationArray for a Page
 * 
 * @class Notification
 * 
 * @property int $pages_id  page ID notification is for (likely a User page)
 * @property int $sort  sort value, as required by Fieldtype
 * @property int $src_id  page ID when notification was generated
 * @property string $title  title/headline
 * @property int $flags  flags: see flag constants 
 * @property int $created  datetime created (unix timestamp)
 * @property int $modified  datetime created (unix timestamp)
 * @property int $qty  quantity of times this notification has been repeated
 * @property array $flagNames Notification flag names
 * 
 * data encoded vars, all optional
 * ===============================
 * @property int $id  unique ID (among others the user may have)
 * @property string $text  extended text
 * @property string $html  extended text as HTML markup 
 * @property string $from  "from" text where applicable, like a class name
 * @property string $icon  fa-icon when applicable
 * @property string $href  clicking notification goes to this URL
 * @property int $progress  progress percent 0-100
 * @property int $expires  datetime after which will automatically be deleted
 *
 */
class Notification extends WireData {

	/**
	 * Flag constants for Notification objects
	 * 
	 * Note that flags 2-32 line up with the same flags from Notice objects
	 * 
	 */

	const flagDebug = 2; 		// Show/save only when the system is in debug mode
	const flagLog = 8; 			// save to log and show
	const flagLogOnly = 16; 	// save to log but don't show
	const flagAllowMarkup = 32;	// allow markup in the title
	
	const flagMessage = 64; 	// informational
	const flagWarning = 4;		// warning 
	const flagError = 128;		// error 
	
	const flagNotice = 256; 	// Show only as a single-request notice (not stored in DB)
	const flagSession = 512; 	// Notification lasts for only this session (not stored in DB)
	const flagEmail = 1024; 	// title and body will also be emailed to user (if page is user)
	const flagOpen = 2048;		// notification will automatically open the text/html area (no click required)
	
	const flagNoGhost = 4096; 	// disable showing of a notification ghost
	const flagAnnoy = 8192; 	// rather than just update bug counter, notification will pop up at top of screen
	const flagShown = 16384; 	// has this flag once the notification has been sent to the UI at least once
	const flagAlert = 32768;	// show an alert that requires acknowledgement (use with flagSession only)

	/**
	 * Provides a name for each of the flags
	 * 
	 * @var array
	 * 
	 */
	static protected $_flagNames = array(
		self::flagDebug => 'debug',
		self::flagLog => 'log', 
		self::flagLogOnly => 'log-only', 
		self::flagAllowMarkup => 'markup', 
		self::flagMessage => 'message',
		self::flagWarning => 'warning',
		self::flagError => 'error',
		self::flagNotice => 'notice',
		self::flagSession => 'session',
		self::flagEmail => 'email',
		self::flagOpen => 'open',
		self::flagNoGhost => 'no-ghost', 
		self::flagAnnoy => 'annoy',
		self::flagShown => 'shown', 
		self::flagAlert => 'alert', 
		);

	/**
	 * Page that this Notification belongs to
	 * 
	 * @var Page
	 * 
	 */
	protected $page; 

	/**
	 * Construct a new Notification
	 *
	 */
	public function __construct() {

		// db native vars
		$this->set('pages_id', 0); 	// page ID notification is for (likely a User page)
		$this->set('sort', 0); 		// sort value, as required by Fieldtype
		$this->set('src_id', 0); 	// page ID when notification was generated
		$this->set('title', ''); 	// title/headline
		$this->set('flags', 0);		// flags: see flag constants 
		$this->set('created', 0); 	// datetime created (unix timestamp)
		$this->set('modified', 0); 	// datetime created (unix timestamp)
		$this->set('qty', 1); 		// quantity of times this notification has been repeated

		// data encoded vars, all optional
		$this->set('id', ''); 		// unique ID (among others the user may have)
		$this->set('text', ''); 	// extended text
		$this->set('html', ''); 	// extended text as HTML markup 
		$this->set('from', '');		// "from" text where applicable, like a class name
		$this->set('icon', ''); 	// fa-icon when applicable
		$this->set('href', ''); 	// clicking notification goes to this URL
		$this->set('progress', 0); 	// progress percent 0-100
		$this->set('expires', 0); 	// datetime after which will automatically be deleted
		
	}

	/*
	 * Fluent interface methods
	 * 
	 */

	public function title($value) { return $this->set('title', $value); }
	public function text($value) { return $this->set('text', $value); }
	public function html($value) { return $this->set('html', $value); }
	public function from($value) { return $this->set('from', $value); }
	public function icon($value) { return $this->set('icon', $value); }
	public function href($value) { return $this->set('href', $value); }
	public function progress($value) { return $this->set('progress', $value); }
	public function expires($value) { return $this->set('expires', $value); }
	public function flag($value) { return $this->setFlag($value, true); }
	public function flags($value) { return $this->setFlags($value, true); }

	/**
	 * Does this Notification match the given flag name(s)?
	 * 
	 * @param string $name
	 * @return bool
	 * 
	 */
	public function is($name) {
		$flags = $this->flagNamesToFlags($name); 
		$is = 0;
		foreach($flags as $flag) { 
			if($this->flags & $flag) $is++;
		}
		return $is == count($flags); 
	}

	/**
	 * Given a flag name, return the corresponding flag value
	 * 
	 * @param string $name
	 * @return int mixed
	 * @throws WireException if given unknown flag
	 * 
	 */
	protected function flagNameToFlag($name) {
		if(is_string($name)) {
			$flag = array_search($name, self::$_flagNames); 
			if(!$flag) throw new WireException("Unknown flag: $name"); 
		} else {
			$flag = $name;
			if(!isset(self::$_flagNames[$flag])) throw new WireException("Unknown flag: $flag"); 
		}
		return $flag;
	}

	/**
	 * Given multiple space separated flag names, return array of flag values
	 * 
	 * @param string $names space separted, will also accept CSV
	 * @return array of flag name => flag value
	 * 
	 */
	protected function flagNamesToFlags($names) {
		if(strpos($names, ',') !== false) $names = str_replace(',', ' ', $names); 
		$names = explode(' ', $names); 
		$flags = array();
		foreach($names as $name) {
			if(empty($name)) continue; 
			$flag = $this->flagNameToFlag($name); 
			if($flag) $flags[$name] = $flag;
		}
		return $flags; 
	}

	/**
	 * Set a named flag
	 *
	 * @param string|int $name Flag to set
	 * @param bool $add True to add flag, false to remove
	 * @return self
	 *
	 */
	public function setFlag($name, $add = true) {

		$flag = ctype_digit("$name") ? (int) $name : $this->flagNameToFlag($name); 	
		$flags = parent::get('flags'); 

		if($add) {
			// add flag
			if($flags & $flag) {
				// flag is already set
			} else {
				$flags = $flags | $flag;
				parent::set('flags', $flags); 
			}
		} else {
			// remove flag
			if($flags & $flag) {
				// flag is set, remove it
				$flags = $flags & ~$flag;
				parent::set('flags', $flags); 
			} else {
				// flag is not set
			}
		}

		return $this; 
	}

	/**
	 * Add the given flag name(s) (shortcut for setFlag)
	 * 
	 * @param string $name One or more space-separated flag names
	 * @return self
	 * 
	 */
	public function addFlag($name) {
		return $this->setFlags($name, true); 
	}

	/**
	 * Remove the given flag name(s) (shortcut for setFlag)
	 * 
	 * @param string $name One or more space-separated flag names
	 * @return self
	 * 
	 */
	public function removeFlag($name) {
		return $this->setFlags($name, false); 
	}

	/**
	 * Set multiple flags
	 * 
	 * @param string $names space separated string of flag names
	 * @param bool $add True to add, false to remove
	 * @return self
	 * 
	 */
	public function setFlags($names, $add = true) {
		
		if(ctype_digit("$names")) {
			// likely a flag or combined flags in bitmask
			$flags = (int) $names;
			// iterate through known flags to see which are set
			foreach(self::$_flagNames as $flag => $name) {
				// if it's a recognized/valid flag, set it 
				if($flags & $flag) $this->setFlag($flag, $add); 
			}
			return $this;
		}
	
		// optimization if this was called with just one flag name
		if(strpos($names, ',') === false && strpos($names, ' ') === false) {
			$this->setFlag($names, $add); 
		}
	
		// named flags
		$flags = $this->flagNamesToFlags($names); 
		foreach($flags as $name => $flag) {
			$this->setFlag($flag, $add); 	
		}
		
		return $this; 
	}

	/**
	 * Set a value to the Notification
	 * 
	 * Note: setting the 'expires' value accepts either a future date, or a quantity of seconds 
	 * in the future relative to now. 
	 * 
	 * @param string $key
	 * @param mixed $value
	 * @return self|Notification|WireData
	 *
	 */
	public function set($key, $value) {
 
		if($key == 'page') {
			$this->page = $value; 
			return $this; 

		} else if($key == 'created' || $key == 'modified' || $key == 'expires') {
			// convert date string to unix timestamp
			if($value && !ctype_digit("$value")) $value = strtotime($value); 	

			// sanitized date value is always an integer
			$value = (int) $value; 
			
			if($key == 'expires' && $value > 0 && $value < strtotime("-10 YEARS")) {
				// assume this is a time relative to now
				$value = time() + $value;
			}

		} else if($key == 'title') {
			if($this->flags & self::flagAllowMarkup) {
				// accept value as-is
			} else {
				// regular text sanitizer
				$value = $this->sanitizer->text($value);
			}
				
		} else if($key == 'from') {
			// regular text sanitizer
			$value = $this->sanitizer->text($value); 

		} else if($key == 'text') {
			// regular text sanitizer
			$value = $this->sanitizer->textarea($value); 

		} else if(in_array($key, array('pages_id', 'sort', 'src_id', 'flags', 'progress'))) {
			$value = (int) $value; 
		}

		return parent::set($key, $value); 
	}

	/**
	 * Return an ID string/hash unique to this Notification within the page that its on
	 * 
	 * The text/html, modified date, expires date, and icon may change without affecting the id. 
	 * 
	 * @return mixed|null|string
	 * 
	 */
	public function getID() {

		$id = parent::get('id'); 
		if($id) return $id; 

		$id = 	parent::get('title') . ',' . 
				parent::get('created') . ',' . 
				parent::get('from') . ',' . 
				parent::get('src_id') . ',' . 
				($this->page ? $this->page->id : '?'); // . ',' . 
				//parent::get('flags');

		return 'noID' . md5($id); 
	}
	
	/**
	 * Return an string hash for comparing other notifications to see if they contain the same content
	 * 
	 * Hash specifically excludes consideration of dates (created, modified, expires)
	 *
	 * @return string
	 *
	 */
	public function getHash() {

		$id = 	trim(parent::get('title')) . ',' .
				// parent::get('from') . ',' .
				// parent::get('src_id') . ',' .
				// ($this->page ? $this->page->id : '?') . ',' . 
				// parent::get('flags') . ',' . 
				// parent::get('icon') . ',' . 
				trim(parent::get('text')) . ',' . 
				trim(parent::get('html'));

		return md5($id);
	}

	/**
	 * Retrieve a value from the Notification
	 * 
	 * @param string $key
	 * @return mixed
	 *
	 */
	public function get($key) {

		if($key == 'id') return $this->getID();
		if($key == 'page') return $this->page; 
		if($key == 'hash') return $this->getHash();

		if($key == 'flagNames') {
			$flags = parent::get('flags');
			$flagNames = array();
			foreach(self::$_flagNames as $val => $name) {
				if($flags & $val) $flagNames[$val] = $name;
			}
			return $flagNames;
		}

		$value = parent::get($key); 

		// if the page's output formatting is on, then we'll return formatted values
		if($this->page && $this->page->of()) {

			if($key == 'created' || $key == 'expires' || $key == 'modified') {
				// format a unix timestamp to a date string
				$value = date('Y-m-d H:i:s', $value); 				

			} else if($key == 'title' || $key == 'text' || $key == 'from') {
				// return entity encoded versions of strings
				if($key == 'title' && ($this->flags & self::flagAllowMarkup)) {
					// leave title alone when markup is allowed
				} else {
					$value = $this->sanitizer->entities($value); 
				}
			}
		} else {
			if($key == 'created' && !$value) $value = time();
		}

		return $value; 
	}

	/**
	 * Is this Notification expired?
	 * 
	 * @return bool
	 * 
	 */
	public function isExpired() {
		return ($this->expires > 0 && $this->expires <= time()); 
	}

	/**
	 * String value of a Notification
	 * 
	 * @return string
	 * 
	 */
	public function __toString() {
		$str = $this->title; 
		$str .= " (" . implode(', ', $this->get('flagNames')) . ")";
		return $str; 
	}


}

