<?php
namespace PHPSocketIO;
use PHPSocketIO\Event\Emitter;
use PHPSocketIO\Parser\Parser;
class Socket extends Emitter
{
    public $nsp = null;
    public $server = null;
    public $adapter = null;
    public $id = null;
    public $path = '/';
    public $request = null;
    public $client = null;
    public $conn = null;
    public $rooms = array();
    public $_rooms = array();
    public $flags = array();
    public $acks = array();
    public $connected = true;
    public $disconnected = false;

    public static $events = array(
        'error'=>'error',
        'connect' => 'connect',
        'disconnect' => 'disconnect',
        'newListener' => 'newListener',
        'removeListener' => 'removeListener'
    );

    public static $flagsMap = array(
        'json' => 'json',
        'volatile' => 'volatile',
        'broadcast' => 'broadcast'
    );

    public function __construct($nsp, $client)
    {
        $this->nsp = $nsp;
        $this->server = $nsp->server;
        $this->adapter = $this->nsp->adapter;
        $this->id = ($nsp->name !== '/') ? $nsp->name .'#' .$client->id : $client->id;
        $this->request = $client->request;
        $this->client = $client;
        $this->conn = $client->conn;
        $this->handshake = $this->buildHandshake();
        Debug::debug('IO Socket __construct');
    }

    public function __destruct()
    {
        Debug::debug('IO Socket __destruct');
    }

    public function buildHandshake()
    {
        //todo check this->request->_query
        $info = !empty($this->request->url) ?  parse_url($this->request->url) : array();
        $query = array();
        if(isset($info['query']))
        {
            parse_str($info['query'], $query);
        }
        return array(
            'headers' => isset($this->request->headers) ? $this->request->headers : array(),
            'time'=> date('D M d Y H:i:s') . ' GMT',
            'address'=> $this->conn->remoteAddress,
            'xdomain'=> isset($this->request->headers['origin']),
            'secure' => !empty($this->request->connection->encrypted),
            'issued' => time(),
            'url' => isset($this->request->url) ? $this->request->url : '',
            'query' => $query,
       );
    }

    public function __get($name)
    {
        if($name === 'broadcast')
        {
            $this->flags['broadcast'] = true;
            return $this;
        }
        return null;
    }

    public function emit($ev = null)
    {
        $args = func_get_args();
        if (isset(self::$events[$ev]))
        {
            call_user_func_array(array(__CLASS__, 'parent::emit'), $args);
        }
        else
        {
            $packet = array();
            // todo check
            //$packet['type'] = hasBin($args) ? Parser::BINARY_EVENT : Parser::EVENT;
            $packet['type'] = Parser::EVENT;
            $packet['data'] = $args;
            $flags = $this->flags;
            // access last argument to see if it's an ACK callback
            if (is_callable(end($args)))
            {
                if ($this->_rooms || isset($flags['broadcast']))
                {
                    throw new \Exception('Callbacks are not supported when broadcasting');
                }
                echo('emitting packet with ack id ' . $this->nsp->ids);
                $this->acks[$this->nsp->ids] = array_pop($args);
                $packet['id'] = $this->nsp->ids++;
            }

            if ($this->_rooms || !empty($flags['broadcast']))
            {
                $this->adapter->broadcast($packet, array(
                    'except' => array($this->id => $this->id),
                    'rooms'=> $this->_rooms,
                    'flags' => $flags
                ));
            }
            else
            {
                // dispatch packet
                $this->packet($packet);
            }

            // reset flags
            $this->_rooms = array();
            $this->flags = array();
        }
        return $this;
    }


    /**
     * Targets a room when broadcasting.
     *
     * @param {String} name
     * @return {Socket} self
     * @api public
     */

    public function to($name)
    {
        if(!isset($this->_rooms[$name]))
        {
            $this->_rooms[$name] = $name;
        }
        return $this;
    }

    public function in($name)
    {
        return $this->to($name);
    }

    /**
     * Sends a `message` event.
     *
     * @return {Socket} self
     * @api public
     */

    public function send()
    {
        $args = func_get_args();
        array_unshift($args, 'message');
        call_user_func_array(array($this, 'emit'), $args);
        return $this;
    }

    public function write()
    {
        $args = func_get_args();
        array_unshift($args, 'message');
        call_user_func_array(array($this, 'emit'), $args);
        return $this;
    }

    /**
     * Writes a packet.
     *
     * @param {Object} packet object
     * @param {Object} options
     * @api private
     */

    public function packet($packet, $preEncoded = false)
    {
        if (!$this->nsp || !$this->client) return;
        $packet['nsp'] = $this->nsp->name;
        //$volatile = !empty(self::$flagsMap['volatile']);
        $volatile = false;
        $this->client->packet($packet, $preEncoded, $volatile);
    }

    /**
     * Joins a room.
     *
     * @param {String} room
     * @param {Function} optional, callback
     * @return {Socket} self
     * @api private
     */

     public function join($room)
     {
        if(isset($this->rooms[$room])) return $this;
        $this->adapter->add($this->id, $room);
        $this->rooms[$room] = $room;
        return $this;
    }

