text: 'use strict';
  
  module.exports = function (Tuio) {
    Tuio.Client = Tuio.Model.extend({
      port: 3333,
      host: '127.0.0.1',
      oscParser: null,
      socket: null,
      connected: null,
      objectList: null,
      aliveObjectList: null,
      newObjectList: null,
      cursorList: null,
      aliveCursorList: null,
      newCursorList: null,
      frameObjects: null,
      frameCursors: null,
      freeCursorList: null,
      maxCursorId: null,
      currentFrame: null,
      currentTime: null,
  
      initialize: function (params) {
        if (params && params.port) {
          this.port = params.port;
        }
  
        if (params && params.host) {
          this.host = params.host;
        }
  
        this.oscParser = require('./osc-parser.js');
        this.connected = false;
        this.objectList = {};
        this.aliveObjectList = [];
        this.newObjectList = [];
        this.cursorList = {};
        this.aliveCursorList = [];
        this.newCursorList = [];
        this.frameObjects = [];
        this.frameCursors = [];
        this.freeCursorList = [];
        this.maxCursorId = -1;
        this.currentFrame = 0;
        this.currentTime = null;
  
        Tuio._.bindAll(this, 'onListening', 'acceptBundle', 'onClose');
      },
  
      listen: function () {
        Tuio.Time.initSession();
        this.currentTime = new Tuio.Time();
        this.currentTime.reset();
  
        var dgram = require('dgram');
        this.socket = dgram.createSocket('udp4');
        this.socket.on('listening', this.onListening);
        this.socket.on('close', this.onClose);
  
        this.socket.bind(this.port, this.host);
      },
  
      onListening: function () {
        var that = this;
        this.socket.on('message', function (msg) {
          var oscBundle = that.oscParser.decode(msg);
          that.acceptBundle(oscBundle);
        });
        this.connected = true;
        this.trigger('listening');
  
        var address = this.socket.address();
        console.log('TuioClient listening on: ' + address.address + ':' + address.port);
      },
  
      onClose: function () {
        this.connected = false;
        this.trigger('close');
      },
  
      isConnected: function () {
        return this.connected;
      },
  
      getTuioObjects: function () {
        return Tuio._.clone(this.objectList);
      },
  
      getTuioCursors: function () {
        return Tuio._.clone(this.cursorList);
      },
  
      getTuioObject: function (sid) {
        return this.objectList[sid];
      },
  
      getTuioCursor: function (sid) {
        return this.cursorList[sid];
      },
  
      acceptBundle: function (oscBundle) {
        var msg = null;
  
        for (var i = 0, max = oscBundle.length; i < max; i++) {
          msg = oscBundle[i];
          switch (msg[0]) {
            case '/tuio/2Dobj':
            case '/tuio/2Dcur':
              this.acceptMessage(msg);
              break;
          }
        }
      },
  
      acceptMessage: function (oscMessage) {
        var address = oscMessage[0],
          command = oscMessage[1],
          args = oscMessage.slice(2, oscMessage.length);
        switch (address) {
          case '/tuio/2Dobj':
            this.handleObjectMessage(command, args);
            break;
          case '/tuio/2Dcur':
            this.handleCursorMessage(command, args);
            break;
        }
      },
  
      handleObjectMessage: function (command, args) {
        switch (command) {
          case 'set':
            this.objectSet(args);
            break;
          case 'alive':
            this.objectAlive(args);
            break;
          case 'fseq':
            this.objectFseq(args);
            break;
        }
      },
  
      handleCursorMessage: function (command, args) {
        switch (command) {
          case 'set':
            this.cursorSet(args);
            break;
          case 'alive':
            this.cursorAlive(args);
            break;
          case 'fseq':
            this.cursorFseq(args);
            break;
        }
      },
  
      objectSet: function (args) {
        var sid = args[0],
          cid = args[1],
          xPos = args[2],
          yPos = args[3],
          angle = args[4],
          xSpeed = args[5],
          ySpeed = args[6],
          rSpeed = args[7],
          mAccel = args[8],
          rAccel = args[9];
  
        if (!Tuio._.has(this.objectList, sid)) {
          var addObject = new Tuio.Object({
            si: sid,
            sym: cid,
            xp: xPos,
            yp: yPos,
            a: angle
          });
          this.frameObjects.push(addObject);
        } else {
          var tobj = this.objectList[sid];
          if (!tobj) {
            return;
          }
          if (
            (tobj.xPos !== xPos) ||
            (tobj.yPos !== yPos) ||
            (tobj.angle !== angle) ||
            (tobj.xSpeed !== xSpeed) ||
            (tobj.ySpeed !== ySpeed) ||
            (tobj.rotationSpeed !== rSpeed) ||
            (tobj.motionAccel !== mAccel) ||
            (tobj.rotationAccel !== rAccel)
          ) {
  
            var updateObject = new Tuio.Object({
              si: sid,
              sym: cid,
              xp: xPos,
              yp: yPos,
              a: angle
            });
  
            updateObject.update({
              xp: xPos,
              yp: yPos,
              a: angle,
              xs: xSpeed,
              ys: ySpeed,
              rs: rSpeed,
              ma: mAccel,
              ra: rAccel
            });
  
            this.frameObjects.push(updateObject);
          }
        }
      },
  
      objectAlive: function (args) {
        var removeObject = null;
        this.newObjectList = args;
        this.aliveObjectList = Tuio._.difference(this.aliveObjectList, this.newObjectList);
  
        for (var i = 0, max = this.aliveObjectList.length; i < max; i++) {
          removeObject = this.objectList[this.aliveObjectList[i]];
          if (removeObject) {
            removeObject.remove(this.currentTime);
            this.frameObjects.push(removeObject);
          }
        }
      },
  
      objectFseq: function (args) {
        var fseq = args[0],
          lateFrame = false,
          tobj = null;
  
        if (fseq > 0) {
          if (fseq > this.currentFrame) {
            this.currentTime = Tuio.Time.getSessionTime();
          }
          if ((fseq >= this.currentFrame) || ((this.currentFrame - fseq) > 100)) {
            this.currentFrame = fseq;
          } else {
            lateFrame = true;
          }
        } else if (Tuio.Time.getSessionTime().subtractTime(this.currentTime).getTotalMilliseconds() > 100) {
          this.currentTime = Tuio.Time.getSessionTime();
        }
  
        if (!lateFrame) {
          for (var i = 0, max = this.frameObjects.length; i < max; i++) {
            tobj = this.frameObjects[i];
            switch (tobj.getTuioState()) {
              case Tuio.Object.TUIO_REMOVED:
                this.objectRemoved(tobj);
                break;
              case Tuio.Object.TUIO_ADDED:
                this.objectAdded(tobj);
                break;
              default:
                this.objectDefault(tobj);
                break;
            }
          }
  
          this.trigger('refresh', Tuio.Time.fromTime(this.currentTime));
  
          var buffer = this.aliveObjectList;
          this.aliveObjectList = this.newObjectList;
          this.newObjectList = buffer;
        }
  
        this.frameObjects = [];
      },
  
      objectRemoved: function (tobj) {
        var removeObject = tobj;
        removeObject.remove(this.currentTime);
        this.trigger('removeTuioObject', removeObject);
        delete this.objectList[removeObject.getSessionId()];
      },
  
      objectAdded: function (tobj) {
        var addObject = new Tuio.Object({
          ttime: this.currentTime,
          si: tobj.getSessionId(),
          sym: tobj.getSymbolId(),
          xp: tobj.getX(),
          yp: tobj.getY(),
          a: tobj.getAngle()
        });
        this.objectList[addObject.getSessionId()] = addObject;
        this.trigger('addTuioObject', addObject);
      },
  
      objectDefault: function (tobj) {
        var updateObject = this.objectList[tobj.getSessionId()];
        if (
          (tobj.getX() !== updateObject.getX() && tobj.getXSpeed() === 0) ||
          (tobj.getY() !== updateObject.getY() && tobj.getYSpeed() === 0)) {
  
          updateObject.update({
            ttime: this.currentTime,
            xp: tobj.getX(),
            yp: tobj.getY(),
            a: tobj.getAngle()
          });
        } else {
          updateObject.update({
            ttime: this.currentTime,
            xp: tobj.getX(),
            yp: tobj.getY(),
            a: tobj.getAngle(),
            xs: tobj.getXSpeed(),
            ys: tobj.getYSpeed(),
            rs: tobj.getRotationSpeed(),
            ma: tobj.getMotionAccel(),
            ra: tobj.getRotationAccel()
          });
        }
  
        this.trigger('updateTuioObject', updateObject);
      },
  
      cursorSet: function (args) {
        var sid = args[0],
          xPos = args[1],
          yPos = args[2],
          xSpeed = args[3],
          ySpeed = args[4],
          mAccel = args[5];
  
        if (!Tuio._.has(this.cursorList, sid)) {
          var addCursor = new Tuio.Cursor({
            si: sid,
            ci: -1,
            xp: xPos,
            yp: yPos
          });
          this.frameCursors.push(addCursor);
        } else {
          var tcur = this.cursorList[sid];
          if (!tcur) {
            return;
          }
          if (
            (tcur.xPos !== xPos) ||
            (tcur.yPos !== yPos) ||
            (tcur.xSpeed !== xSpeed) ||
            (tcur.ySpeed !== ySpeed) ||
            (tcur.motionAccel !== mAccel)) {
  
            var updateCursor = new Tuio.Cursor({
              si: sid,
              ci: tcur.getCursorId(),
              xp: xPos,
              yp: yPos
            });
  
            updateCursor.update({
              xp: xPos,
              yp: yPos,
              xs: xSpeed,
              ys: ySpeed,
              ma: mAccel
            });
  
            this.frameCursors.push(updateCursor);
          }
        }
      },
  
      cursorAlive: function (args) {
        var removeCursor = null;
        this.newCursorList = args;
        this.aliveCursorList = Tuio._.difference(this.aliveCursorList, this.newCursorList);
  
        for (var i = 0, max = this.aliveCursorList.length; i < max; i++) {
          removeCursor = this.cursorList[this.aliveCursorList[i]];
          if (removeCursor) {
            removeCursor.remove(this.currentTime);
            this.frameCursors.push(removeCursor);
          }
        }
      },
  
      cursorFseq: function (args) {
        var fseq = args[0],
          lateFrame = false,
          tcur = null;
  
        if (fseq > 0) {
          if (fseq > this.currentFrame) {
            this.currentTime = Tuio.Time.getSessionTime();
          }
          if ((fseq >= this.currentFrame) || ((this.currentFrame - fseq) > 100)) {
            this.currentFrame = fseq;
          } else {
            lateFrame = true;
          }
        } else if (Tuio.Time.getSessionTime().subtractTime(this.currentTime).getTotalMilliseconds() > 100) {
          this.currentTime = Tuio.Time.getSessionTime();
        }
  
        if (!lateFrame) {
          for (var i = 0, max = this.frameCursors.length; i < max; i++) {
            tcur = this.frameCursors[i];
            switch (tcur.getTuioState()) {
              case Tuio.Cursor.TUIO_REMOVED:
                this.cursorRemoved(tcur);
                break;
              case Tuio.Cursor.TUIO_ADDED:
                this.cursorAdded(tcur);
                break;
              default:
                this.cursorDefault(tcur);
                break;
            }
          }
  
          this.trigger('refresh', Tuio.Time.fromTime(this.currentTime));
  
          var buffer = this.aliveCursorList;
          this.aliveCursorList = this.newCursorList;
          this.newCursorList = buffer;
        }
  
        this.frameCursors = [];
      },
  
      cursorRemoved: function (tcur) {
        var removeCursor = tcur;
        removeCursor.remove(this.currentTime);
  
        this.trigger('removeTuioCursor', removeCursor);
  
        delete this.cursorList[removeCursor.getSessionId()];
  
        if (removeCursor.getCursorId() === this.maxCursorId) {
          this.maxCursorId = -1;
          if (Tuio._.size(this.cursorList) > 0) {
            var maxCursor = Tuio._.max(this.cursorList, function (cur) {
              return cur.getCursorId();
            });
            if (maxCursor.getCursorId() > this.maxCursorId) {
              this.maxCursorId = maxCursor.getCursorId();
            }
  
            this.freeCursorList = Tuio._.without(this.freeCursorList, function (cur) {
              return cur.getCursorId() >= this.maxCursorId;
            });
          } else {
            this.freeCursorList = [];
          }
        } else if (removeCursor.getCursorId() < this.maxCursorId) {
          this.freeCursorList.push(removeCursor);
        }
      },
  
      cursorAdded: function (tcur) {
        var cid = Tuio._.size(this.cursorList),
          testCursor = null;
  
        if ((cid <= this.maxCursorId) && (this.freeCursorList.length > 0)) {
          var closestCursor = this.freeCursorList[0];
          for (var i = 0, max = this.freeCursorList.length; i < max; i++) {
            testCursor = this.freeCursorList[i];
            if (testCursor.getDistanceToPoint(tcur) < closestCursor.getDistanceToPoint(tcur)) {
              closestCursor = testCursor;
            }
          }
          cid = closestCursor.getCursorId();
          this.freeCursorList = Tuio._.without(this.freeCursorList, function (cur) {
            return cur.getCursorId() === cid;
          });
        } else {
          this.maxCursorId = cid;
        }
  
        var addCursor = new Tuio.Cursor({
          ttime: this.currentTime,
          si: tcur.getSessionId(),
          ci: cid,
          xp: tcur.getX(),
          yp: tcur.getY()
        });
        this.cursorList[addCursor.getSessionId()] = addCursor;
  
        this.trigger('addTuioCursor', addCursor);
      },
  
      cursorDefault: function (tcur) {
        var updateCursor = this.cursorList[tcur.getSessionId()];
        if (
          (tcur.getX() !== updateCursor.getX() && tcur.getXSpeed() === 0) ||
          (tcur.getY() !== updateCursor.getY() && tcur.getYSpeed() === 0)) {
  
          updateCursor.update({
            ttime: this.currentTime,
            xp: tcur.getX(),
            yp: tcur.getY()
          });
        } else {
          updateCursor.update({
            ttime: this.currentTime,
            xp: tcur.getX(),
            yp: tcur.getY(),
            xs: tcur.getXSpeed(),
            ys: tcur.getYSpeed(),
            ma: tcur.getMotionAccel()
          });
        }
  
        this.trigger('updateTuioCursor', updateCursor);
      }
    });
  };
  