
function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
	var _t = instance.web._t;

    // the JobQueue schedules a sequence of 'jobs'. each job is
    // a function returning a deferred. the queue waits for each job to finish 
    // before launching the next. Each job can also be scheduled with a delay. 
    // the  is used to prevent parallel requests to the proxy.

    module.JobQueue = function(){
        var queue = [];
        var running = false;
        var scheduled_end_time = 0;
        var end_of_queue = (new $.Deferred()).resolve();
        var stoprepeat = false;

        var run = function(){
            if(end_of_queue.state() === 'resolved'){
                end_of_queue =  new $.Deferred();
            }
            if(queue.length > 0){
                running = true;
                var job = queue[0];
                if(!job.opts.repeat || stoprepeat){
                    queue.shift();
                    stoprepeat = false;
                }

                // the time scheduled for this job
                scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);

                // we run the job and put in def when it finishes
                var def = job.fun() || (new $.Deferred()).resolve();
                
                // we don't care if a job fails ... 
                def.always(function(){
                    // we run the next job after the scheduled_end_time, even if it finishes before
                    setTimeout(function(){
                        run();
                    }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); 
                });
            }else{
                running = false;
                scheduled_end_time = 0;
                end_of_queue.resolve();
            }
        };
        
        // adds a job to the schedule.
        // opts : {
        //    duration    : the job is guaranteed to finish no quicker than this (milisec)
        //    repeat      : if true, the job will be endlessly repeated
        //    important   : if true, the scheduled job cannot be canceled by a queue.clear()
        // }
        this.schedule  = function(fun, opts){
            queue.push({fun:fun, opts:opts || {}});
            if(!running){
                run();
            }
        }

        // remove all jobs from the schedule (except the ones marked as important)
        this.clear = function(){
            queue = _.filter(queue,function(job){job.opts.important === true}); 
        };

        // end the repetition of the current job
        this.stoprepeat = function(){
            stoprepeat = true;
        };
        
        // returns a deferred that resolves when all scheduled 
        // jobs have been run.
        // ( jobs added after the call to this method are considered as well )
        this.finished = function(){
            return end_of_queue;
        }

    };


    // this object interfaces with the local proxy to communicate to the various hardware devices
    // connected to the Point of Sale. As the communication only goes from the POS to the proxy,
    // methods are used both to signal an event, and to fetch information. 

    module.ProxyDevice  = instance.web.Class.extend(openerp.PropertiesMixin,{
        init: function(parent,options){
            openerp.PropertiesMixin.init.call(this,parent);
            var self = this;
            options = options || {};
            url = options.url || 'http://localhost:8069';

            this.pos = parent;
            
            this.weighting = false;
            this.debug_weight = 0;
            this.use_debug_weight = false;

            this.paying = false;
            this.default_payment_status = {
                status: 'waiting',
                message: '',
                payment_method: undefined,
                receipt_client: undefined,
                receipt_shop:   undefined,
            };    
            this.custom_payment_status = this.default_payment_status;

            this.receipt_queue = [];

            this.notifications = {};
            this.bypass_proxy = false;

            this.connection = null; 
            this.host       = '';
            this.keptalive  = false;

            this.set('status',{});

            this.set_connection_status('disconnected');

            this.on('change:status',this,function(eh,status){
                status = status.newValue;
                if(status.status === 'connected'){
                    self.print_receipt();
                }
            });

            window.hw_proxy = this;
        },
        set_connection_status: function(status,drivers){
            oldstatus = this.get('status');
            newstatus = {};
            newstatus.status = status;
            newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
            newstatus.drivers = drivers ? drivers : newstatus.drivers;
            this.set('status',newstatus);
        },
        disconnect: function(){
            if(this.get('status').status !== 'disconnected'){
                this.connection.destroy();
                this.set_connection_status('disconnected');
            }
        },

        // connects to the specified url
        connect: function(url){
            var self = this;
            this.connection = new instance.web.Session(undefined,url, { use_cors: true});
            this.host   = url;
            this.set_connection_status('connecting',{});

            return this.message('handshake').then(function(response){
                    if(response){
                        self.set_connection_status('connected');
                        localStorage['hw_proxy_url'] = url;
                        self.keepalive();
                    }else{
                        self.set_connection_status('disconnected');
                        console.error('Connection refused by the Proxy');
                    }
                },function(){
                    self.set_connection_status('disconnected');
                    console.error('Could not connect to the Proxy');
                });
        },

        // find a proxy and connects to it. for options see find_proxy
        //   - force_ip : only try to connect to the specified ip. 
        //   - port: what port to listen to (default 8069)
        //   - progress(fac) : callback for search progress ( fac in [0,1] ) 
        autoconnect: function(options){
            var self = this;
            this.set_connection_status('connecting',{});
            var found_url = new $.Deferred();
            var success = new $.Deferred();

            if ( options.force_ip ){
                // if the ip is forced by server config, bailout on fail
                found_url = this.try_hard_to_connect(options.force_ip, options)
            }else if( localStorage['hw_proxy_url'] ){
                // try harder when we remember a good proxy url
                found_url = this.try_hard_to_connect(localStorage['hw_proxy_url'], options)
                    .then(null,function(){
                        return self.find_proxy(options);
                    });
            }else{
                // just find something quick
                found_url = this.find_proxy(options);
            }

            success = found_url.then(function(url){
                    return self.connect(url);
                });

            success.fail(function(){
                self.set_connection_status('disconnected');
            });

            return success;
        },

        // starts a loop that updates the connection status
        keepalive: function(){
            var self = this;
            if(!this.keptalive){
                this.keptalive = true;
                function status(){
                    self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})       
                        .then(function(driver_status){
                            self.set_connection_status('connected',driver_status);
                        },function(){
                            if(self.get('status').status !== 'connecting'){
                                self.set_connection_status('disconnected');
                            }
                        }).always(function(){
                            setTimeout(status,5000);
                        });
                }
                status();
            };
        },

        message : function(name,params){
            var callbacks = this.notifications[name] || [];
            for(var i = 0; i < callbacks.length; i++){
                callbacks[i](params);
            }
            if(this.get('status').status !== 'disconnected'){
                return this.connection.rpc('/hw_proxy/' + name, params || {});       
            }else{
                return (new $.Deferred()).reject();
            }
        },

        // try several time to connect to a known proxy url
        try_hard_to_connect: function(url,options){
            options   = options || {};
            var port  = ':' + (options.port || '8069');

            this.set_connection_status('connecting');

            if(url.indexOf('//') < 0){
                url = 'http://'+url;
            }

            if(url.indexOf(':',5) < 0){
                url = url+port;
            }

            // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
            function try_real_hard_to_connect(url, retries, done){

                done = done || new $.Deferred();

                var c = $.ajax({
                    url: url + '/hw_proxy/hello',
                    method: 'GET',
                    timeout: 1000,
                })
                .done(function(){
                    done.resolve(url);
                })
                .fail(function(){
                    if(retries > 0){
                        try_real_hard_to_connect(url,retries-1,done);
                    }else{
                        done.reject();
                    }
                });
                return done;
            }

            return try_real_hard_to_connect(url,3);
        },

        // returns as a deferred a valid host url that can be used as proxy.
        // options:
        //   - port: what port to listen to (default 8069)
        //   - progress(fac) : callback for search progress ( fac in [0,1] ) 
        find_proxy: function(options){
            options = options || {};
            var self  = this;
            var port  = ':' + (options.port || '8069');
            var urls  = [];
            var found = false;
            var parallel = 8;
            var done = new $.Deferred(); // will be resolved with the proxies valid urls
            var threads  = [];
            var progress = 0;


            urls.push('http://localhost'+port);
            for(var i = 0; i < 256; i++){
                urls.push('http://192.168.0.'+i+port);
                urls.push('http://192.168.1.'+i+port);
                urls.push('http://10.0.0.'+i+port);
            }

            var prog_inc = 1/urls.length; 

            function update_progress(){
                progress = found ? 1 : progress + prog_inc;
                if(options.progress){
                    options.progress(progress);
                }
            }

            function thread(done){
                var url = urls.shift();

                done = done || new $.Deferred();

                if( !url || found || !self.searching_for_proxy ){ 
                    done.resolve();
                    return done;
                }

                var c = $.ajax({
                        url: url + '/hw_proxy/hello',
                        method: 'GET',
                        timeout: 400, 
                    }).done(function(){
                        found = true;
                        update_progress();
                        done.resolve(url);
                    })
                    .fail(function(){
                        update_progress();
                        thread(done);
                    });

                return done;
            }

            this.searching_for_proxy = true;

            for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
                threads.push(thread());
            }
            
            $.when.apply($,threads).then(function(){
                var urls = [];
                for(var i = 0; i < arguments.length; i++){
                    if(arguments[i]){
                        urls.push(arguments[i]);
                    }
                }
                done.resolve(urls[0]);
            });

            return done;
        },

        stop_searching: function(){
            this.searching_for_proxy = false;
            this.set_connection_status('disconnected');
        },

        // this allows the client to be notified when a proxy call is made. The notification 
        // callback will be executed with the same arguments as the proxy call
        add_notification: function(name, callback){
            if(!this.notifications[name]){
                this.notifications[name] = [];
            }
            this.notifications[name].push(callback);
        },
        
        // returns the weight on the scale. 
        scale_read: function(){
            var self = this;
            var ret = new $.Deferred();
            if (self.use_debug_weight) {
                return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'});
            }
            this.message('scale_read',{})
                .then(function(weight){
                    ret.resolve(weight);
                }, function(){ //failed to read weight
                    ret.resolve({weight:0.0, unit:'Kg', info:'ok'});
                });
            return ret;
        },

        // sets a custom weight, ignoring the proxy returned value. 
        debug_set_weight: function(kg){
            this.use_debug_weight = true;
            this.debug_weight = kg;
        },

        // resets the custom weight and re-enable listening to the proxy for weight values
        debug_reset_weight: function(){
            this.use_debug_weight = false;
            this.debug_weight = 0;
        },

        // ask for the cashbox (the physical box where you store the cash) to be opened
        open_cashbox: function(){
            return this.message('open_cashbox');
        },

        /* 
         * ask the printer to print a receipt
         */
        print_receipt: function(receipt){
            var self = this;
            if(receipt){
                this.receipt_queue.push(receipt);
            }
            var aborted = false;
            function send_printing_job(){
                if (self.receipt_queue.length > 0){
                    var r = self.receipt_queue.shift();
                    self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
                        .then(function(){
                            send_printing_job();
                        },function(error){
                            if (error) {
                                self.pos.pos_widget.screen_selector.show_popup('error-traceback',{
                                    'message': _t('Printing Error: ') + error.data.message,
                                    'comment': error.data.debug,
                                });
                                return;
                            }
                            self.receipt_queue.unshift(r)
                        });
                }
            }
            send_printing_job();
        },

        // asks the proxy to log some information, as with the debug.log you can provide several arguments.
        log: function(){
            return this.message('log',{'arguments': _.toArray(arguments)});
        },

    });

    // this module interfaces with the barcode reader. It assumes the barcode reader
    // is set-up to act like  a keyboard. Use connect() and disconnect() to activate 
    // and deactivate the barcode reader. Use set_action_callbacks to tell it
    // what to do when it reads a barcode.
    module.BarcodeReader = instance.web.Class.extend({
        actions:[
            'product',
            'cashier',
            'client',
        ],

        init: function(attributes){
            this.pos = attributes.pos;
            this.action_callback = {};
            this.proxy = attributes.proxy;
            this.remote_scanning = false;
            this.remote_active = 0;

            this.action_callback_stack = [];

            this.add_barcode_patterns(attributes.patterns || {
                'product':  ['xxxxxxxxxxxxx'],
                'cashier':  ['041xxxxxxxxxx'],
                'client':   ['042xxxxxxxxxx'],
                'weight':   ['21xxxxxNNDDDx'],
                'discount': ['22xxxxxxxxNNx'],
                'price':    ['23xxxxxNNNDDx'],
            });

        },

        add_barcode_patterns: function(patterns){
            this.patterns = this.patterns || {};
            for(type in patterns){
                this.patterns[type] = this.patterns[type] || [];

                var patternlist = patterns[type];
                if( typeof patternlist === 'string'){
                    patternlist = patternlist.split(',');
                }
                for(var i = 0; i < patternlist.length; i++){
                    var pattern = patternlist[i].trim().substring(0,12);
                    if(!pattern.length){
                        continue;
                    }
                    pattern = pattern.replace(/[x\*]/gi,'x');
                    while(pattern.length < 12){
                        pattern += 'x';
                    }
                    this.patterns[type].push(pattern);
                }
            }

            this.sorted_patterns = [];
            for (var type in this.patterns){
                var patterns = this.patterns[type];
                for(var i = 0; i < patterns.length; i++){
                    var pattern = patterns[i];
                    var score = 0;
                    for(var j = 0; j < pattern.length; j++){
                        if(pattern[j] != 'x'){
                            score++;
                        }
                    }
                    this.sorted_patterns.push({type:type, pattern:pattern,score:score});
                }
            }
            this.sorted_patterns.sort(function(a,b){
                return b.score - a.score;
            });

        },

        save_callbacks: function(){
            var callbacks = {};
            for(name in this.action_callback){
                callbacks[name] = this.action_callback[name];
            }
            this.action_callback_stack.push(callbacks);
        },

        restore_callbacks: function(){
            if(this.action_callback_stack.length){
                var callbacks = this.action_callback_stack.pop();
                this.action_callback = callbacks;
            }
        },
       
        // when an ean is scanned and parsed, the callback corresponding
        // to its type is called with the parsed_ean as a parameter. 
        // (parsed_ean is the result of parse_ean(ean)) 
        // 
        // callbacks is a Map of 'actions' : callback(parsed_ean)
        // that sets the callback for each action. if a callback for the
        // specified action already exists, it is replaced. 
        // 
        // possible actions include : 
        // 'product' | 'cashier' | 'client' | 'discount' 
    
        set_action_callback: function(action, callback){
            if(arguments.length == 2){
                this.action_callback[action] = callback;
            }else{
                var actions = arguments[0];
                for(action in actions){
                    this.set_action_callback(action,actions[action]);
                }
            }
        },

        //remove all action callbacks 
        reset_action_callbacks: function(){
            for(action in this.action_callback){
                this.action_callback[action] = undefined;
            }
        },
        // returns the checksum of the ean, or -1 if the ean has not the correct length, ean must be a string
        ean_checksum: function(ean){
            var code = ean.split('');
            if(code.length !== 13){
                return -1;
            }
            var oddsum = 0, evensum = 0, total = 0;
            code = code.reverse().splice(1);
            for(var i = 0; i < code.length; i++){
                if(i % 2 == 0){
                    oddsum += Number(code[i]);
                }else{
                    evensum += Number(code[i]);
                }
            }
            total = oddsum * 3 + evensum;
            return Number((10 - total % 10) % 10);
        },
        // returns true if the ean is a valid EAN codebar number by checking the control digit.
        // ean must be a string
        check_ean: function(ean){
            return /^\d+$/.test(ean) && this.ean_checksum(ean) === Number(ean[ean.length-1]);
        },
        // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
        sanitize_ean:function(ean){
            ean = ean.substr(0,13);

            for(var n = 0, count = (13 - ean.length); n < count; n++){
                ean = ean + '0';
            }
            return ean.substr(0,12) + this.ean_checksum(ean);
        },
        
        // attempts to interpret an ean (string encoding an ean)
        // it will check its validity then return an object containing various
        // information about the ean.
        // most importantly : 
        // - code    : the ean
        // - type   : the type of the ean: 
        //      'price' |  'weight' | 'product' | 'cashier' | 'client' | 'discount' | 'error'
        //
        // - value  : if the id encodes a numerical value, it will be put there
        // - base_code : the ean code with all the encoding parts set to zero; the one put on
        //               the product in the backend

        parse_ean: function(ean){
            var self = this;
            var parse_result = {
                encoding: 'ean13',
                type:'error',  
                code:ean,
                base_code: ean,
                value: 0,
            };

            if (!this.check_ean(ean)){
                return parse_result;
            }

            function is_number(char){
                n = char.charCodeAt(0);
                return n >= 48 && n <= 57;
            }

            function match_pattern(ean,pattern){
                for(var i = 0; i < pattern.length; i++){
                    var p = pattern[i];
                    var e = ean[i];
                    if( is_number(p) && p !== e ){
                        return false;
                    }
                }
                return true;
            }
            
            function get_value(ean,pattern){
                var value = 0;
                var decimals = 0;
                for(var i = 0; i < pattern.length; i++){
                    var p = pattern[i];
                    var v = parseInt(ean[i]);
                    if( p === 'N'){
                        value *= 10;
                        value += v;
                    }else if( p === 'D'){
                        decimals += 1;
                        value += v * Math.pow(10,-decimals);
                    }
                }
                return value;
            }

            function get_basecode(ean,pattern){
                var base = '';
                for(var i = 0; i < pattern.length; i++){
                    var p = pattern[i];
                    var v = ean[i];
                    if( p === 'x'  || is_number(p)){
                        base += v;
                    }else{
                        base += '0';
                    }
                }
                return self.sanitize_ean(base);
            }

            var patterns = this.sorted_patterns;

            for(var i = 0; i < patterns.length; i++){
                if(match_pattern(ean,patterns[i].pattern)){
                    parse_result.type  = patterns[i].type;
                    parse_result.value = get_value(ean,patterns[i].pattern);
                    parse_result.base_code = get_basecode(ean,patterns[i].pattern);
                    return parse_result;
                }
            }

            return parse_result;
        },
        
        scan: function(code){
            if(code.length < 3){
                return;
            }else if(code.length === 13 && this.check_ean(code)){
                var parse_result = this.parse_ean(code);
            }else if(code.length === 12 && this.check_ean('0'+code)){
                // many barcode scanners strip the leading zero of ean13 barcodes.
                // This is because ean-13 are UCP-A with an additional zero at the beginning,
                // so by stripping zeros you get retrocompatibility with UCP-A systems.
                var parse_result = this.parse_ean('0'+code);
            }else if(this.pos.db.get_product_by_reference(code)){
                var parse_result = {
                    encoding: 'reference',
                    type: 'product',
                    code: code,
                };
            }else{
                var parse_result = {
                    encoding: 'error',
                    type: 'error',
                    code: code,
                };
            }

            if(parse_result.type in {'product':'', 'weight':'', 'price':''}){    //ean is associated to a product
                if(this.action_callback['product']){
                    this.action_callback['product'](parse_result);
                }
            }else{
                if(this.action_callback[parse_result.type]){
                    this.action_callback[parse_result.type](parse_result);
                }
            }
        },

        // starts catching keyboard events and tries to interpret codebar 
        // calling the callbacks when needed.
        connect: function(){

            var self = this;
            var code = "";
            var timeStamp  = 0;
            var onlynumbers = true;
            var timeout = null;

            this.handler = function(e){

                if(e.which === 13){ //ignore returns
                    e.preventDefault();
                    return;
                }

                if(timeStamp + 50 < new Date().getTime()){
                    code = "";
                    onlynumbers = true;
                }

                timeStamp = new Date().getTime();
                clearTimeout(timeout);

                if( e.which < 48 || e.which >= 58 ){ // not a number
                    onlynumbers = false;
                }

                code += String.fromCharCode(e.which);

                // we wait for a while after the last input to be sure that we are not mistakingly
                // returning a code which is a prefix of a bigger one :
                // Internal Ref 5449 vs EAN13 5449000...

                timeout = setTimeout(function(){
                    self.scan(code);
                    code = "";
                    onlynumbers = true;
                },100);
            };

            $('body').on('keypress', this.handler);
        },

        // stops catching keyboard events 
        disconnect: function(){
            $('body').off('keypress', this.handler)
        },

        // the barcode scanner will listen on the hw_proxy/scanner interface for 
        // scan events until disconnect_from_proxy is called
        connect_to_proxy: function(){ 
            var self = this;
            this.remote_scanning = true;
            if(this.remote_active >= 1){
                return;
            }
            this.remote_active = 1;

            function waitforbarcode(){
                return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
                    .then(function(barcode){
                        if(!self.remote_scanning){ 
                            self.remote_active = 0;
                            return; 
                        }
                        self.scan(barcode);
                        waitforbarcode();
                    },
                    function(){
                        if(!self.remote_scanning){
                            self.remote_active = 0;
                            return;
                        }
                        setTimeout(waitforbarcode,5000);
                    });
            }
            waitforbarcode();
        },

        // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
        disconnect_from_proxy: function(){
            this.remote_scanning = false;
        },
    });

}
