class GameSocket
{
    constructor( scene, server, port, protocol_id, token, user_id, game_id, casino_id, external_currency, symbols )
    {
        this.scene = scene;

        this.server = server;
        this.port = port;
        this.protocol_id = protocol_id;
        this.token = token;
        this.user_id = user_id;
        this.game_id = game_id;
        this.casino_id = casino_id;
        this.external_currency = external_currency;
        this.symbols = symbols;

        this.websocket = null;

        this.lines = [];
        this.reels = [];
        this.paytable = [];

//
        this.pos = 0;

        this.spin_info = {};
//
        this.game_options = [];
        this.bets_per_line = [];

        this.units = 0;
        this.bet_multiplier = 5;
        this.min_bet = 0;
        this.max_bet = 0;//max line bet!
        this.balance = 0;
        this.currency = "";

        this.restricted_credits = 0;
        this.nonrestricted_credits = 0;

        this.real_cash = 0;
        this.prm_cashable = 0;
        this.prm_playable = 0;
        this.prm_residual = 0;

        this.bet = 0;
        this.number_of_lines = 0;

//jackpots (3 for now)

        this.jackpots = [];
        this.jackpot_wins = [];

//spin info

        this.total_win = 0;
        this.cashback_win = 0;

        this.total_win_after_legal_limit = 0;

//extra info

//
        this.cid = 0xA3B5F;
        this.seq = 0;
        this.tid = this.terminal_get_id();

        this.challenge = [ 0, 0 ];
//
        this.bpl_index = 0;
        
        this.enable_jackpot_received_send_message = true;

        this.game_protocols = [];
    }
    
    connect()
    {
        let socket_url = 'wss://' + this.server + ':' + this.port + '/ws';
        
        this.websocket = new WebSocket( socket_url );

        this.websocket.binaryType = "arraybuffer";

        this.websocket.onopen = function( ev )
        {
            this.scene.onSocketOpen();
        }.bind(this);

        this.websocket.onclose = function( ev )
        {
            this.scene.onError('CONNECTION CLOSED (WEBSOCKET)');
        }.bind(this);

        this.websocket.onmessage = function( ev )
        {
            let msg = ev.data;

            if( typeof msg != 'string' )
            {
                this.parse_response( new Int32Array( msg ) );
            }
        }.bind(this);

        this.websocket.onerror = function( ev )
        {
            this.scene.onError('CONNECTION FAILED (WEBSOCKET)');
        }.bind(this);
    }

    disconnect()
    {
        this.websocket.close();
    }

    send( msg )
    {
        if(this.websocket.readyState == WebSocket.OPEN)
        {
			this.websocket.send( msg );
	    }
    }

    send_command( code )
    {
        let msg = new ArrayBuffer( 16 );
        let v = new Int32Array( msg );

        v[0] = this.cid; // cid
        v[1] = ++this.seq;
        v[2] = code; // code
        v[3] = 0; // length

        this.send( msg );
    }

    send_cmd_wallet_token_login2()
    {
        let token_padded_length = this.token.length + 1;

        while( token_padded_length % 4 )
        {
            ++token_padded_length;
        }

        let user_id_padded_length = this.user_id.length + 1;

        while( user_id_padded_length % 4 )
        {
            ++user_id_padded_length;
        }

        let game_id_padded_length = this.game_id.length + 1;

        while( game_id_padded_length % 4 )
        {
            ++game_id_padded_length;
        }

        let payload_length = 16 + 4 + token_padded_length + 4 + user_id_padded_length + 4 + game_id_padded_length + 4;
        let msg = new ArrayBuffer( 16 + payload_length );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x2758; // cmd_wallet_token_login2
        v[ 3 ] = payload_length;   // length

        let w = new Uint8Array( msg );

        for( let i = 0; i < 8; ++i )
        {
            w[ 16 + i ] = this.tid.charCodeAt( i );
        }

        let auth = this.terminal_auth( this.challenge, this.tid, "" );

        v[ 6 ] = auth[ 0 ];
        v[ 7 ] = auth[ 1 ];


        let offset = 8;
        
        v[ offset++ ] = this.token.length;

        for( var i = 0; i < this.token.length; ++i )
        {
            w[ offset * 4 + i ] = this.token.charCodeAt( i );
        }

        offset += Math.floor( token_padded_length / 4 );

        v[ offset++ ] = this.user_id.length;

        for( let i = 0; i < this.user_id.length; ++i )
        {
            w[ offset * 4 + i ] = this.user_id.charCodeAt( i );
        }

        offset += Math.floor( user_id_padded_length / 4 );

        v[ offset++ ] = this.game_id.length;

        for( let i = 0; i < this.game_id.length; ++i )
        {
            w[ offset * 4 + i ] = this.game_id.charCodeAt( i );
        }

        offset += Math.floor( game_id_padded_length / 4 );

        v[ offset ] = this.casino_id;

        this.send( msg );
    }

