<?php

/**
 * ~~summary~~
 * 
 * ~~description~~
 * 
 * PHP version 5
 * 
 * @category  Net
 * @package   PEAR2_Net_RouterOS
 * @author    Vasil Rangelov <boen.robot@gmail.com>
 * @copyright 2011 Vasil Rangelov
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @version   GIT: $Id$
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
 */
/**
 * The namespace declaration.
 */
namespace PEAR2\Net\RouterOS;

/**
 * Refers to transmitter direction constants.
 */
use PEAR2\Net\Transmitter as T;

/**
 * Locks are released upon any exception from anywhere.
 */
use Exception as E;

/**
 * Represents a RouterOS response.
 * 
 * @category Net
 * @package  PEAR2_Net_RouterOS
 * @author   Vasil Rangelov <boen.robot@gmail.com>
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
 * @link     http://pear2.php.net/PEAR2_Net_RouterOS
 */
class Response extends Message
{
    
    /**
     * The last response for a request.
     */
    const TYPE_FINAL = '!done';
    
    /**
     * A response with data.
     */
    const TYPE_DATA = '!re';
    
    /**
     * A response signifying error.
     */
    const TYPE_ERROR = '!trap';
    
    /**
     * A response signifying a fatal error, due to which the connection would be
     * terminated.
     */
    const TYPE_FATAL = '!fatal';

    /**
     * @var array An array of unrecognized words in network order.
     */
    protected $unrecognizedWords = array();

    /**
     * @var string The response type.
     */
    private $_type;

    /**
     * Extracts a new response from a communicator.
     * 
     * @param Communicator $com       The communicator from which to extract
     *     the new response.
     * @param bool         $asStream  Whether to populate the argument values
     *     with streams instead of strings.
     * @param int          $sTimeout  If a response is not immediatly
     *     available, wait this many seconds. If NULL, wait indefinetly.
     * @param int          $usTimeout Microseconds to add to the waiting time.
     * @param Registry     $reg       An optional registry to sync the
     *     response with.
     * 
     * @see getType()
     * @see getArgument()
     */
    public function __construct(
        Communicator $com,
        $asStream = false,
        $sTimeout = 0,
        $usTimeout = null,
        Registry $reg = null
    ) {
        if (null === $reg) {
            if ($com->getTransmitter()->isPersistent()) {
                $old = $com->getTransmitter()
                    ->lock(T\Stream::DIRECTION_RECEIVE);
                try {
                    $this->_receive($com, $asStream, $sTimeout, $usTimeout);
                } catch (E $e) {
                    $com->getTransmitter()->lock($old, true);
                    throw $e;
                }
                $com->getTransmitter()->lock($old, true);
            } else {
                $this->_receive($com, $asStream, $sTimeout, $usTimeout);
            }
        } else {
            while (null === ($response = $reg->getNextResponse())) {
                $newResponse = new self($com, true, $sTimeout, $usTimeout);
                $tagInfo = $reg::parseTag($newResponse->getTag());
                $newResponse->setTag($tagInfo[1]);
                if (!$reg->add($newResponse, $tagInfo[0])) {
                    $response = $newResponse;
                    break;
                }
            }
            
            $this->_type = $response->_type;
            $this->attributes = $response->attributes;
            $this->unrecognizedWords = $response->unrecognizedWords;
            $this->setTag($response->getTag());
            
            if (!$asStream) {
                foreach ($this->attributes as $name => $value) {
                    $this->setAttribute(
                        $name,
                        stream_get_contents($value)
                    );
                }
                foreach ($response->unrecognizedWords as $i => $value) {
                    $this->unrecognizedWords[$i] = stream_get_contents($value);
                }
            }
        }
    }
    
