<?php
/**
 * A DataFormatter object handles transformation of data from SilverStripe model objects to a particular output
 * format, and vice versa.  This is most commonly used in developing RESTful APIs.
 *
 * @package framework
 * @subpackage formatters
 */
abstract class DataFormatter extends Object {

	/**
	 * Set priority from 0-100.
	 * If multiple formatters for the same extension exist,
	 * we select the one with highest priority.
	 *
	 * @var int
	 */
	private static $priority = 50;

	/**
	 * Follow relations for the {@link DataObject} instances
	 * ($has_one, $has_many, $many_many).
	 * Set to "0" to disable relation output.
	 *
	 * @todo Support more than one nesting level
	 *
	 * @var int
	 */
	public $relationDepth = 1;

	/**
	 * Allows overriding of the fields which are rendered for the
	 * processed dataobjects. By default, this includes all
	 * fields in {@link DataObject::inheritedDatabaseFields()}.
	 *
	 * @var array
	 */
	protected $customFields = null;

	/**
	 * Allows addition of fields
	 * (e.g. custom getters on a DataObject)
	 *
	 * @var array
	 */
	protected $customAddFields = null;

	/**
	 * Allows to limit or add relations.
	 * Only use in combination with {@link $relationDepth}.
	 * By default, all relations will be shown.
	 *
	 * @var array
	 */
	protected $customRelations = null;

	/**
	 * Fields which should be expicitly excluded from the export.
	 * Comes in handy for field-level permissions.
	 * Will overrule both {@link $customAddFields} and {@link $customFields}
	 *
	 * @var array
	 */
	protected $removeFields = null;

	/**
	 * Specifies the mimetype in which all strings
	 * returned from the convert*() methods should be used,
	 * e.g. "text/xml".
	 *
	 * @var string
	 */
	protected $outputContentType = null;

	/**
	 * Used to set totalSize properties on the output
	 * of {@link convertDataObjectSet()}, shows the
	 * total number of records without the "limit" and "offset"
	 * GET parameters. Useful to implement pagination.
	 *
	 * @var int
	 */
	protected $totalSize;

	/**
	 * Get a DataFormatter object suitable for handling the given file extension.
	 *
	 * @param string $extension
	 * @return DataFormatter
	 */
	public static function for_extension($extension) {
		$classes = ClassInfo::subclassesFor("DataFormatter");
		array_shift($classes);
		$sortedClasses = array();
		foreach($classes as $class) {
			$sortedClasses[$class] = singleton($class)->stat('priority');
		}
		arsort($sortedClasses);
		foreach($sortedClasses as $className => $priority) {
			$formatter = new $className();
			if(in_array($extension, $formatter->supportedExtensions())) {
				return $formatter;
			}
		}
	}

	/**
	 * Get formatter for the first matching extension.
	 *
	 * @param array $extensions
	 * @return DataFormatter
	 */
	public static function for_extensions($extensions) {
		foreach($extensions as $extension) {
			if($formatter = self::for_extension($extension)) return $formatter;
		}

		return false;
	}

	/**
	 * Get a DataFormatter object suitable for handling the given mimetype.
	 *
	 * @param string $mimeType
	 * @return DataFormatter
	 */
	public static function for_mimetype($mimeType) {
		$classes = ClassInfo::subclassesFor("DataFormatter");
		array_shift($classes);
		$sortedClasses = array();
		foreach($classes as $class) {
			$sortedClasses[$class] = singleton($class)->stat('priority');
		}
		arsort($sortedClasses);
		foreach($sortedClasses as $className => $priority) {
			$formatter = new $className();
			if(in_array($mimeType, $formatter->supportedMimeTypes())) {
				return $formatter;
			}
		}
	}

	/**
	 * Get formatter for the first matching mimetype.
	 * Useful for HTTP Accept headers which can contain
	 * multiple comma-separated mimetypes.
	 *
	 * @param array $mimetypes
	 * @return DataFormatter
	 */
	public static function for_mimetypes($mimetypes) {
		foreach($mimetypes as $mimetype) {
			if($formatter = self::for_mimetype($mimetype)) return $formatter;
		}

		return false;
	}

	/**
	 * @param array $fields
	 */
	public function setCustomFields($fields) {
		$this->customFields = $fields;
	}

	/**
	 * @return array
	 */
	public function getCustomFields() {
		return $this->customFields;
	}

	/**
	 * @param array $fields
	 */
	public function setCustomAddFields($fields) {
		$this->customAddFields = $fields;
	}

	/**
	 * @param array $relations
	 */
	public function setCustomRelations($relations) {
		$this->customRelations = $relations;
	}

	/**
	 * @return array
	 */
	public function getCustomRelations() {
		return $this->customRelations;
	}

	/**
	 * @return array
	 */
	public function getCustomAddFields() {
		return $this->customAddFields;
	}

	/**
	 * @param array $fields
	 */
	public function setRemoveFields($fields) {
		$this->removeFields = $fields;
	}

	/**
	 * @return array
	 */
	public function getRemoveFields() {
		return $this->removeFields;
	}

	public function getOutputContentType() {
		return $this->outputContentType;
	}

	/**
	 * @param int $size
	 */
	public function setTotalSize($size) {
		$this->totalSize = (int)$size;
	}

	/**
	 * @return int
	 */
	public function getTotalSize() {
		return $this->totalSize;
	}

	/**
	 * Returns all fields on the object which should be shown
	 * in the output. Can be customised through {@link self::setCustomFields()}.
	 *
	 * @todo Allow for custom getters on the processed object (currently filtered through inheritedDatabaseFields)
	 * @todo Field level permission checks
	 *
	 * @param DataObject $obj
	 * @return array
	 */
	protected function getFieldsForObj($obj) {
		$dbFields = array();

		// if custom fields are specified, only select these
		if(is_array($this->customFields)) {
			foreach($this->customFields as $fieldName) {
				// @todo Possible security risk by making methods accessible - implement field-level security
				if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
					$dbFields[$fieldName] = $fieldName;
				}
			}
		} else {
			// by default, all database fields are selected
			$dbFields = $obj->inheritedDatabaseFields();
		}

		if(is_array($this->customAddFields)) {
			foreach($this->customAddFields as $fieldName) {
				// @todo Possible security risk by making methods accessible - implement field-level security
				if($obj->hasField($fieldName) || $obj->hasMethod("get{$fieldName}")) {
					$dbFields[$fieldName] = $fieldName;
				}
			}
		}

		// add default required fields
		$dbFields = array_merge($dbFields, array('ID'=>'Int'));

		if(is_array($this->removeFields)) {
			$dbFields = array_diff_key($dbFields, array_combine($this->removeFields,$this->removeFields));
		}

		return $dbFields;
	}

	/**
	 * Return an array of the extensions that this data formatter supports
	 */
	abstract public function supportedExtensions();

	abstract public function supportedMimeTypes();


	/**
	 * Convert a single data object to this format.  Return a string.
	 */
	abstract public function convertDataObject(DataObjectInterface $do);

	/**
	 * Convert a data object set to this format.  Return a string.
	 */
	abstract public function convertDataObjectSet(SS_List $set);

	/**
	 * @param string $strData HTTP Payload as string
	 */
	public function convertStringToArray($strData) {
		user_error('DataFormatter::convertStringToArray not implemented on subclass', E_USER_ERROR);
	}

}