    send_cmd_wallet_token_login3()
    {
        let token_padded_length = this.token.length + 1;

        while( token_padded_length % 4 )
        {
            ++token_padded_length;
        }

        let user_id_padded_length = this.user_id.length + 1;

        while( user_id_padded_length % 4 )
        {
            ++user_id_padded_length;
        }

        let game_id_padded_length = this.game_id.length + 1;

        while( game_id_padded_length % 4 )
        {
            ++game_id_padded_length;
        }

        let external_currency_padded_length = this.external_currency.length + 1;

        while( external_currency_padded_length % 4 )
        {
            ++external_currency_padded_length;
        }


        let payload_length = 16 + 4 + token_padded_length + 4 + user_id_padded_length + 4 + game_id_padded_length + 4 + external_currency_padded_length;
        let msg = new ArrayBuffer( 16 + payload_length );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x275C; // cmd_wallet_token_login3
        v[ 3 ] = payload_length;   // length

        let w = new Uint8Array( msg );

        for( let i = 0; i < 8; ++i )
        {
            w[ 16 + i ] = this.tid.charCodeAt( i );
        }

        let auth = this.terminal_auth( this.challenge, this.tid, "" );

        v[ 6 ] = auth[ 0 ];
        v[ 7 ] = auth[ 1 ];


        let offset = 8;
        
        v[ offset ] = this.token.length;

        ++offset;

        for( var i = 0; i < this.token.length; ++i )
        {
            w[ offset * 4 + i ] = this.token.charCodeAt( i );
        }

        offset += Math.floor( token_padded_length / 4 );

        v[ offset ] = this.user_id.length;

        ++offset;

        for( let i = 0; i < this.user_id.length; ++i )
        {
            w[ offset * 4 + i ] = this.user_id.charCodeAt( i );
        }

        offset += Math.floor( user_id_padded_length / 4 );

        v[ offset ] = this.game_id.length;

        ++offset;

        for( let i = 0; i < this.game_id.length; ++i )
        {
            w[ offset * 4 + i ] = this.game_id.charCodeAt( i );
        }

        offset += Math.floor( game_id_padded_length / 4 );

        v[ offset ] = this.casino_id;

        ++offset;

        for( let i = 0; i < this.external_currency.length; ++i )
        {
            w[ offset * 4 + i ] = this.external_currency.charCodeAt( i );
        }

        this.send( msg );
    }

    send_cmd_init()
    {
        let protocol = this.protocol_id +  '\0';

        while( protocol.length % 4 )
        {
            protocol += '\0';
        }
        
        let n = protocol.length;

        let msg = new ArrayBuffer( 16 + n );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x0001; // cmd_init
        v[ 3 ] = n;      // length

        let w = new Uint8Array( msg );

        for( let i = 0; i < n; ++i )
        {
            w[ 16 + i ] = protocol.charCodeAt( i );
        }

        this.send( msg );
    }

    send_cmd_set_units( units )
    {
        let n = 4;
        let msg = new ArrayBuffer( 16+n );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x7000; // cmd_set_units
        v[ 3 ] = n;      // length

        v[ 4 ] = units;

        this.send( msg );
    }

    send_cmd_get_paytable()
    {
        let msg = new ArrayBuffer( 16 );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x4002; // cmd_query_paytable
        v[ 3 ] = 0;      // length

        this.send( msg );
    }

    send_cmd_get_reels()
    {
        let msg = new ArrayBuffer( 16 );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x4003; // cmd_get_reels
        v[ 3 ] = 0;      // length

        this.send( msg );
    }
   
    send_cmd_get_lines()
    {
        let msg = new ArrayBuffer( 16 );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x4004; // cmd_get_lines
        v[ 3 ] = 0;      // length

        this.send( msg );
    }

    send_cmd_v2_mystery_jackpot()
    {
        let msg = new ArrayBuffer( 16 );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x3701; // cmd query jackpot 2
        v[ 3 ] = 0;      // length

        this.send( msg );
    }

