<?php

/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
 */

/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoTests\models\runner\time;

/**
 * Class TimePoint
 *
 * Describes a temporal point by storing a timestamp with microseconds and some flags.
 * A TimePoint can describe a START or a END temporal point used to define a time range.
 * Each TimePoint can be related to a target (CLIENT or SERVER).
 * A list of tags can be attached to a TimePoint to explain its role or its context.
 *
 * @package oat\taoTests\models\runner\time
 */
class TimePoint implements ArraySerializable, \Serializable, \JsonSerializable
{
    /**
     * Type of TimePoint: start of range
     */
    const TYPE_START = 1;

    /**
     * Type of TimePoint: end of range
     */
    const TYPE_END = 2;

    /**
     * Represents all types of TimePoint
     */
    const TYPE_ALL = 3;

    /**
     * Type of TimePoint target: client side
     */
    const TARGET_CLIENT = 1;

    /**
     * Type of TimePoint target: server side
     */
    const TARGET_SERVER = 2;

    /**
     * Represents all types of TimePoint targets
     */
    const TARGET_ALL = 3;

    /**
     * The decimal precision used to compare timestamps
     */
    const PRECISION = 10000;

    /**
     * The timestamp representing the TimePoint
     * @var float
     */
    protected $timestamp = 0.0;

    /**
     * A collection of tags attached to the TimePoint
     * @var array
     */
    protected $tags = [];

    /**
     * The type of TimePoint. Must be a value from TYPE_START or TYPE_END constants.
     * @var int
     */
    protected $type = 0;

    /**
     * The type of target. Must be a value from TARGET_CLIENT or TARGET_SERVER constants.
     * @var int
     */
    protected $target = 0;

    /**
     * The unique reference to name the TimePoint
     * @var string
     */
    protected $ref;

    /**
     * QtiTimePoint constructor.
     * @param string|array $tags
     * @param float $timestamp
     * @param int $type
     * @param int $target
     */
    public function __construct($tags = null, $timestamp = null, $type = null, $target = null)
    {
        if (isset($tags)) {
            $this->setTags($tags);
        }

        if (isset($timestamp)) {
            $this->setTimestamp($timestamp);
        }

        if (isset($type)) {
            $this->setType($type);
        }

        if (isset($target)) {
            $this->setTarget($target);
        }
    }

    /**
     * Exports the internal state to an array
     * @return array
     */
    public function toArray()
    {
        return [
            'ts' => $this->getTimestamp(),
            'type' => $this->getType(),
            'target' => $this->getTarget(),
            'tags' => $this->getTags(),
        ];
    }

    /**
     * Imports the internal state from an array
     * @param array $data
     */
    public function fromArray($data)
    {
        if ($data) {
            if (isset($data['tags'])) {
                $this->setTags($data['tags']);
            }

            if (isset($data['ts'])) {
                $this->setTimestamp($data['ts']);
            }

            if (isset($data['type'])) {
                $this->setType($data['type']);
            }

            if (isset($data['target'])) {
                $this->setTarget($data['target']);
            }
        }
    }

    /**
     * Specify data which should be serialized to JSON
     * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
     * @return mixed data which can be serialized by <b>json_encode</b>,
     * which is a value of any type other than a resource.
     * @since 5.4.0
     */
    public function jsonSerialize()
    {
        return $this->toArray();
    }

    /**
     * String representation of object
     * @link http://php.net/manual/en/serializable.serialize.php
     * @return string the string representation of the object or null
     * @since 5.1.0
     */
    public function serialize()
    {
        return serialize($this->toArray());
    }

    /**
     * Constructs the object
     * @link http://php.net/manual/en/serializable.unserialize.php
     * @param string $serialized <p>
     * The string representation of the object.
     * </p>
     * @return void
     * @since 5.1.0
     */
    public function unserialize($serialized)
    {
        $this->fromArray(unserialize($serialized));
    }

    /**
     * Sets the timestamp of the TimePoint
     * @param float $timestamp
     * @return TimePoint
     */
    public function setTimestamp($timestamp)
    {
        $this->timestamp = floatval($timestamp);
        return $this;
    }

