/*
 * Modelled Trace API
 *
 * This file is part of ktbs4js.
 *
 * ktbs4js is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * ktbs4js is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with ktbs4js.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
/* FIXME: properly use require.js feature. This will do for debugging in the meantime */
window.tracemanager = (function($) {
     // If there are more than MAX_FAILURE_COUNT synchronisation
     // failures, then disable synchronisation
    var MAX_FAILURE_COUNT = 20;

     // If there are more than MAX_BUFFER_SIZE obsels in the buffer,
     // then "compress" them as a single "ktbsFullBuffer"
    var  MAX_BUFFER_SIZE = 500;

    var _replacement = {
         ';': '"',
         '"': ';',
         '#': '%23',
         '&': '%26',
         '?': '%3F'
     };
     var BufferedService_prototype = {
         /*
          *  Buffered service for traces
          */
         // url: "",
         // buffer: [],
         // isReady: false,
         // timer: null,
         // failureCount: 0,

         /* Flush buffer */
         flush: function() {
             // FIXME: add mutex on this.buffer
             if (! this.isReady)
             {
                 if (window.console) window.console.log("Sync service not ready");
             } else if (this.failureCount > MAX_FAILURE_COUNT)
             {
                 // Disable synchronisation
                 this.set_sync_mode('none');
             } else if (this.buffer.length) {
                 var temp = this.buffer;
                 this.buffer = [];

                 if (this.mode == 'GET')
                 {
                     // GET mode: do some data mangline. We mark the
                     // "compressed" nature of the generated JSON by
                     // prefixing it with c
                     var data = 'c' + JSON.stringify(temp.map(function (o) { return o.toCompactJSON(); }));
                     // Swap " (very frequent, which will be
                     // serialized into %22) and ; (rather rare), this
                     // saves some bytes
                     data = data.replace(/[;"#?&]/g, function(s){ return _replacement[s]; });
                     // FIXME: check data length (< 2K is safe)
                     var request=$('<img />').error( function() { this.failureCount += 1; })
                         .load( function() { this.failureCount = 0; })
                         .attr('src', this.url + 'trace/?data=' + data);
                 }
                 else
                 {
                     $.ajax({ url: this.url + 'trace/',
                              type: 'POST',
                              contentType: 'application/json',
                              data: JSON.stringify(temp.map(function (o) { return o.toJSON(); })),
                              processData: false,
                              // Type of the returned data.
                              dataType: "html",
                              error: function(jqXHR, textStatus, errorThrown) {
                                  if (window.console) window.console.log("Error when sending buffer:", textStatus);
                                  this.failureCount += 1;
                              },
                              success: function(data, textStatus, jqXHR) {
                                  // Reset failureCount to 0 as soon as there is 1 valid answer
                                  this.failureCount = 0;
                              }
                            });
                 }
             }
         },

         /* Sync mode: delayed, sync (immediate sync), none (no
          * synchronisation with server, the trace has to be explicitly saved
          * if needed */
         set_sync_mode: function(mode, default_subject) {
             this.sync_mode = mode;
             if (! this.isReady && mode !== "none")
                 this.init(default_subject);
             if (mode == 'delayed') {
                 this.start_timer();
             } else {
                 this.stop_timer();
             }
         },

         /* Enqueue an obsel */
         enqueue: function(obsel) {
             if (this.buffer.length > MAX_BUFFER_SIZE)
             {
                 obsel = new Obsel('ktbsFullBuffer', this.buffer[0].begin,
                                   this.buffer[this.buffer.length - 1].end, this.buffer[0].subject);
                 obsel.trace = this.buffer[0].trace;
                 this.buffer = [];
             }
             this.buffer.push(obsel);
             if (this.sync_mode === 'sync') {
                 // Immediate sync of the obsel.
                 this.flush();
             }
         },

         start_timer: function() {
             var self = this;
             if (this.timer === null) {
                 this.timer = window.setInterval(function() {
                                                     self.flush();
                                                 }, this.timeOut);
             }
         },

         stop_timer: function() {
             if (this.timer !== null) {
                 window.clearInterval(this.timer);
                 this.timer = null;
             }
         },

         /*
          * Initialize the sync service
          */
         init: function(default_subject) {
             var self = this;
             if (this.isReady)
                 /* Already initialized */
                 return;
             if (typeof default_subject === 'undefined')
                 default_subject = 'anonymous';
             if (this.mode == 'GET')
             {
                 var request=$('<img/>').attr('src', this.url + 'login?userinfo={"default_subject": "' + default_subject + '"}');
                 // Do not wait for the return, assume it is
                 // initialized. This assumption will not work anymore
                 // if login returns some necessary information
                 this.isReady = true;
             }
             else
             {
                 $.ajax({ url: this.url + 'login',
                          type: 'POST',
                          data: 'userinfo={"default_subject":"' + default_subject + '"}',
                          success: function(data, textStatus, jqXHR) {
                              self.isReady = true;
                              if (self.buffer.length) {
                                  self.flush();
                              }
                          }
                        });
             }
         }
     };
     var BufferedService = function(url, mode) {
         this.url = url;
         this.buffer = [];
         this.isReady = false;
         this.timer = null;
         this.failureCount = 0;
         // sync_mode is either "none", "sync" or "buffered"
         this.sync_mode = "none";
         /* mode can be either POST or GET */
         if (mode == 'POST' || mode == 'GET')
             this.mode = mode;
         else
             this.mode = 'POST';
         /* Flush buffer every timeOut ms if the sync_mode is delayed */
         this.timeOut = 2000;
     };
     BufferedService.prototype = BufferedService_prototype;

     var Trace_prototype = {
         /* FIXME: We could/should use a sorted list such as
          http://closure-library.googlecode.com/svn/docs/class_goog_structs_AvlTree.html
          to speed up queries based on time */
         obsels: [],
         /* Trace URI */
         uri: "",
         default_subject: "",
         /* baseuri is used as the base URI to resolve relative
          * attribute-type names in obsels. Strictly speaking, this
          * should rather be expressed as a reference to model, or
          * more generically, as a qname/URI dict */
         baseuri: "",
         /* Mapping of obsel type or property name to a compact
          * representation (shorthands).
          */
         shorthands: null,
         syncservice: null,

         /* Define the trace URI */
         set_uri: function(uri) {
             this.uri = uri;
         },

         /* Sync mode: delayed, sync (immediate sync), none (no
          * synchronisation with server, the trace has to be explicitly saved
          * if needed */
         set_sync_mode: function(mode) {
             if (this.syncservice !== null) {
                 this.syncservice.set_sync_mode(mode, this.default_subject);
             }
         },

         /*
          * Return a list of the obsels of this trace matching the parameters
          */
         list_obsels: function(_begin, _end, _reverse) {
             var res;
             if (typeof _begin !== 'undefined' || typeof _end !== 'undefined') {
                 /*
                  * Not optimized yet.
                  */
                 res = [];
                 var l = this.obsels.length;
                 for (var i = 0; i < l; i++) {
                     var o = this.obsels[i];
                     if ((typeof _begin !== 'undefined' && o.begin > _begin) && (typeof _end !== 'undefined' && o.end < _end)) {
                         res.push(o);
                     }
                 }
             }

             if (typeof _reverse !== 'undefined') {
                 if (res !== undefined) {
                     /* Should reverse the whole list. Make a copy. */
                     res = this.obsels.slice(0);
                 }
                 res.sort(function(a, b) { return b.begin - a.begin; });
                 return res;
             }

             if (res === undefined) {
                 res = this.obsels;
             }
             return res;

         },

         /*
          * Return the obsel of this trace identified by the URI, or undefined
          */
         get_obsel: function(id) {
             for (var i = 0; i < this.obsels.length; i++) {
                 /* FIXME: should check against variations of id/uri, take this.baseuri into account */
                 if (this.obsels[i].uri === id) {
                     return this.obsels[i];
                 }
             }
             return undefined;
         },

         set_default_subject: function(subject) {
             // FIXME: if we call this method after the sync_service
             // init method, then the default_subject will not be
             // consistent anymore. Maybe we should then call init() again?
             this.default_subject = subject;
         },

         get_default_subject: function() {
             return this.default_subject;
         },

         /* (type:ObselType, begin:int, end:int?, subject:str?, attributes:[AttributeType=>any]?) */
         /* Create a new obsel and add it to the trace */
         create_obsel: function(type, begin, end, subject, _attributes) {
             var o = new Obsel(type, begin, end, subject);
             if (typeof _attributes !== 'undefined') {
                 o.attributes = _attributes;
             }
             o.trace = this;
             this.obsels.push(o);
             if (this.syncservice !== null)
                 this.syncservice.enqueue(o);
         },

         /* Helper methods */

         /* Create a new obsel with the given attributes */
         trace: function(type, _attributes, _begin, _end, _subject) {
             var t = (new Date()).getTime();
             if (typeof begin === 'undefined') {
                 _begin = t;
             }
             if (typeof end === 'undefined') {
                 _end = _begin;
             }
             if (typeof subject === 'undefined') {
                 _subject = this.default_subject;
             }
             if (typeof _attributes === 'undefined') {
                 _attributes = {};
             }
             return this.create_obsel(type, _begin, _end, _subject, _attributes);
         }
     };

     var Trace = function(uri, requestmode) {
         /* FIXME: We could/should use a sorted list such as
          http://closure-library.googlecode.com/svn/docs/class_goog_structs_AvlTree.html
          to speed up queries based on time */
         this.obsels = [];
         /* Trace URI */
         if (uri === undefined)
             uri = "";
         this.uri = uri;
         this.sync_mode = "none";
         this.default_subject = "";
         this.shorthands = {};
         /* baseuri is used a the base URI to resolve relative attribute names in obsels */
         this.baseuri = "";

         this.syncservice = new BufferedService(uri, requestmode);
         $(window).unload( function () {
                               if (this.syncservice && this.sync_mode !== 'none') {
                                   this.syncservice.flush();
                                   this.syncservice.stop_timer();
                               }
                           });
     };
     Trace.prototype = Trace_prototype;

     var Obsel_prototype = {
         /* The following attributes are here for documentation
          * purposes. They MUST be defined in the constructor
          * function. */
         trace: undefined,
         type: undefined,
         begin: undefined,
         end: undefined,
         subject: undefined,
         /* Dictionary indexed by ObselType URIs */
         attributes: {},

         /* Method definitions */
         get_trace: function() {
             return this.trace;
         },

         get_obsel_type: function() {
             return this.type;
         },
         get_begin: function() {
             return this.begin;
         },
         get_end: function() {
             return this.end;
         },
         get_subject: function() {
             return this.subject;
         },

         list_attribute_types: function() {
             var result = [];
             for (var prop in this.attributes) {
                 if (this.attributes.hasOwnProperty(prop))
                     result.push(prop);
             }
             /* FIXME: we return URIs here instead of AttributeType elements */
             return result;
         },

         list_relation_types: function() {
             /* FIXME: not implemented yet */
         },

         list_related_obsels: function (rt) {
             /* FIXME: not implemented yet */
         },
         list_inverse_relation_types: function () {
             /* FIXME: not implemented yet */
         },
         list_relating_obsels: function (rt) {
             /* FIXME: not implemented yet */
         },
         /*
          * Return the value of the given attribute type for this obsel
          */
         get_attribute_value: function(at) {
             if (typeof at === "string")
                 /* It is a URI */
                 return this.attributes[at];
             else
                 /* FIXME: check that at is instance of AttributeType */
                 return this.attributes[at.uri];
         },


         /* obsel modification (trace amendment) */

         set_attribute_value: function(at, value) {
             if (typeof at === "string")
                 /* It is a URI */
                 this.attributes[at] = value;
             /* FIXME: check that at is instance of AttributeType */
             else
                 this.attributes[at.uri] = value;
         },

         del_attribute_value: function(at) {
             if (typeof at === "string")
                 /* It is a URI */
                 delete this.attributes[at];
             /* FIXME: check that at is instance of AttributeType */
             else
                 delete this.attributes[at.uri];
         },

         add_related_obsel: function(rt, value) {
             /* FIXME: not implemented yet */
         },

         del_related_obsel: function(rt, value) {
             /* FIXME: not implemented yet */
         },

         /*
          * Return a JSON representation of the obsel
          */
         toJSON: function() {
             var r = {
                 "@id": this.id,
                 "@type": this.type,
                 "begin": this.begin,
                 "end": this.end,
                 "subject": this.subject
             };
             for (var prop in this.attributes) {
                 if (this.attributes.hasOwnProperty(prop))
                     r[prop] = this.attributes[prop];
             }
             return r;
         },

         /*
          * Return a compact JSON representation of the obsel.
          * Use predefined + custom shorthands for types/properties
          */
         toCompactJSON: function() {
             var r = {
                 "@t": (this.trace.shorthands.hasOwnProperty(this.type) ? this.trace.shorthands[this.type] : this.type),
                 "@b": this.begin,
             };
             // Transmit subject only if different from default_subject
             if (this.subject !== this.trace.default_subject)
                 r["@s"] = this.subject;

             // Store duration (to save some bytes) and only if it is non-null
             if (this.begin !== this.end)
                 r["@d"] = this.end - this.begin;

             // Store id only if != ""
             if (this.id !== "")
                 r["@i"] = this.id;

             for (var prop in this.attributes) {
                 if (this.attributes.hasOwnProperty(prop))
                 {
                     var v = this.attributes[prop];
                     r[prop] = this.trace.shorthands.hasOwnProperty(v) ? this.trace.shorthands[v] : v;
                 }
             }
             return r;
         },

         toJSONstring: function() {
             return JSON.stringify(this.toJSON());
         }
     };

     var Obsel = function(type, begin, end, subject, attributes) {
         this.trace = undefined;
         this.uri = "";
         this.id = "";
         this.type = type;
         this.begin = begin;
         this.end = end;
         this.subject = subject;
         /* Is the obsel synched with the server ? */
         this.sync_status = false;
         /* Dictionary indexed by ObselType URIs */
         this.attributes = {};
     };
     Obsel.prototype = Obsel_prototype;

     var TraceManager_prototype = {
         traces: [],
         /*
          * Return the trace with id name
          * If it was not registered, return undefined.
          */
         get_trace: function(name) {
             return this.traces[name];
         },

         /*
          * Explicitly create and initialize a new trace with the given name.
          * The optional uri parameter allows to initialize the trace URI.
          *
          * If another existed with the same name before, then it is replaced by a new one.
          */
         init_trace: function(name, params)
         {
             if (window.console) window.console.log("init_trace", params);
             url = params.url ? params.url : "";
             requestmode = params.requestmode ? params.requestmode : "POST";
             syncmode = params.syncmode ? params.syncmode : "none";
             default_subject = params.default_subject ? params.default_subject : "default";
             var t = new Trace(url, requestmode);
             t.set_default_subject(default_subject);
             t.set_sync_mode(syncmode);
             this.traces[name] = t;
             return t;
         }
     };

     var TraceManager = function() {
         this.traces = {};
     };
     TraceManager.prototype = TraceManager_prototype;

     var tracemanager  = new TraceManager();
     return tracemanager;
 })(jQuery);