    send_cmd_get_options()
    {
        let opt = Array();

        opt[0] = 'denoms';
        opt[1] = 'max_total_bet';
        opt[2] = 'bets_per_line';
        
        let length = 0;
        let total_length = 0;

        for( let i = 0; i < opt.length; ++i )
        {
            length = opt[i].length;
        
            if( (length % 4) == 0 )
            {
                length += 4;
            }
            else
            {
                while( (length % 4) != 0 )
                {
                    ++length;
                }
            }

            total_length += length;
        }

        let msg = new ArrayBuffer( 20 + opt.length * 4  +  total_length );

        let v = new Int32Array( msg );

        v[0] = this.cid; // cid
        v[1] = ++this.seq;
        v[2] = 8; // cmd get protocol options
        v[3] = 4 + opt.length * 4  +  total_length; // length
        v[4] = opt.length;

        let j = 5;

        let w = new Uint8Array( msg );

        for( let i = 0; i < opt.length; ++i )
        {
            v[j] = opt[i].length;

            let len = opt[i].length;
        
            if( (len % 4) == 0 )
            {
                len += 4;
            }
            else
            {
                while( (len % 4) != 0 )
                {
                    ++len;
                }
            }

            for( let k = 0; k < opt[i].length; ++k )
            {
                w[ ( j+1 ) * 4 + k ] = opt[i].charCodeAt( k );
            }

            for( let k = opt[i].length; k < len; ++k )
            {
                w[ ( j+1 ) * 4 + k ] = 0;
            }

            j += 1 + len / 4; 
        }

        this.send( msg );
    }

    send_cmd_spin()
    {
        let msg = new ArrayBuffer( 24 );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x4001; // cmd_spin
        v[ 3 ] = 8;      // length

        v[ 4 ] = this.units * this.bets_per_line[ this.bpl_index ];
        v[ 5 ] = 5;//number of lines

        this.send( msg );

        let message = "Sent: " + parseFloat((this.units * this.bets_per_line[ this.bpl_index ] * this.bet_multiplier)/100).toFixed(2) + " 5";
        this.send_cmd_event( message );
    }

    send_cmd_event( message )
    {
        let date = new Date();
        let timestamp = Math.floor(date.getTime()/1000);
        
        let gamemodule = this.protocol_id +  '\0';

        while( gamemodule.length % 4 )
        {
            gamemodule += '\0';
        }
        
        while( message.length % 4 )
        {
            message += '\0';
        }

        let n = 8 + gamemodule.length + message.length;

        let msg = new ArrayBuffer( 28 + n );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 13; // cmd_event
        v[ 3 ] = 12 + n;      // length
        v[ 4 ] = timestamp;     

        v[5] = gamemodule.length;

        let w = new Uint8Array( msg );

        for( let i = 0; i < gamemodule.length; ++i )
        {
            w[ 24 + i ] = gamemodule.charCodeAt( i );
        }

        let p = 6 + gamemodule.length / 4;

        v[p++] = 0;
        v[p++] = message.length;

        for( let i = 0; i < message.length; ++i )
        {
            w[ p * 4 + i ] = message.charCodeAt( i );
        }

        v[p + message.length / 4] = 0;

        this.send( msg );
    }

    send_cmd_get_games()
    {
        let msg = new ArrayBuffer( 16 );

        let v = new Int32Array( msg );

        v[ 0 ] = this.cid;    // cid
        v[ 1 ] = ++this.seq;
        v[ 2 ] = 0x272A;
        v[ 3 ] = 0;      // length

        this.send( msg );
    }

    response_balance( msg )
    {
        let oldBalance = this.balance;
        this.balance = msg[ 4 ];
        this.currency = this.currency_code( msg[ 5 ] );

        this.restricted_credits = msg[ 6 ];
        this.nonrestricted_credits = msg[ 7 ];

        if(oldBalance != this.balance && this.units > 0)
        {
            this.scene.onBalanceChange(Math.floor(this.balance / this.units), Math.floor(oldBalance / this.units));
        }            
    }

    response_units_set( msg )
    {
        this.currency = this.currency_code( msg[ 4 ] );
        this.units = msg[ 5 ];
        this.min_bet = msg[ 6 ];
        this.max_bet = msg[ 7 ];        

        this.send_cmd_v2_mystery_jackpot();
        this.send_cmd_get_paytable();
        this.send_cmd_get_lines();
        this.send_cmd_get_reels();

        let message = "Logged in; balance: " + parseFloat(this.balance/100).toFixed(2) + " + " + parseFloat((this.restricted_credits + this.nonrestricted_credits)/100).toFixed(2);
        this.send_cmd_event( message );

        message = "Units set: " + parseFloat(this.units/100).toFixed(2) + ", " + this.currency;
        this.send_cmd_event( message );

        this.scene.onBalance();
    }   

    response_challenge( msg )
    {
        this.challenge = [ msg[ 4 ], msg[ 5 ] ];
    	
        if(this.external_currency === "")
        {
    	this.send_cmd_wallet_token_login2();
    }
        else
        {
            this.send_cmd_wallet_token_login3();
        }
    }

    response_login_successful( /*msg*/ )
    {
        this.send_command(2);
        this.send_cmd_init();
    }
    
    response_init( /*msg*/ )
    {
        this.send_cmd_get_options()
    }