    /**
     * Gets the timestamp of the TimePoint
     * @return float
     */
    public function getTimestamp()
    {
        return $this->timestamp;
    }

    /**
     * Gets the normalized value of the timestamp. This value is the result of:
     * `normalized_timestamp = timestamp_with_microseconds * precision`
     * @return int
     */
    public function getNormalizedTimestamp()
    {
        return round($this->getTimestamp() * self::PRECISION);
    }

    /**
     * Sets the type of TimePoint
     * @param int $type Must be a value from TYPE_START or TYPE_END constants.
     * @return TimePoint
     */
    public function setType($type)
    {
        $this->type = intval($type);
        return $this;
    }

    /**
     * Gets the type of TimePoint
     * @return int
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Sets the target type of the TimePoint
     * @param int $target Must be a value from TARGET_CLIENT or TARGET_SERVER constants.
     * @return TimePoint
     */
    public function setTarget($target)
    {
        $this->target = intval($target);
        return $this;
    }

    /**
     * Gets the target type of the TimePoint
     * @return int
     */
    public function getTarget()
    {
        return $this->target;
    }

    /**
     * Adds another tag to the TimePoint
     * @param string $tag
     * @return TimePoint
     */
    public function addTag($tag)
    {
        $this->tags[] = (string)$tag;
        $this->ref = null;
        return $this;
    }

    /**
     * Removes a tag from the TimePoint
     * @param string $tag
     * @return TimePoint
     */
    public function removeTag($tag)
    {
        $index = array_search($tag, $this->tags);

        if ($index !== false) {
            array_splice($this->tags, $index, 1);
            $this->ref = null;
        }

        return $this;
    }

    /**
     * Gets a tag from the TimePoint. By default, it will return the first tag.
     * @param int $index
     * @return string
     */
    public function getTag($index = 0)
    {
        $index = min(max(0, $index), count($this->tags));
        return $this->tags[$index];
    }

    /**
     * Sets the tags of the TimePoint
     * @param string|array $tags
     * @return TimePoint
     */
    public function setTags($tags)
    {
        $this->tags = [];
        $this->ref = null;

        if (is_array($tags)) {
            foreach ($tags as $tag) {
                $this->addTag($tag);
            }
        } else {
            $this->addTag($tags);
        }

        return $this;
    }

    /**
     * Gets all tags from the TimePoint
     * @return array
     */
    public function getTags()
    {
        return $this->tags;
    }

    /**
     * Gets a unique reference to name the TimePoint
     * @return string
     */
    public function getRef()
    {
        if (is_null($this->ref)) {
            $tags = $this->tags;
            sort($tags);
            $this->ref = md5(implode('-', $tags));
        }
        return $this->ref;
    }

    /**
     * Checks if a TimePoint matches the criteria
     * @param array $tags
     * @param int $target
     * @param int $type
     * @return bool
     */
    public function match(array $tags = null, $target = self::TARGET_ALL, $type = self::TYPE_ALL)
    {
        $match = ($this->getType() & $type) && ($this->getTarget() & $target);
        
        if ($match && isset($tags)) {
            $match = (count(array_intersect($tags, $this->getTags())) == count($tags));
        }
        
        return $match;
    }

    /**
     * Compares the TimePoint with another instance.
     * The comparison is made in this order:
     * - reference
     * - target
     * - timestamp
     * - type
     *
     * CAUTION!: The result order is not based on chronological order.
     * Its goal is to gather TimePoint by reference and target, then sort by type and timestamp.
     *
     * @param TimePoint $point
     * @return int
     */
    public function compare(TimePoint $point)
    {
        $diff = strcmp($this->getRef(), $point->getRef());
        if ($diff == 0) {
            $diff = $this->getTarget() - $point->getTarget();
            if ($diff == 0) {
                $diff = $this->getNormalizedTimestamp() - $point->getNormalizedTimestamp();
                if ($diff == 0) {
                    $diff = $this->getType() - $point->getType();
                }
            }
        }
        return $diff;
    }

    /**
     * Sorts a range of TimePoint
     * @param array $range
     * @return array
     */
    public static function sort(array &$range)
    {
        usort($range, function (TimePoint $a, TimePoint $b) {
            return $a->compare($b);
        });
        return $range;
    }
}