dojo.provide("finder.ftabs.text");
dojo.require("finder.ftabs.loader");
dojo.require("finder.ftabs.errors");
dojo.require("dojo.date.locale");

/**
 * This class is used to resolve text messages and HTML templates, including some
 * very basic expression language stuff. Supported grammar right now is something
 * like:
 *
 *
 * TEMPLATE               = CONTENT? TEMPLATE_PART*
 * TEMPLATE_PART          = EXPR CONTENT
 * EXPR                   = "#{" EXPR_BODY "}"
 * EXPR_BODY              = SUBMESSAGE | REFERENCE | REFERENCE_WITH_FUNCT | STRING_CONST
 * SUBMESSAGE             = "m[" EXPR "]"
 * REFERENCE              = CONTEXT* ATOM
 * REFERENCE_WITH_FUNCT   = REFERENCE "." FUNCT
 * FUNCT                  = DATEF_FUNCT | TIMEF_FUNCT | TELF_FUNCT
 * DATEF_FUNCT            = "datef(" STRING_CONST ")"
 * TIMEF_FUNCT            = "timef(" STRING_CONST ")"
 * TELF_FUNCT             = "telf()"
 * STRING_CONST           = SQUOTE_CONST | DQUOTE_CONST
 * SQUOTE_CONST           = "'" CONST_CHAR_NO_SQUOT* "'"
 * DQUOTE_CONST           = "'" CONST_CHAR_NO_DQUOT* "'"
 * CONST_CHAR_NO_SQUOT    = [^]'}]
 * CONST_CHAR_NO_DQUOT    = [^]"}]
 * CONTEXT                = ATOM  "."
 * ATOM                   = ATOM_START_CHAR ATOM_CHAR*
 * ATOM_START_CHAR        = [a-zA-Z]
 * ATOM_CHAR              = [-_a-zA-Z0-9]
 *
 * We use dojo.date.locale for date and time formatting. So the strings supplied to "datef" or "timef"
 * are as accepted there, which are the strings supported by:
 *    http://www.unicode.org/reports/tr35/tr35-4.html#Date_Format_Patterns
 *
 * I understand that the above isn't particularly legible for some people, so here are some
 * text string examples, along with the strings they might produce in practice:
 *
 *   Today, #{date.datef('MMMM d')}
 *     -> Today, September 3
 *
 *   Hello, #{name}! Welcome to #{m['application.name']}!
 *     -> Hello, Phillip! Welcome to AT&T Family Map!
 *
 *   A message was sent to #{userInfo.phone.telf()} containing your temporary password.
 *     -> A message was sent to 555-111-1212 containing your temporary pasword.
 *
 * The telephone formatting code (ie "telf()") uses the telephone number format strings
 * from the, ie it works using the text string "phoneFormat.<length>". For US phones, that's
 * going to always use "phoneFormat.10". There was a conscious choice here to not allow
 * a specific string to be specified here, because that approach won't work for systems
 * that need to simultaneously support multiple phone number lengths. (A possible
 * enhancement might be to allow named phone number format styles, eg "full" and
 * "short", and to look for text strings with the name, eg "phoneFormat.full.10",
 * "phoneFormat.short.9". But there's no clear need for that yet. )
 */