    response_warning( str, msg )
    {
        let code = new Long(msg[ 2 ], 0, true);
        console.log("response_error: " + str + ', Error Code ' + code.low);
    }

    response_error( str, msg )
    {
        let code = new Long(msg[ 2 ], 0, true);
        this.scene.onError(str +  ', Error Code ' + code.low);
    }

    response_login_failed( msg )
    {
        this.scene.onError('LOGIN FAILED');
    }

    response_paytable( msg )
    {
    	this.paytable.length = 0;
	    let pos = 4;

        for(let i = 0; i < 13; ++i)
        {
            let a = [];

            for(let j = 0; j < 6; ++j)
            {
                a[j] = msg[ pos + i * 6 + j];
            }
            
            this.paytable.push(a);				
        }
    }

    response_reels( msg )
    {
        this.reels.length = 0;
        let pos = 4;
    
        let reel_size = 0;
    
        for( let i = 0; i < 5; ++i )
        {
            reel_size = msg[pos++];
    
            let reel = [];
            for( let j = 0; j < reel_size; ++j )
            {
                reel[j] = msg[ pos + j ];
            }
            
            this.reels.push(reel);
            pos += reel_size;
        }

        //this is last initialization step, notify main scene that game session is started

        this.scene.onGameSessionReady();
    }

    response_lines( msg )
    {
        this.lines.length = 0;
        let pos = 4;
    
        for(let i = 0; i < 5; ++i)
        {
            let a = [];
    
            for(let j = 0; j < 5; ++j)
            {
                a[j] = msg[ pos + i * 5 + j];
            }
    
            this.lines.push(a);				
        }
    }

    response_mystery_jackpot( msg )
    {
        this.jackpots.length = 0;
        let pos = 4;
        let nm_jackpots = msg[ pos++ ];

        for(let i = 0; i < nm_jackpots; ++i)
        {
            this.jackpots[i] = msg[ pos++ ];
        }

        let message = "Current jackpots:\n";
        
        if(this.jackpots[0] != null)
        {
            message += "- grand: " + parseFloat(this.jackpots[0]/100).toFixed(2) + "\n";
        }

        if(this.jackpots[1] != null)
        {
            message += "- maxi: " + parseFloat(this.jackpots[1]/100).toFixed(2) + "\n";
        }

        if(this.jackpots[2] != null)
        {
            message += "- mini: " + parseFloat(this.jackpots[2]/100).toFixed(2) + "\n";
        }
        
        if(this.enable_jackpot_received_send_message)
        {
            this.send_cmd_event( message );
            this.enable_jackpot_received_send_message = false;
        }            
    }

    response_v2_mystery_jackpot( msg )
    {
        this.jackpots.length = 0;
        this.jackpot_wins.length = 0;

        let pos = 4;
        let nm_jackpots = msg[ pos++ ];//always 3 jackpots

        for(let i = 0; i < nm_jackpots; ++i)
        {
            this.jackpot_wins[i] = msg[ pos++ ];
        }

        nm_jackpots = msg[ pos++ ];

        for(let i = 0; i < nm_jackpots; ++i)
        {
            this.jackpots[i] = msg[ pos++ ];
        }
        
        if(this.enable_jackpot_received_send_message)
        {
            this.send_jackpot_v2_mystery_event();
        }            
    }

    response_v2_cashback( msg )
    {
        this.cashback_win = msg[ 4 ];
        this.balance = msg[ 5 ];

        let message = "Cashback win: " + parseFloat(this.cashback_win/100).toFixed(2) + "\n";
        
        this.send_cmd_event( message );
    }

    response_query_options( msg )
    {
        this.game_options.length = 0;

        let total_options = msg[4];
        let pos = 5;
        
        let i, j;

        for ( i = 0; i < total_options; ++i )
        {
            let key = "";
            let value = "";

            let len = msg[pos];

            if( (len % 4) == 0 )
            {
                len += 4;
            }
            else
            {
                while( (len % 4) != 0 )
                {
                    ++len;
                }
            }
            
            for( j = 0; j < len / 4; ++j  )
            {
                key += this.convert( msg[pos + 1 + j].toString(16) );
            }
            
            pos += 1 + len / 4;

            len = msg[pos];

            if( (len % 4) == 0 )
            {
                len += 4;
            }
            else
            {
                while( (len % 4) != 0 )
                {
                    ++len;
                }
            }
            
            for( j = 0; j < len / 4; ++j  )
            {
                value += this.convert( msg[pos + 1 + j].toString(16) );
            }
            
            pos += 1 + len / 4;
            this.game_options[key] = value;
        }

        this.bets_per_line = this.game_options['bets_per_line'].split(' ');

        let denoms = this.game_options['denoms'].split(' ');

        if( denoms.length == 0 || denoms[0] == "" )
        {
            this.scene.onError('INVALID GAME SETTINGS');
        }
        else
        {
            this.send_cmd_set_units(denoms[0] * 100);
        }
    }