    /**
     * Extracts a new response from a communicator.
     * 
     * This is the function that performs the actual receiving, while the
     * constructor is also involved in locks and registry sync.
     * 
     * @param Communicator $com       The communicator from which to extract
     *     the new response.
     * @param bool         $asStream  Whether to populate the argument values
     *     with streams instead of strings.
     * @param int          $sTimeout  If a response is not immediatly
     *     available, wait this many seconds. If NULL, wait indefinetly.
     *     Note that if an empty sentence is received, the timeout will be
     *     reset for another sentence receiving.
     * @param int          $usTimeout Microseconds to add to the waiting time.
     * 
     * @return void
     */
    private function _receive(
        Communicator $com,
        $asStream = false,
        $sTimeout = 0,
        $usTimeout = null
    ) {
        do {
            if (!$com->getTransmitter()->isDataAwaiting(
                $sTimeout,
                $usTimeout
            )) {
                throw new SocketException(
                    'No data within the time limit',
                    SocketException::CODE_NO_DATA
                );
            }
            $type = $com->getNextWord();
        } while ('' === $type);
        $this->setType($type);
        if ($asStream) {
            for ($word = $com->getNextWordAsStream(), fseek($word, 0, SEEK_END);
                ftell($word) !== 0;
                $word = $com->getNextWordAsStream(), fseek(
                    $word,
                    0,
                    SEEK_END
                )) {
                rewind($word);
                $ind = fread($word, 1);
                if ('=' === $ind || '.' === $ind) {
                    $prefix = stream_get_line($word, null, '=');
                }
                if ('=' === $ind) {
                    $value = fopen('php://temp', 'r+b');
                    $bytesCopied = ftell($word);
                    while (!feof($word)) {
                        $bytesCopied += stream_copy_to_stream(
                            $word,
                            $value,
                            0xFFFFF,
                            $bytesCopied
                        );
                    }
                    rewind($value);
                    $this->setAttribute($prefix, $value);
                    continue;
                }
                if ('.' === $ind && 'tag' === $prefix) {
                    $this->setTag(stream_get_contents($word, -1, -1));
                    continue;
                }
                rewind($word);
                $this->unrecognizedWords[] = $word;
            }
        } else {
            for ($word = $com->getNextWord(); '' !== $word;
                $word = $com->getNextWord()) {
                if (preg_match('/^=([^=]+)=(.*)$/sS', $word, $matches)) {
                    $this->setAttribute($matches[1], $matches[2]);
                } elseif (preg_match('/^\.tag=(.*)$/sS', $word, $matches)) {
                    $this->setTag($matches[1]);
                } else {
                    $this->unrecognizedWords[] = $word;
                }
            }
        }
    }

    /**
     * Sets the response type.
     * 
     * Sets the response type. Valid values are the TYPE_* constants.
     * 
     * @param string $type The new response type.
     * 
     * @return $this The response object.
     * @see getType()
     */
    protected function setType($type)
    {
        switch ($type) {
        case self::TYPE_FINAL:
        case self::TYPE_DATA:
        case self::TYPE_ERROR:
        case self::TYPE_FATAL:
            $this->_type = $type;
            return $this;
        default:
            throw new UnexpectedValueException(
                'Unrecognized response type.',
                UnexpectedValueException::CODE_RESPONSE_TYPE_UNKNOWN,
                null,
                $type
            );
        }
    }

    /**
     * Gets the response type.
     * 
     * @return string The response type.
     * @see setType()
     */
    public function getType()
    {
        return $this->_type;
    }

    /**
     * Gets the value of an argument.
     * 
     * @param string $name The name of the argument.
     * 
     * @return string|resource|null The value of the specified argument.
     *     Returns NULL if such an argument is not set.
     * @deprecated 1.0.0b5 Use {@link static::getProperty()} instead.
     *     This method will be removed upon final release, and is currently
     *     left standing merely because it can't be easily search&replaced in
     *     existing code, due to the fact the name "getArgument()" is shared
     *     with {@link Request::getArgument()}, which is still valid.
     * @codeCoverageIgnore
     */
    public function getArgument($name)
    {
        trigger_error(
            'Response::getArgument() is deprecated in favor of ' .
            'Response::getProperty() (but note that Request::getArgument() ' .
            'is still valid)',
            E_USER_DEPRECATED
        );
        return $this->getAttribute($name);
    }

    /**
     * Gets the value of a property.
     * 
     * @param string $name The name of the property.
     * 
     * @return string|resource|null The value of the specified property.
     *     Returns NULL if such a property is not set.
     */
    public function getProperty($name)
    {
        return parent::getAttribute($name);
    }

    /**
     * Gets a list of unrecognized words.
     * 
     * @return array The list of unrecognized words.
     */
    public function getUnrecognizedWords()
    {
        return $this->unrecognizedWords;
    }

    /**
     * Counts the number of arguments or words.
     * 
     * @param int $mode The counter mode.
     *     Either COUNT_NORMAL or COUNT_RECURSIVE.
     *     When in normal mode, counts the number of arguments.
     *     When in recursive mode, counts the number of API words.
     * 
     * @return int The number of arguments/words.
     */
    public function count($mode = COUNT_NORMAL)
    {
        $result = parent::count($mode);
        if ($mode !== COUNT_NORMAL) {
            $result += count($this->unrecognizedWords);
        }
        return $result;
    }
}