dojo.declare("finder.ftabs.text.TextGetter", finder.ftabs.Loader, {

   constructor: function() {
      this.data = null;
      
      // looking for expressions of the form:
      //  #{<expr content>}
      // also trim whitespace from around contents of internals.
      // end with g to allow multiple matches.
      this.expressionRegex = /#\{[ \t\r\n]*([^ \t\r\n\}][^\}]*[^ \t\r\n\}])[ \t\r\n]*}/g;

      // Looking for expressions of the forms:
      //   m[<something>]
      // In practice, this is probably
      //   m["some.text.key"]
      //   m['some.text.key']
      // or
      //   m[someVariable]
      // this.submessageRegex = /^m\[(?:'([a-zA-Z0-9.]+)'|"([a-zA-Z0-9.]+)")\]$/;
      this.submessageRegex = /^m\[([^\]]+)]$/;

      // search for "<some content>" or '<some content>'
      // This is a string constant; however, it is NOT treated like a template. it's content
      // is assumed to be static.
      this.stringConstantRegex = /^(?:'([^']*)'|"([^"]*)")$/;

      // Looking for expressions of the forms:
      //    datef('<date format string>')
      //    datef("<date format string>")
      //    timef('<time format string>')
      //    timef("<time format string>")
      this.dateFormatRegexp = /^(date|time)f\((?:'([a-zA-Z-:, \/]+)'|"([a-zA-Z-:, \/]+)")\)$/;

      // Looking for expressions with the specific form:
      //    telf()
      this.telFormatRegexp = /^telf\(\)$/;

      this.load();
   },

   get : function(code, params) {

      var value = this.getRaw(code);

      if (!value) {
         var result = new String("UNDEFINED(" + code + ")");
         result._safetext = true;
         return result;
      }

      // Expression handling
      // We search for substrings of the form "#{expr}" - using similar syntax to the
      // Spring EL we use server-side.
      // ... I'm using the regexp to trim whitespace too, because it's easier that way.
      return this.template(value, params);
   },

   /**
    * Creates a string based on the supplied template and the provided parameters.
    * The contents of the template are assumed to be "safe".
    * The fields in the parameter map are considered "safe" only if:
    *   a) they are Numbers
    *   b) they are string objects with the property "_safetext" set to true.
    *
    * @param template
    * @param params
    */
   template : function(template, params) {
      if (dojo.isArray(template)) {
         template = template.join("");
      }

      template = template.replace(/^\s+|\s+$/g,"");

      // Expression handling
      // We search for substrings of the form "#{expr}" - using similar syntax to the
      // Spring EL we use server-side.
      // ... I'm using the regexp to trim whitespace too, because it's easier that way.
      var result = dojo.replace(template,
            dojo.hitch(this, this._processExpression, params),
            this.expressionRegex);
      result = new String(result);
      result._safetext = true;
      return result;
   },

   _makeSafe : function(unknownValue) {

      switch (typeof unknownValue) {

         // numbers are literals - they are always safe
         case 'number': return unknownValue;

         // booleans (raw type) are also safe
         case 'boolean': return unknownValue;

         // string literals are not safe
         case 'string': return this.escape(unknownValue);

         case 'object':
            if (unknownValue instanceof String) {
               if (unknownValue._safetext) {
                  // already safe
                  return unknownValue;
               }
               return this.escape(unknownValue);

            }
            else if (unknownValue instanceof Date ||
                     unknownValue instanceof Boolean ||
                     unknownValue instanceof Number) {
               // These basic wrapper objects are always safe
               return unknownValue;
            }
            else if (unknownValue instanceof Array ||
                     unknownValue instanceof Function ||
                     unknownValue instanceof RegExp) {

               // These are probably safe, but certainly unexpected
               return finder.ftabs.text.__empty;
            }
            else {
               // some other kind of object.
               return finder.ftabs.text.__empty;
            }

         // functions should not be passed in here, but if we do see
         // one, its value should be nothing.
         case 'function': return finder.ftabs.text.__empty;

         // undefined = our safe empty string.
         case 'undefined': return finder.ftabs.text.__empty;
      }
   },

   escape : function(raw) {
      var replaced = new String(finder.ftabs.util.escape(raw));
      replaced._safetext = true;
      return replaced;
   },

   getRaw : function(code) {
      if (!this.isLoaded()) {
         return null;
      }
      if (dojo.isArray(code)) {
         code = code.join(".");
      }
      return this.data[code];
   },

   formatList : function(list, style) {

      if (!style) {
         style = "basic";
      }

      if (!list || list.length == 0) {
         return "";
      }

      if (list.length == 1) {
         return list[0];
      }

      if (list.length == 2) {
         var sepTwo = this.getRaw([ "listSeparator", style, "two"]);
         if (sepTwo) {
            return list.join(sepTwo);
         }
      }
      var sepFinal = this.getRaw(["listSeparator", style, "final"]);
      if (sepFinal && list.length == 2) {
         return list.join(sepFinal);
      }
      var sep = this.getRaw(["listSeparator", style]);
      if (sepFinal) {
         return list.slice(0, list.length - 1).join(sep) + sepFinal + list[list.length - 1];
      }
      else {
         return list.join(sep);
      }
   },

   _doLoad : function() {
      // Check Config properties
      dojo.xhrGet({
         url : "textData.json",
         handleAs : "json-comment-optional",
         load : finder.ftabs.errors.wrapLoader(this, this._loadingComplete)
      });
   },

   _onLoaded: function(data) {
      this.data = data;
      this.inherited(arguments);
   },

   setWhenLoaded : function(container, code, params) {
      this.afterLoad(this, this._setWithText, container, code, params);
   },

   _setWithText : function(container, code, params) {
      if (container instanceof String) {
         container = dojo.byId(container);
      }

      container.innerHtml = this.get(code, params);
   },

   _processExpression : function(params, _, expr) {
      var submessageMatches = expr.match(this.submessageRegex);
      if (submessageMatches) {
         // evaluate the expression inside the m[]
         var code = this._processExpression(params, _, submessageMatches[1]);
         // use that result as a message code, and get that message
         return this.get(code, params);
      }

      var stringConstantMatches = expr.match(this.stringConstantRegex);
      if (stringConstantMatches) {
         // either first or second group is valid, not both.
         return stringConstantMatches[1] || stringConstantMatches[2];
      }

      var result = params;
      if (params === undefined) {
         console.log("text: could not find param for expression " + expr + " as no parameters were supplied");
         return finder.ftabs.text.__empty;
      }
      
      var names = expr.split(".");
      for (var i = 0; i < names.length; i++) {
         if (i == names.length - 1) {
            // last name
            var lastName = names[i];

            // special case for date formatting
            var formatMatches = lastName.match(this.dateFormatRegexp);
            if (formatMatches) {
               if (formatMatches[1] == "date") {
                  return dojo.date.locale.format(result, {
                     selector : "date",
                     datePattern : (formatMatches[2] || formatMatches[3])
                  });
               }
               else {
                  return dojo.date.locale.format(result, {
                     selector : "time",
                     timePattern : (formatMatches[2] || formatMatches[3])
                  });
               }
            }

            // special case for telephone number formatting.
            formatMatches = lastName.match(this.telFormatRegexp);
            if (formatMatches) {
               return this.formatTel(result);
            }
         }

         result = result[names[i]];
         if (result == null) {
            if (i == 0) {
               console.log("text: could not find param " + names[i]);
            }
            else {
               console.log("text: could not find property " + names[i] + " of object " + names[i - 1]);
            }
            return finder.ftabs.text.__empty;
         }
      }

      return this._makeSafe(result);
   },

   /**
    * Format the given telephone number for display, using the given format string.
    * @param tel Raw telephone number
    * @param formatString Display string where 'n|N' represents numbers i.e. '(Nnn) nnn-nnnn'
    */
   simpleFormatTel : function(tel, formatString) {
      if (!tel || tel.length == 0) {
         return "";
      }
      var ret = "";
      var j = 0; // telArray index
      for (var i = 0; i < formatString.length; i++) {
         var theChar = formatString.charAt(i);
         if (theChar === "N" || theChar === "n") {
            theChar = tel.charAt(j++);
         }
         ret = ret.concat(theChar);
      }
      return this._makeSafe(ret);
   },

   /**
    * Format the given phone number, using the text strings for default formats for numbers of that length.
    * We assume standard text strings exist of the format:
    *    phoneFormat.<length>
    * where <length> is the length of the phone number. Some systems may support numbers of different lengths,
    * which would have different formats.
    * @param tel the phone number.
    */
   formatTel : function(tel) {
      return this.simpleFormatTel(tel, this.getRaw(["phoneFormat", tel.length]));

   }
});

finder.ftabs.text.__empty = new String("");
finder.ftabs.text.__empty._safetext = true;

finder.ftabs.text.instance = new finder.ftabs.text.TextGetter();

finder.ftabs.text.get = function(code, params) {
   return finder.ftabs.text.instance.get(code, params);
};

finder.ftabs.text.template = function(template, params) {
   return finder.ftabs.text.instance.template(template, params);
};

finder.ftabs.text.use = function(container, code, params) {
   finder.ftabs.text.instance.setWhenLoaded(container, code, params);
};

finder.ftabs.text.formatList = function(list, style) {
   return finder.ftabs.text.instance.formatList(list, style);
};

finder.ftabs.text.simpleFormatTel = function(tel, formatString) {
   return finder.ftabs.text.instance.simpleFormatTel(tel, formatString);
};

finder.ftabs.text.formatTel = function(tel) {
   return finder.ftabs.text.instance.formatTel(tel);
};