    response_games( msg )
    {
        this.game_protocols = [];

        let total = msg[4];//total games
        let pos = 0;
        
        let payload = msg.slice(5);

        for(let i = 0; i < total; ++i)
        {
            let ci = 0;//current item
            
            let j;

            while(true)
            {
                if(ci == 4)
                {
                    ++pos;//category id

                    let nd = payload[pos++];//number of denoms
                    pos += nd;
                    break;
                }

                let item = "";//protocol id, dll, game name and image
                
                let len = payload[pos++];

                if( (len % 4) == 0 )
                {
                    len += 4;
                }
                else
                {
                    while( (len % 4) != 0 )
                    {
                        ++len;
                    }
                }
                
                for( j = 0; j < len / 4; ++j  )
                {
                    item += this.convert( payload[pos + j].toString(16) );
                }
        
                if(ci == 0)
                {
                    this.game_protocols.push(item);//first item is game protocol id
                }
                
                pos = pos + j;
                ++ci;
            }                
        }            
    }

    get_spin_info( msg )
    {
        let si = {};
        si.spin_result = {};
       
        let combination_id = new Long(msg[ this.pos ], msg[ this.pos + 1 ], true);

        si.combination_id = combination_id.toString();

        ++this.pos;
        ++this.pos

        si.spin_result.symbol_field = [];
    
        for(let i = 0; i < 5; ++i)
        {
            let a = [];
    
            for(let j = 0; j < 3; ++j)
            {
                a[j] = msg[ this.pos++ ];
            }
    
            si.spin_result.symbol_field.push(a);
        }

        si.spin_result.break_present = msg[ this.pos++ ];

        si.spin_result.nm_lines = msg[ this.pos++ ];

        si.spin_result.line_info = [];

        for(let i = 0; i < si.spin_result.nm_lines; ++i)
        {
            let a = [];

            a["id"] = i;
            a["symbol"] = msg[ this.pos++ ];
            a["len"] = msg[ this.pos++ ];
            a["win"] = msg[ this.pos++ ];

            if(a["win"] > 0)
            {
                si.spin_result.line_info.push(a);
            }                
        }

        si.spin_result.total_lines_win = msg[ this.pos++ ];

        let nm_scatters = msg[ this.pos++ ];

        si.spin_result.scatters_info = [];

        for(let i = 0; i < nm_scatters; ++i)
        {
            let a = [];

            a["symbol"] = msg[ this.pos++ ];
            a["len"] = msg[ this.pos++ ];
            a["win"] = msg[ this.pos++ ];

            a["scatters_field"] = [];
                
            if(a["win"] > 0)
            {
                for(let u = 0; u < si.spin_result.symbol_field.length; ++u)
                {
                    let sfield = [...si.spin_result.symbol_field[u]];
    
                    for(let v = 0; v < sfield.length; ++v)
                    {
                        if(sfield[v] == a["symbol"])
                        {
                            sfield[v] = 1;
                        }
                        else
                        {
                            sfield[v] = 0;
                        }
                    }
    
                    a["scatters_field"].push(sfield);
                }
    
                si.spin_result.scatters_info.push(a);
            }                
        }

        return si;
    }

    response_spin( msg )
    {
        this.pos = 4;
        this.bet = msg[ this.pos++ ];
        this.number_of_lines = msg[ this.pos++ ];

        this.spin_info = this.get_spin_info(msg);

        this.total_win = msg[ this.pos++ ];
        this.total_win_after_legal_limit = this.total_win;

        this.balance = msg[ this.pos++ ];

        // new in server version 1.2.517:
        this.real_cash = msg[ this.pos++ ];
        this.prm_cashable = msg[ this.pos++ ];
        this.prm_playable = msg[ this.pos++ ];
        this.prm_residual = msg[ this.pos++ ];

       	// new in server version 1.2.574:

        if(this.pos < msg.length)
        {
            this.total_win_after_legal_limit = msg[ this.pos++ ];
        }

        this.spin_info.legal_limit_applyed = (this.total_win != this.total_win_after_legal_limit);

        this.send_spin_received_event();
        this.scene.onSpinReceived();
    }

    line_to_string(line_id, symbol_field)
    {
        let str = ""
        
        for(let i = 0; i < this.lines[line_id].length; ++i)
        {
            let v = this.lines[line_id][i] + 1;
            let sym = symbol_field[i][v];

            if(str == "")
            {
                str += this.symbols[sym];
            }
            else
            {
                str += " " + this.symbols[sym];
            }                
        }

        return str;
    }
    
