<?php
namespace Ratchet;
use Ratchet\Application\ProtocolInterface;

/**
 * A wrapper for the PHP socket_ functions
 * @author Chris Boden <shout at chrisboden dot ca>
 * @link http://ca2.php.net/manual/en/book.sockets.php
 * @todo Possibly move this into Ratchet\Resource - another concrete could use streams
 */
class Socket implements SocketInterface {
    /**
     * @type resource
     */
    protected $_resource;

    public static $_defaults = array(
        'domain'   => AF_INET
      , 'type'     => SOCK_STREAM
      , 'protocol' => SOL_TCP
    );

    /**
     * @param int Specifies the protocol family to be used by the socket.
     * @param int The type of communication to be used by the socket
     * @param int Sets the specific protocol within the specified domain to be used when communicating on the returned socket
     * @throws Ratchet\Exception
     */
    public function __construct($domain = null, $type = null, $protocol = null) {
        list($domain, $type, $protocol) = static::getConfig($domain, $type, $protocol);

        $this->_resource = @socket_create($domain, $type, $protocol);

        if (!is_resource($this->_resource)) {
            throw new Exception($this);
        }
    }

    public function __destruct() {
        @socket_close($this->_resource);
    }

    public function __toString() {
        $id = (string)$this->getResource();
        return (string)substr($id, strrpos($id, '#') + 1);
    }

    /**
     * @return resource (Socket)
     */
    public function getResource() {
        return $this->_resource;
    }

    public function __clone() {
        $this->_resource = @socket_accept($this->_resource);

        if (false === $this->_resource) {
            throw new Exception($this);
        }
    }

    public function deliver($message) {
        $len = strlen($message);

        do {
            $sent    = $this->write($message, 4);
            $len    -= $sent;
            $message = substr($message, $sent);
        } while ($len > 0);
    }

    public function bind($address, $port = 0) {
        if (false === @socket_bind($this->getResource(), $address, $port)) {
            throw new Exception($this);
        }

        return $this;
    }

    public function close() {
        @socket_close($this->getResource());
        unset($this->_resource);
    }

    public function connect($address, $port = 0) {
        if (false === @socket_connect($this->getResource(), $address, $port)) {
            throw new Exception($this);
        }

        return $this;
    }

    public function getRemoteAddress() {
        $address = $port = '';
        if (false === @socket_getpeername($this->getResource(), $address, $port)) {
            throw new Exception($this);
        }

        return $address;
    }

    public function get_option($level, $optname) {
        if (false === ($res = @socket_get_option($this->getResource(), $level, $optname))) {
            throw new Exception($this);
        }

        return $res;
    }

    public function listen($backlog = 0) {
        if (false === @socket_listen($this->getResource(), $backlog)) {
            throw new Exception($this);
        }

        return $this;
    }

    /**
     * @see http://ca3.php.net/manual/en/function.socket-recv.php
     * @param string Variable to write data to
     * @param int Number of bytes to read
     * @param int
     * @return int Number of bytes received
     * @throws Exception
     */
    public function recv(&$buf, $len, $flags) {
        if (false === ($bytes = @socket_recv($this->_resource, $buf, $len, $flags))) {
            throw new Exception($this);
        }

        return $bytes;
    }

    /**
     * Since PHP is retarded and their golden hammer, the array, doesn't implement any interfaces I have to hackishly overload socket_select
     * @see http://ca3.php.net/manual/en/function.socket-select.php
     * @param Iterator|array|NULL The sockets listed in the read array will be watched to see if characters become available for reading (more precisely, to see if a read will not block - in particular, a socket resource is also ready on end-of-file, in which case a socket_read() will return a zero length string).
     * @param Iterator|array|NULL The sockets listed in the write array will be watched to see if a write will not block.
     * @param Iterator|array|NULL The sockets listed in the except array will be watched for exceptions.
     * @param int The tv_sec and tv_usec together form the timeout parameter. The timeout is an upper bound on the amount of time elapsed before socket_select() return. tv_sec may be zero , causing socket_select() to return immediately. This is useful for polling. If tv_sec is NULL (no timeout), socket_select() can block indefinitely.
     * @param int
     * @throws \InvalidArgumentException
     * @throws Exception
     */
    public function select(&$read, &$write, &$except, $tv_sec, $tv_usec = 0) {
        $read   = static::mungForSelect($read);
        $write  = static::mungForSelect($write);
        $except = static::mungForSelect($except);

        $num = socket_select($read, $write, $except, $tv_sec, $tv_usec);

        if (false === $num) {
            throw new Exception($this);
        }

        return $num;
    }

    public function set_block() {
        if (false === @socket_set_block($this->getResource())) {
            throw new Exception($this);
        }

        return $this;
    }

    public function set_nonblock() {
        if (false === @socket_set_nonblock($this->getResource())) {
            throw new Exception($this);
        }

        return $this;
    }

    public function set_option($level, $optname, $optval) {
        if (false === @socket_set_option($this->getResource(), $level, $optname, $optval)) {
            throw new Exception($this);
        }

        return $this;
    }

    public function shutdown($how = 2) {
        if (false === @socket_shutdown($this->getResource(), $how)) {
            throw new Exception($this);
        }

        return $this;
    }

    public function write($buffer, $length = 0) {
        if (false === ($res = @socket_write($this->getResource(), $buffer, $length))) {
            throw new Exception($this);
        }

        return $res;
    }

    /**
     * @param Ratchet\Application\ProtocolInterface
     * @return Socket
     * @throws Exception
     */
    public static function createFromConfig(ProtocolInterface $protocol) {
        $config = $protocol::getDefaultConfig();
        $class  = get_called_class();

        $socket = new $class($config['domain'] ?: null, $config['type'] ?: null, $config['protocol'] ?: null);

        if (is_array($config['options'])) {
            foreach ($config['options'] as $level => $pair) {
                foreach ($pair as $optname => $optval) {
                    $socket->set_option($level, $optname, $optval);
                }
            }
        }

        return $socket;
    }

    /**
     * @internal
     * @param int Specifies the protocol family to be used by the socket.
     * @param int The type of communication to be used by the socket
     * @param int Sets the specific protocol within the specified domain to be used when communicating on the returned socket
     * @return array
     */
    protected static function getConfig($domain = null, $type = null, $protocol = null) {
        foreach (static::$_defaults as $key => $val) {
            if (null === $$key) {
                $$key = $val;
            }
        }

        return array($domain, $type, $protocol);
    }

    /**
     * @internal
     * @param Iterator|array|NULL
     * @return array|NULL
     * @throws \InvalidArgumentException
     */
    protected static function mungForSelect($collection) {
        if (null === $collection || is_array($collection)) {
            return $collection;
        }

        if (!($collection instanceof \Traversable)) {
            throw new \InvalidArgumentException('Object pass is not traversable');
        }

        $return = array();
        foreach ($collection as $key => $socket) {
            $return[$key] = ($socket instanceof \Ratchet\Socket ? $socket->getResource() : $socket);
        }

        return $return;
    }
}