<?php

/**
 * RouterOS API client implementation.

 * 
 * RouterOS is the flag product of the company MikroTik and is a powerful router software. One of its many abilities is to allow control over it via an API. This package provides a client for that API, in turn allowing you to use PHP to control RouterOS hosts.
 * 
 * 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   1.0.0b4
 * @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          $timeout_s  If a response is not immediatly
     *     available, wait this many seconds. If NULL, wait indefinetly.
     * @param int          $timeout_us 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,
        $timeout_s = 0,
        $timeout_us = null,
        Registry $reg = null
    ) {
        if (null === $reg) {
            if ($com->getTransmitter()->isPersistent()) {
                $old = $com->getTransmitter()
                    ->lock(T\Stream::DIRECTION_RECEIVE);
                try {
                    $this->_receive($com, $asStream, $timeout_s, $timeout_us);
                } catch (E $e) {
                    $com->getTransmitter()->lock($old, true);
                    throw $e;
                }
                $com->getTransmitter()->lock($old, true);
            } else {
                $this->_receive($com, $asStream, $timeout_s, $timeout_us);
            }
        } else {
            while (null === ($response = $reg->getNextResponse())) {
                $newResponse = new self($com, true, $timeout_s, $timeout_us);
                $tagInfo = $reg::parseTag($newResponse->getTag());
                $newResponse->setTag($tagInfo[1]);
                if (!$reg->add($newResponse, $tagInfo[0])) {
                    $response = $newResponse;
                    break;
                }
            }
            
            $this->_type = $response->_type;
            $this->arguments = $response->arguments;
            $this->unrecognizedWords = $response->unrecognizedWords;
            $this->setTag($response->getTag());
            
            if (!$asStream) {
                foreach ($this->arguments as $name => $value) {
                    $this->setArgument(
                        $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          $timeout_s  If a response is not immediatly
     *     available, wait this many seconds. If NULL, wait indefinetly.
     * @param int          $timeout_us Microseconds to add to the waiting time.
     * 
     * @return void
     */
    private function _receive(
        Communicator $com,
        $asStream = false,
        $timeout_s = 0,
        $timeout_us = null
    ) {
        if (!$com->getTransmitter()->isDataAwaiting(
            $timeout_s,
            $timeout_us
        )) {
            throw new SocketException(
                'No data within the time limit',
                SocketException::CODE_NO_DATA
            );
        }
        $this->setType($com->getNextWord());
        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->setArgument($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->setArgument($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 self|Response 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 a list of unrecognized words.
     * 
     * @return array The list of unrecognized words.
     */
    public function getUnrecognizedWords()
    {
        return $this->unrecognizedWords;
    }
}