/**
 * jintervals -- JavaScript library for interval formatting
 *
 * Copyright (c) 2009 Rene Saarsoo <http://code.google.com/p/jintervals/>
 *
 * jintervals is cross-licensed under MIT and LGPL.
 *
 * ====
 *
 * jintervals 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.
 *
 * jintervals 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 jintervals. If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * ====
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
var jintervals = (function() {
  function jintervals(seconds, format) {
    return Interpreter.evaluate(new Time(seconds), Parser.parse(format));
  };

  /**
   * Parses format string into data structure,
   * that can be interpreted later.
   */
  var Parser = {
    parse: function(format) {
      var unparsed = format;
      var result = [];
      while ( unparsed.length > 0 ) {
        // leave plain text untouched
        var textMatch = /^([^\\{]+)([\\{].*|)$/.exec(unparsed);
        if (textMatch) {
          result.push(textMatch[1]);
          unparsed = textMatch[2];
        }

        // parse jintervals {Code} separately
        var match = /^([{].*?(?:[}]|$))(.*)$/i.exec(unparsed);
        if (match) {
          result.push(this.parseCode(match[1]));
          unparsed = match[2];
        }

        // backslash escapes next character
        // transform \{ --> {
        // transform \\ --> \
        if (unparsed.charAt(0) === "\\") {
          result.push(unparsed.charAt(1));
          unparsed = unparsed.slice(2);
        }
      }
      return result;
    },

    // parses single {Code} in format string
    // Returns object representing the code or false when incorrect format string
    parseCode: function(code) {
      var re = /^[{]([smhdg])([smhdg]*)?(ays?|ours?|inutes?|econds?|reatests?|\.)?(\?(.*))?[}]$/i;
      var matches = re.exec(code);
      if (!matches) {
        return false;
      }

      return {
        // single-letter uppercase name of the type
        type: matches[1].toUpperCase(),
        // when code begins with lowercase letter, then set showing limited amount to true
        limited: (matches[1].toLowerCase() == matches[1]),
        paddingLength: (matches[2] || "").length + 1,
        format: (matches[3]||"") == "" ? false : (matches[3] == "." ? "letter" : "full"),
        optional: !!matches[4],
        optionalSuffix: matches[5] || ""
      };
    }

  };

  /**
   * Evaluates parse tree in the context of given time object
   */
  var Interpreter = {
    evaluate: function(time, parseTree) {
      var smallestUnit = this.smallestUnit(parseTree);
      var result = "";
      while ( parseTree.length > 0 ) {
        var code = parseTree.shift();
        // leave plain text untouched
        if (typeof code === "string") {
          result += code;
        }

        // evaluate the code
        else if (typeof code === "object") {
          var unit = (code.type == "G") ? time.getGreatestUnit() : code.type;
          var smallest = (code.type == "G") ? unit : smallestUnit;
          var value = time.get(unit, code.limited, smallest);
          var suffix = code.format ? Localization.translate(code.format, unit, value) : "";

          // show when not optional or totalvalue is non-zero
          if (!code.optional || time.get(unit) != 0) {
            result += this.zeropad(value, code.paddingLength) + suffix + code.optionalSuffix;
          }
        }

        // otherwise we have error
        else {
          result += "?";
        }
      }
      return result;
    },

    /**
     * Finds the smallest unit from parse tree.
     *
     * For example when parse tree contains "d", "m", "h" then returns "m"
     */
    smallestUnit: function(parseTree) {
      var unitOrder = {
        "S": 0,
        "M": 1,
        "H": 2,
        "D": 3
      };

      var smallest = "D";
      for (var i = 0; i < parseTree.length; i++) {
        if (typeof parseTree[i] === "object") {
          var type = parseTree[i].type;
          if (type !== "G" && unitOrder[type] < unitOrder[smallest]) {
            smallest = type;
          }
        }
      }

      return smallest;
    },

    // utility function to pad number with leading zeros
    zeropad: function(nr, decimals) {
      var padLength = decimals - (""+nr).length;
      return (padLength > 0) ? this.repeat("0", padLength) + nr : nr;
    },

    // utility function to repeat string
    repeat: function(string, times) {
      var result = "";
      for (var i=0; i < times; i++) {
        result += string;
      }
      return result;
    }
  };

  /**
   * Time class that deals with the actual computation of time units.
   */
  var Time = function(s) {
    this.seconds = s;
  };
  Time.prototype = {
    nextUnit: {D: "H", H: "M", M: "S", S: "S"},

    /**
     * Returns the value of time in given unit
     *
     * @param {String} unit  Either "S", "M", "H" or "D"
     * @param {Boolean} limited  When true 67 seconds will become just 7 seconds (defaults to false)
     * @param {String} smallest  The name of smallest unit.  Defaults to next unit.
     * For example for "D" it will be "H", for "H" it will be "M" and so on...
     */
    get: function(unit, limited, smallest) {
      if (!this[unit]) {
        return "?";
      }
      smallest = smallest || this.nextUnit[unit];
      return this[unit](limited, smallest);
    },

    // functions for each unit

    S: function(limited, smallest) {
      return limited ? this.seconds - this.M(false, smallest) * 60 : this.seconds;
    },

    M: function(limited, smallest) {
      var minutes = this.seconds / 60;
      minutes = (smallest === "M") ? Math.round(minutes): Math.floor(minutes);
      if (limited) {
        minutes = minutes - this.H(false, smallest) * 60;
      }
      return minutes;
    },

    H: function(limited, smallest) {
      var hours = this.M(false, smallest) / 60;
      hours = (smallest === "H") ? Math.round(hours): Math.floor(hours);
      if (limited) {
        hours = hours - this.D(false, smallest) * 24;
      }
      return hours;
    },

    D: function(limited, smallest) {
      var days = this.H(false, smallest) / 24;
      return (smallest === "D") ? Math.round(days): Math.floor(days);
    },

    /**
     * Returns the name of greatest time unit.
     *
     * For example when we have 2 hours, 30 minutes, and 7 seconds,
     * then the greatest unit is hour and "H" is returned.
     */
    getGreatestUnit: function() {
      if (this.seconds < 60) {
        return "S";
      }
      else if (this.M(false, "M") < 60) {
        return "M";
      }
      else if (this.H(false, "H") < 24) {
        return "H";
      }
      else {
        return "D";
      }
    }
  };

  var Localization = {
    translate: function(format, lcType, value) {
      var loc = this.locales[this.currentLocale];
      var translation = loc[format][lcType];
      if (typeof translation === "string") {
        return translation;
      }
      else {
        return translation[loc.plural(value)];
      }
    },

    locale: function(loc) {
      if (loc) {
        this.currentLocale = loc;
      }
      return this.currentLocale;
    },

    currentLocale: "en_US",

    locales: {
      en_US: {
        letter: {
          D: "d",
          H: "h",
          M: "m",
          S: "s"
        },
        full: {
          D: [" day", " days"],
          H: [" hour", " hours"],
          M: [" minute", " minutes"],
          S: [" second", " seconds"]
        },
        plural: function(nr) {
          return (nr == 1) ? 0 : 1;
        }
      },
      et_EE: {
        letter: {
          D: "p",
          H: "h",
          M: "m",
          S: "s"
        },
        full: {
          D: [" p\u00E4ev", " p\u00E4eva"],
          H: [" tund", " tundi"],
          M: [" minut", " minutit"],
          S: [" sekund", " sekundit"]
        },
        plural: function(nr) {
          return (nr == 1) ? 0 : 1;
        }
      },
      lt_LT: {
        letter: {
          D: "d",
          H: "h",
          M: "m",
          S: "s"
        },
        full: {
          D: [" dieną", " dienas", " dienų"],
          H: [" valandą", " valandas", " valandų"],
          M: [" minutę", " minutes", " minučių"],
          S: [" sekundę", " sekundes", " sekundžų"]
        },
        plural: function(n) {
          return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);
        }
      },
      ru_RU: {
        letter: {
          D: "д",
          H: "ч",
          M: "м",
          S: "с"
        },
        full: {
          D: [" день", " дня", " дней"],
          H: [" час", " часа", " часов"],
          M: [" минута", " минуты", " минут"],
          S: [" секунда", " секунды", " секунд"]
        },
        plural: function(n) {
          return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
        }
      },
      uk_UA: {
        letter: {
          D: "д",
          H: "г",
          M: "х",
          S: "с"
        },
        full: {
          D: [" день", " дні", " днів"],
          H: [" година", " години", " годин"],
          M: [" хвилина", " хвилини", " хвилин"],
          S: [" секунда", " секунди", " секунд"]
        },
        plural: function(n) {
          return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
        }
      },
      fi_FI: {
        letter: {
          D: "p",
          H: "h",
          M: "m",
          S: "s"
        },
        full: {
          D: [" päivä", " päivää"],
          H: [" tunti", " tuntia"],
          M: [" minuutti", " minuuttia"],
          S: [" sekunti", " sekunttia"]
        },
        plural: function(nr) {
          return (nr == 1) ? 0 : 1;
        }
      }
    }
  };

  // Changing and getting current locale
  jintervals.locale = function(loc) {
    return Localization.locale(loc);
  };
  jintervals.Time = Time;

  return jintervals;
})();