    /**
     * Leaves a room.
     *
     * @param {String} room
     * @param {Function} optional, callback
     * @return {Socket} self
     * @api private
     */

    public function leave($room)
    {
        $this->adapter->del($this->id, $room);
        unset($this->rooms[$room]);
        return $this;
    }

    /**
     * Leave all rooms.
     *
     * @api private
     */

    public function leaveAll()
    {
        $this->adapter->delAll($this->id);
        $this->rooms = array();
    }

    /**
     * Called by `Namespace` upon succesful
     * middleware execution (ie: authorization).
     *
     * @api private
     */

    public function onconnect()
    {
        $this->nsp->connected[$this->id] = $this;
        $this->join($this->id);
        $this->packet(array(
            'type' => Parser::CONNECT)
         );
    }

    /**
     * Called with each packet. Called by `Client`.
     *
     * @param {Object} packet
     * @api private
     */

    public function onpacket($packet)
    {
        switch ($packet['type'])
        {
            case Parser::EVENT:
                $this->onevent($packet);
                break;

            case Parser::BINARY_EVENT:
                $this->onevent($packet);
                break;

            case Parser::ACK:
                $this->onack($packet);
                break;

            case Parser::BINARY_ACK:
                $this->onack($packet);
                break;

            case Parser::DISCONNECT:
                $this->ondisconnect();
                break;

            case Parser::ERROR:
                $this->emit('error', $packet['data']);
        }
    }

    /**
     * Called upon event packet.
     *
     * @param {Object} packet object
     * @api private
     */

    public function onevent($packet)
    {
        $args = isset($packet['data']) ? $packet['data'] : array();
        if (!empty($packet['id']) || (isset($packet['id']) && $packet['id'] === 0))
        {
            $args[] = $this->ack($packet['id']);
        }
        call_user_func_array(array(__CLASS__, 'parent::emit'), $args);
    }

    /**
     * Produces an ack callback to emit with an event.
     *
     * @param {Number} packet id
     * @api private
     */

    public function ack($id)
    {
        $self = $this;
        $sent = false;
        return function()use(&$sent, $id, $self){
            // prevent double callbacks
            if ($sent) return;
            $args = func_get_args();
            $type = $this->hasBin($args) ? Parser::BINARY_ACK : Parser::ACK;
            $self->packet(array(
                'id' => $id,
                'type' => $type,
                'data' => $args
            ));
        };
    }

    /**
     * Called upon ack packet.
     *
     * @api private
     */

    public function onack($packet)
    {
        $ack = $this->acks[$packet['id']];
        if (is_callable($ack))
        {
            call_user_func($ack, $packet['data']);
            unset($this->acks[$packet['id']]);
        } else {
            echo ('bad ack '. packet.id);
        }
    }

    /**
     * Called upon client disconnect packet.
     *
     * @api private
     */

    public function ondisconnect()
    {
        //echo('got disconnect packet');
        $this->onclose('client namespace disconnect');
    }

    /**
     * Handles a client error.
     *
     * @api private
     */

    public function onerror($err)
    {
        if ($this->listeners('error'))
        {
            $this->emit('error', $err);
        }
        else
        {
            //echo('Missing error handler on `socket`.');
        }
    }

    /**
     * Called upon closing. Called by `Client`.
     *
     * @param {String} reason
     * @param {Error} optional error object
     * @api private
     */

     public function onclose($reason)
     {
        if (!$this->connected) return $this;
        $this->emit('disconnect', $reason);
        $this->leaveAll();
        $this->nsp->remove($this);
        $this->client->remove($this);
        $this->connected = false;
        $this->disconnected = true;
        unset($this->nsp->connected[$this->id]);
        // ....
        $this->nsp = null;
        $this->server = null;
        $this->adapter = null;
        $this->request = null;
        $this->client = null;
        $this->conn = null;
        $this->removeAllListeners();
    }

    /**
     * Produces an `error` packet.
     *
     * @param {Object} error object
     * @api private
     */

    public function error($err)
    {
        $this->packet(array(
            'type' => Parser::ERROR, 'data' => $err )
         );
    }

    /**
     * Disconnects this client.
     *
     * @param {Boolean} if `true`, closes the underlying connection
     * @return {Socket} self
     * @api public
     */

    public function disconnect( $close = false )
    {
        if (!$this->connected) return $this;
        if ($close)
        {
            $this->client->disconnect();
        } else {
            $this->packet(array(
                'type'=> Parser::DISCONNECT
            ));
            $this->onclose('server namespace disconnect');
        }
        return $this;
    }

    /**
     * Sets the compress flag.
     *
     * @param {Boolean} if `true`, compresses the sending data
     * @return {Socket} self
     * @api public
     */

    public function compress($compress)
    {
        $this->flags['compress'] = $compress;
        return $this;
    }

    protected function hasBin($args) {
        $hasBin = false;

        array_walk_recursive($args, function($item, $key) use ($hasBin) {
            if (!ctype_print($item)) {
                $hasBin = true;
            }
        });

        return $hasBin;
    }
}
