<?php
/**
 * JavaScriptクライアントからのデータを処理するコールバック関数
 * 下で定義するWebSocketServerクラスを使う。
 * @param WebSocketUser $user 現在のユーザ
 * @param string $msg クライアントから受け取ったデータ
 * @param WebSocketServer $server サーバオブジェクト
 */
function process($user, $msg, $server){
    $return = array();
    $msgArray = explode(',',$msg);
    // 最初に受け取るデータはコマンド (CONNECT, UPDATE etc).
    $msg = $msgArray[0];
        
    switch ($msg) {
        case 'DISCONNECT':
            $user->data['disconnect'] = true;
            $return[$user->id] = $user->data;
            break;
        case 'CONNECT':
            $user->data['pos'] = '0'.','. $msgArray[2];
            $user->data['colr'] = $msgArray[1];
            $return[$user->id] = $user->data;
            // 新しいユーザから各ユーザ（自分を含む）に送信
            $json = json_encode($return);   
            $return2 = array();
            foreach ($server->getUsers() as $user2){  
                $server->send($user2->socket, $json);
                if ($user2 != $user) $return2[$user2->id] = $user2->data;
            }
            // 他のユーザを、新しいユーザに送る
            $json = json_encode($return2);  
            foreach ($server->getUsers() as $user2){  
                if ($user2 != $user) $server->send($user->socket, $json);
            }
            return;
        case 'CHATTEXT':
            $user->data['chattext'] = $msgArray[1];
            $return[$user->id] = $user->data;
            unset($user->data['chattext']); 
            break;
        case 'UPDATE':
            $user->data['pos'] = $msgArray[1].','. $msgArray[2];
            $return[$user->id] = $user->data;
            break;
    }    
    // 現在の全ユーザにデータを送る
    $json = json_encode($return);   
    
    foreach ($server->getUsers() as $user){
        $server->send($user->socket, $json );
    }
}


/**
 * WebSocketServer クラス
 * 参考： http://code.google.com/p/phpwebsocket/
 * @author DerFichtl AT gmail.com / @DerFichtl on Twitter
 */
class WebSocketServer {
    protected $address = null;
    protected $port = null;
    protected $users = array();
    protected $master = null;
    protected $sockets = array();
    protected $callback = null;
    protected $maxConnection = 99;
    
    public function __construct($address, $port, $callback) {
        $this->address = $address;
        $this->port = $port;
        $this->callback = $callback;
        $this->connectMaster($address, $port);        
    }
    
    public function getUsers() {
        return $this->users;
    }
    
    protected function connectMaster() {
        ob_implicit_flush(true);
        $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or
            die("socket_create() failed");
        $this->sockets[] = $this->master;
        socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or
            die("socket_option() failed");
        socket_bind($this->master, $this->address, $this->port) or
            die("socket_bind() failed");
        socket_listen($this->master, 5) or
            die("socket_listen() failed");
        return $this->master;
    }

    public function run() {
        while (true) {
            $changed = $this->sockets;
            $write=NULL;
            $except=NULL;
            socket_select($changed, $write, $except, 0);
            foreach ($changed as $socket) {
                if ($socket == $this->master) {
                    $client = socket_accept($this->master);
                    if ($client < 0) { console("socket_accept() failed"); continue; }
                    else { $this->connect($client); }
                } else {
                    $bytes = socket_recv($socket, $buffer, 10485760, 0);
                    if ($bytes == 0) {
                        $user = $this->getUserBySocket($socket);                    
                        call_user_func($this->callback, $user, 'DISCONNECT', $this);
                        $this->disconnect($socket);
                    } else {
                        $user = $this->getUserBySocket($socket);
                        if (!$user->handshake) {
                            $user->doHandshake($buffer);
                        } else {
                            $user->lastAction = time();
                            // コールバック関数を呼ぶ
                            if ($this->callback) {
                                call_user_func($this->callback, $user, 
                                               $this->unwrap($buffer), $this);
                            }
                        }
                    }
                }
            }
        }
    }
    
    public function connect($socket) {
        $this->users[] = new WebSocketUser($socket);
        $this->sockets[] = $socket;
    }
    
    public function disconnect($socket) {
        if ($this->users) {
            $found = null;
            $n = count($this->users);
            for ($i = 0; $i < $n; $i++) {
                if ($this->users[$i]->socket == $socket) { $found=$i; break; }
            }
            if (!is_null($found)) { array_splice($this->users, $found, 1); }
            $index = array_search($socket, $this->sockets);
            socket_close($socket);
            $this->say($socket." DISCONNECTED!");
            if ($index >= 0) { array_splice($this->sockets, $index, 1); }
        }
    }
    