    send_spin_received_event()
    {
        let message = "Units: " + parseFloat(this.units/100).toFixed(2) + "\n";
        message += "Total bet: " +  parseFloat((this.units * this.bets_per_line[ this.bpl_index ] * this.bet_multiplier)/100).toFixed(2) + "\n";
        message += "Bet: " + parseFloat(this.bet/100).toFixed(2) + "\n";
        message += "Number of lines: " + this.number_of_lines + "\n";
        message += "Combination: " + this.spin_info.combination_id + "\n\n";

        for(let i = 0; i < 3; ++i)
        {
            let sm = "";
            for(let j = 0; j < 5; ++j)
            {
                sm += " " + this.symbols[this.spin_info.spin_result.symbol_field[j][i]] + "(" + this.spin_info.spin_result.symbol_field[j][i] + ")";
            }

            message += sm + "\n";
        }
        
        if(this.spin_info.spin_result.total_lines_win > 0)
        {
            message += "\n";
        }

        for(let i = 0; i < this.spin_info.spin_result.line_info.length; ++i)
        {
            if(this.spin_info.spin_result.line_info[i]["win"] > 0)
            {
                message += "Line " + (this.spin_info.spin_result.line_info[i]["id"] + 1) + ": " + "(" + this.spin_info.spin_result.line_info[i]["len"] + " " + this.symbols[this.spin_info.spin_result.line_info[i]["symbol"]] + ")" + " ";
                message += this.line_to_string(this.spin_info.spin_result.line_info[i]["id"], this.spin_info.spin_result.symbol_field);
                message += "; Win: " + parseFloat((this.units * this.bets_per_line[ this.bpl_index ] * this.spin_info.spin_result.line_info[i]["win"])/100).toFixed(2);
                message += "\n";
            }
        }

        if(this.spin_info.spin_result.total_lines_win > 0)
        {
            message += "Total lines win: " + parseFloat(this.spin_info.spin_result.total_lines_win/100).toFixed(2) + "\n";
        }

        if(this.spin_info.spin_result.scatters_info.length > 0)
        {
            message += "\n";
            
            if(this.spin_info.spin_result.break_present)
            {
                message += "SPECIAL SYMBOL (Break)\n";
            }

            for(let i = 0; i < this.spin_info.spin_result.scatters_info.length; ++i)
            {
                message += this.spin_info.spin_result.scatters_info[i]["len"];
                message += " " + this.symbols[this.spin_info.spin_result.scatters_info[i]["symbol"]]; 
                message += "; Win: " + parseFloat(this.spin_info.spin_result.scatters_info[i]["win"]/100).toFixed(2) + "\n";
            }
        }            


        if(this.total_win > 0)
        {
            message += "Total win: " + parseFloat(this.total_win/100).toFixed(2) + "\n";
        }

        message += "Balance: " + parseFloat(this.balance/100).toFixed(2) + "\n";

        this.send_cmd_event( message );
    }

    send_jackpot_v2_mystery_event()
    {
        let message = "";

        if(this.getJackpotWin() > 0)
        {
            message = "Jackpots win: \n";

            if(this.jackpot_wins[0] != null && this.jackpot_wins[0] > 0)
            {
                message += "- grand: " + parseFloat(this.jackpot_wins[0]/100).toFixed(2) + "\n";
            }

            if(this.jackpot_wins[1] != null && this.jackpot_wins[1] > 0)
            {
                message += "- maxi: " + parseFloat(this.jackpot_wins[1]/100).toFixed(2) + "\n";
            }

            if(this.jackpot_wins[2] != null && this.jackpot_wins[2] > 0)
            {
                message += "- mini: " + parseFloat(this.jackpot_wins[2]/100).toFixed(2) + "\n";
            }
        }

        message += "Current jackpots:\n";
        
        if(this.jackpots[0] != null)
        {
            message += "- grand: " + parseFloat(this.jackpots[0]/100).toFixed(2) + "\n";
        }

        if(this.jackpots[1] != null)
        {
            message += "- maxi: " + parseFloat(this.jackpots[1]/100).toFixed(2) + "\n";
        }

        if(this.jackpots[2] != null)
        {
            message += "- mini: " + parseFloat(this.jackpots[2]/100).toFixed(2) + "\n";
        }

        this.send_cmd_event( message );

        this.enable_jackpot_received_send_message = false;
    }

    parse_response( msg )
    {
        switch( msg[ 2 ] )
        {
        case 2:
    
            this.response_balance( msg );
            break;

        case 3:

            this.response_units_set( msg );
            break;
        
        case 0x2700:
    
            this.response_challenge( msg );
            break;
    
        case 0x2701:
    
            this.response_login_successful( msg );
            break;
        case 1:
    
            this.response_init();
            break;
    
        case 8:
        
            this.response_query_options( msg );
            break;

        case 0x4001:
            
            this.response_spin( msg );
            break;
    
        case 0x4002:
            
            this.response_paytable( msg );
            break;    
            
        case 0x4003:
    
            this.response_reels( msg );
            break;

        case 0x4004:            

            this.response_lines( msg );
            break;

        case 0x272A:
            this.response_games( msg );
            break;

        case 0x7001:
    
            this.response_mystery_jackpot( msg );
            break;
    
        case 0x7020:
    
            this.response_v2_mystery_jackpot( msg );
            break;
    
        case 0x7021:
    
            this.response_v2_cashback( msg );
            break;
    
        case -1:
    
            this.response_warning( "UNKNOWN PACKET", msg );
            break;
    
        case -2:
    
            this.response_error( "BAD PACKET", msg );
            break;
    
        case -3:
    
            this.response_error( "NOT INITIALIZED", msg );
            break;
    
        case -4:
    
            this.response_error( "HANDLER NAME UNKNOWN", msg );
            break;
    
        case -5:
    
            this.response_error( "NOT ENOUGH CREDIT", msg );
            break;
    
        case -6:
    
            this.response_error( "INVALID UNITS", msg );
            break;
    
        case -7:
    
            this.response_error( "UNITS NOT SET", msg );
            break;
    
        case -8:
    
            this.response_warning( "Units already set", msg );
            break;
    
        case -9:
    
            this.response_error( "INVALID BET", msg );
            break;
    
        case -10:
    
            this.response_error( "SERVER ERROR", msg );
            break;
    
        case -11:
    
            this.response_error( "NOT LOGGED IN", msg );
            break;
    
        case -12:
    
            this.response_error( "COMMAND NOT ALLOWED", msg );
            break;
    
        case -13:
    
            this.response_error( "GAME DISABLED", msg );
            break;
    
        case -14:
    
            this.response_error( "GAME NOT CONFIGURED", msg );
            break;
    
        case -15:
    
            this.response_warning( "Game not in tournament", msg );
            break;
    
        case -16:
    
            this.response_error( "INVALID LINES", msg );
            break;
    
        case -17:
    
            this.response_error( "INVALID POSITION", msg );
            break;
    
        case -0x2701:
    
            this.response_error( "LOGIN FAILED", msg );
            break;
        }
    }
    
//helper functions

    currency_code( v )
    {
        let b = new ArrayBuffer( 4 );

        let w1 = new Int32Array( b );
        w1[ 0 ] = v;

        let w2 = new Uint8Array( b );

        let s = "";

        for( let i = 0; i < 4; ++i )
        {
            if(i < 3 || (i == 3 && w2[ i ] != 0))
            {
                s += String.fromCharCode( w2[ i ] );
            }                
        }

        return s;
    }

    dec2hex( i )
    {
        return (i+0x100000000).toString(16).substr(-8).toUpperCase();
    }

    convert( input )
    {
        let str = '';
        
        for (let i = input.length -2; i >=0 ; i -= 2 )
        {
            str += String.fromCharCode(parseInt(input.substr(i, 2), 16));
        }
        
        return str;
    }

    message_to_string( buffer )
    {
        if( typeof buffer == 'string' )
        {
            return '"' + buffer + '"';
        }

        let n = buffer.byteLength;
        
        if( n > 4096 )
        {
            return n + ' [ ... ]';
        }

        let v = new Uint32Array( buffer );
        let s = buffer.byteLength + ' [';

        let i = 0;

        for( i = 0; i < v.length; ++i )
        {
            s += this.dec2hex( v[i] ) + ', ';
        }

        return s + ']';
    }

    random_char_from( str )
    {
        let n = str.length;
        let i = Math.floor( Math.random() * n );
        return str.charAt( i );
    }
    
    random_tid()
    {
        let s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        let tid = "";
        
        for( let i = 0; i < 8; ++i )
        {
            tid += this.random_char_from( s );
        }
        
        return tid;
    }
    
    terminal_get_id()
    {
        let tid = localStorage? localStorage.terminal_id: null;
    
        if( !tid )
        {
            tid = this.random_tid();
            if( localStorage) localStorage.terminal_id = tid;
        }
    
        return tid;
    }

    terminal_auth( challenge, tid, password )
    {
    
        let hash = [ 0, 0 ];

        this.hash64_combine_string( hash, tid );
        this.hash64_combine_string( hash, password );

        this.hash64_combine( hash, challenge );

        return hash;
    }