    public function getUserBySocket($socket) {
        foreach ($this->users as $user) {
            if ($user->socket == $socket) {
                return $user;
            }
        }
        return null;
    }

    public function send($client, $msg){ 
        $this->say(">".$msg);
        $msg = $this->wrap($msg);

        socket_write($client, $msg, strlen($msg));
    }
    
    private function say($msg="") { echo $msg."\n"; }

    private function wrap($data="") {
        $enc = chr(0x81);
        $len = strlen($data);
        if ($len <= 125) {
            $enc .= chr($len + 128);
        } else if ($len <= 65535) {
            $enc .= chr(126 | 0x80);
            $enc .= chr($len >> 8);
            $enc .= chr($len % 0xff);
        } else {
            return '';
        }
        $mask = array();
        for($i = 0; $i < 4; $i++) {
            $enc .= $mask[$i] = chr(rand(0, 255));
        }
        for ($i = 0; $i < $len; $i++) {
            $enc .= $data[$i] ^ $mask[$i % 4];
        }
        return $enc;
    }

    private function unwrap($data="") {
        $opcode = ord($data[0]) & 0x0f;
        if ($opcode != 1) return '';

        $is_masked = ord($data[1]) & 0x80;
        if (!$is_masked) return '';

        $len = ord($data[1]) & 0x7f;
        if ($len === 127) return '';

        $mask = '';
        $offset = 2;
        if ($len === 126) {
            $len = ord($data[2]) * 256 + ord($data[3]);
            $offset += 2;
        }
        $mask = substr($data, $offset, 4);
        $offset += 4;

        $dec = '';
        for ($i = 0; $i < $len; $i++) {
            $dec .= $data[$i + $offset] ^ $mask[$i % 4];
        }
        return $dec;
    }
}


/**
 * WebSocketUser クラス
 */
class WebSocketUser {
    public $id = null;
    public $socket = null;
    public $handshake = false;
    public $ip = null;
    public $lastAction = null;
    public $data = array();

    public function __construct($socket) {
        $this->id = uniqid();
        $this->socket = $socket;
        socket_getpeername($socket, $ip);
        $this->ip = $ip;
    }
    
    public function doHandshake($buffer) {
        list($resource, $headers, $securityCode) = 
            $this->handleRequestHeader($buffer);
        print_r($headers);
        $securityResponse = '';

        if (isset($headers['Sec-WebSocket-Key'])) {
            // draft-ietf-hybi-thewebsocketprotocol-10
            $securityResponse =
                "Sec-WebSocket-Accept: " .
                $this->getHandshakeSecurityKey10($headers['Sec-WebSocket-Key']) .
                "\r\n\r\n";
        }

        $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
                   "Upgrade: WebSocket\r\n" .
                   "Connection: Upgrade\r\n" .
                    $securityResponse;

        print_r($upgrade);

        socket_write($this->socket, $upgrade, strlen($upgrade));

        $this->handshake = true;
        return true;    
    }

    private function handleSecurityKey($key) {
        preg_match_all('/[0-9]/', $key, $number);
        preg_match_all('/ /', $key, $space);
        if ($number && $space) {
            return implode('', $number[0]) / count($space[0]);
        }
        return '';
    } 

    private function getHandshakeSecurityKey($key1, $key2, $code) {
        return md5(
            pack('N', $this->handleSecurityKey($key1)).
            pack('N', $this->handleSecurityKey($key2)).
            $code,
            true
        );
    }

    private function getHandshakeSecurityKey10($key) {
        return base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
    }
    
    private function handleRequestHeader($request) {
        $resource = $code = null;
        preg_match('/GET (.*?) HTTP/', $request, $match) && $resource = $match[1];
        preg_match("/\r\n(.*?)\$/", $request, $match) && $code = $match[1];
        $headers = array();
        foreach(explode("\r\n", $request) as $line) {
            if (strpos($line, ': ') !== false) {
                list($key, $value) = explode(': ', $line);
                $headers[trim($key)] = trim($value);
            }
        }
        return array($resource, $headers, $code);
    }
}

// ソケットサーバにIPアドレスとポートを指定して初期化する。
// 'process'を受信データを処理するコールバック関数と指定する。
$webSocket = new WebSocketServer("127.0.0.1", 8999, 'process');
$webSocket->run();

?>