    hash64_encrypt( v, k )
    {
        let y = v[ 0 ];
        let z = v[ 1 ];

        let sum = 0;
        let delta = 0x9E3779B9;

        for( let n = 32; n > 0; --n )
        {
            y += (((z << 4) ^ (z >>> 5)) + z) ^ (sum + k[sum & 3]);
            y |= 0;

            sum += delta;
            sum |= 0;

            z += (((y << 4) ^ (y >>> 5)) + y) ^ (sum + k[(sum >> 11) & 3]);
            z |= 0;
        }

        return [ y, z ];
    }

    hash64_combine( hash, v )
    {
        let k = [ v[0], v[1], 0, 0 ];
        let h2 = this.hash64_encrypt( hash, k );

        hash[0] = h2[0] ^ hash[0] ^ v[0];
        hash[1] = h2[1] ^ hash[1] ^ v[1];
    }

    hash64_combine_string( hash, s )
    {
        let n = s.length;

        let m = n + 1;
        while( m % 8 ) ++m;

        let v = new ArrayBuffer( m );
        
        let v8 = new Uint8Array( v );

        for( let i = 0; i < n; ++i )
        {
            v8[ i ] = s.charCodeAt( i );
        }
        
        v8[ n ] = 0xFF;

        let v32 = new Uint32Array( v );

        m = m / 8;

        for( let i = 0; i < m; ++i )
        {
            let v = [ v32[ i*2 ], v32[ i*2+1 ] ];
            this.hash64_combine( hash, v );
        }

        this.hash64_combine( hash, [ n, 0 ] );
    }

    shuffle( a )
    {
        for (let i = a.length - 1; i > 0; i--)
        {
            const j = Math.floor(Math.random() * (i + 1));
            [a[i], a[j]] = [a[j], a[i]];
        }
        
        return a;
    }

//scene events

    onBetChange()
    {
        if( ++this.bpl_index >= this.bets_per_line.length || this.bets_per_line[ this.bpl_index ] > this.max_bet )
        {
            this.bpl_index = 0;
        }
    }

    onBetChangeEx()
    {
        if( (this.bpl_index + 1) >= this.bets_per_line.length)
        {
            return;
        }

        if( this.bets_per_line[ this.bpl_index + 1 ] > this.max_bet )
        {
            return;
        }

        ++this.bpl_index;
    }

    getBetPerLine()
    {
        return this.bets_per_line[ this.bpl_index ];
    }

    getBet()
    {
        return this.bet_multiplier * this.bets_per_line[ this.bpl_index ];
    }

    getMaxBet()
    {
        return this.bet_multiplier * this.bets_per_line[ this.bets_per_line.length - 1 ];
    }

    setMaxBet()
    {
        for( let i = 0; i < this.bets_per_line.length; ++i)
        {
            if(this.bets_per_line[i] > this.max_bet)
            {
                return;
            }
            else
            {
                this.bpl_index = i;
            }
        }            
    }

    getBalance()
    {
        return (this.units > 0)? Math.floor(this.balance / this.units):0;
    }

    getBalanceBeforeWin()
    {
        let w = this.getTotalWin();

        for(let i = 0; i < this.jackpot_wins.length; ++i)
        {
            w += this.jackpot_wins[i];
        }

        w += this.cashback_win;
        
        return (this.units > 0)? Math.floor((this.balance - w)/ this.units):0;
    }

    getTotalWin()
    {
        
        if(this.total_win_after_legal_limit < this.total_win)
        {
            return this.total_win_after_legal_limit;
        }
        else
        {
            return this.total_win;
        }
    }

    getJackpots()
    {
        return this.jackpots;
    }

    getUnits()
    {
        return this.units;
    }

    getCurrency()
    {
        return this.currency;
    }

    getLines()
    {
        return this.lines;
    }

    getReels()
    {
        return this.reels;
    }

    getPaytable()
    {
        return this.paytable;
    }

    getCashbackWin()
    {
        return this.cashback_win;
    }

    getJackpotWin()
    {
        let win = 0;

        for(let i = 0; i < this.jackpot_wins.length; ++i)
        {
            win += this.jackpot_wins[i];
        }

        return win;
    }

    getGameProtocols()
    {
        return this.game_protocols;
    }

    clearJackpotWin()
    {
        for(let i = 0; i < this.jackpot_wins.length; ++i)
        {
            this.jackpot_wins[i] = 0;
        }
    }

    getJackpotWins()
    {
        return this.jackpot_wins;
    }    
    
    getGame()
    {
        return this.spin_info;
    }

    queryBalance()
    {
        this.send_command(2);
    }

    queryJackpots()
    {
        this.send_cmd_v2_mystery_jackpot();
    }

    spin()
    {
        this.jackpot_wins.length = 0;
        this.cashback_win = 0;
        this.enable_jackpot_received_send_message = true;

        this.send_cmd_spin();
    }
}    
