(function($) {
  
  $.fn.input_validation = function() {
    $(this).each( function() {
      new $.input_validation(this);
    });
  }
  
  
  
  $.input_validation = function(t) {
    

    // Variables for our input's rules
    var rules = {
                  type: "String",
                  
                  // String type rules
                  max_words: null,
                  max_lines: null,
                  max_chars: null,
                  chars_per_line: null,
                  char_count_offset: null,
                  // Numeric type rules
                  min_value: null,
                  max_value: null,
                  allow_negative: false,
                  allow_floating_point: false,
                  
                  // Feedback rules
                  word_count: 0,
                  char_count: 0,
                  line_count: 0,
                  
                  manual_hint: null
                };
    
    // Class vars
    var target = $(t);
    var self = this;
    var autosave_timer = null;
    var has_focus = false;
    var rules_label = target.parent().parent().find("label.rules").get(0) || null;
    if (rules_label)
      rules_label = $(rules_label)
    var desc_label = target.parent().parent().find("label.desc").get(0) || null;
    if (desc_label)
      desc_label = $(desc_label)


    init();
    

    function init()
    {
      // Figure out our rules
      if (rules_label)
      {
        $.input_validation.Process_RulesString(rules_label.text(), rules);
        rules_label.remove();
        rules_label == null;
      }
      
      // Bind Events
      target.keypress(onEvent_KeyUp);
      target.keyup(onEvent_KeyUp);
      target.change(onEvent_Change);
      target.blur(onEvent_Blur);
      target.focus(onEvent_Focus);
      
      // Initial Update
      EnforceValue(target.val(), false, true);
    }
    

    
    function onEvent_Focus()
    {
      has_focus = true;
    }
    
    function onEvent_Blur()
    {
      has_focus = false;
    }
    
    
    
    
    function EnforceValue(value, isFinal, isInitial)
    {
      if (!target || !target.get(0))
        return;
        
      if (isFinal == null)
        isFinal = false;
      if (isInitial == null)
        isInitial = false;
        
      var data = { errors: [], result: value};
      
      // Reset feedback
      rules.word_count = 0;
      rules.char_count = 0;
      rules.line_count = 1;
      

        
      if (value != "")
      { 
        if (rules.type == "String")
        {
          data = $.input_validation.StringEnforce(value, rules);
        }
        else if (rules.type == "Number" || rules.type == "Currency")
        {
          data = $.input_validation.NumberEnforce(value, rules, isFinal);
        }
        
        
        // Only worry about cursor position if we have focus
        if (has_focus)
        {
          var selStart = target.get(0).selectionStart || 0;
          var selEnd = target.get(0).selectionEnd || 0;
          var scrollTop = target.get(0).scrollTop;

          // We added a sign to our data so account on that offset
          if (data.offset_sign)
          {
            selStart += 1;
            selEnd += 1;
          }
          
          // Update new value
          target.val(data.result);

          target.get(0).selectionStart = selStart;
          target.get(0).selectionEnd = selEnd;
          target.get(0).scrollTop = scrollTop;
        }

        
        if (data.errors.length > 0)
        {
          
          if (isInitial)
          {
            setTimeout(function() { target.change(); }, 500);
          }
        }
      }
      
      // Finally update our help text
      $.input_validation.HelpText(desc_label, rules, data.errors);
    }




    function onEvent_Change(e)
    {
      // Clear any pending Auto-Save
      if (autosave_timer != null)
        clearTimeout(autosave_timer);
      autosave_timer = null;
        
        
      EnforceValue(target.val(), true);
      
      return true;
    }



    function onEvent_KeyUp(e)
    {
      // Start Auto-Save Timer on valid key
      if (!e || !e.metaKey || (e.keyCode == 0 || e.keyCode == 13))
      {
        if (autosave_timer != null)
          clearTimeout(autosave_timer);
        autosave_timer = setTimeout(onTimer_AutoSave, 5000);
      }
      
      EnforceValue(target.val());
    }



    function onTimer_AutoSave()
    {
      autosave_timer = null;
      target.change();
    }
    
  }
  
  
  $.input_validation.Process_RulesString = function(rule_string, rules)
  {
    var i, val;
    flags = rule_string.match(/^(.*?)(?:\[|$)/)[1].split('|');        
    rulz = rule_string.match(/\[(.*?)\]/g);

    for (i=0; i<flags.length; i++)
    {
      var flag = flags[i];

      if (flag == "#")
      {
        rules.type = "Number";
      }
      else if (flag == "$")
      {
        rules.type = "Currency";
      }
      else if (flag == "url")
      {
        rules.type = "URL";
      }
      else if (flag == ".")
      {
        rules.allow_floating_point = true;
      }
      else if (flag == "-")
      {
        rules.allow_negative = true;
      }
      else if (flag == "phone")
      {
        rules.type = "Phone";
      }
    }
    
    // No Rules? So, just stick with the default
    if (rulz == null)
      return;
    
    for (i=0; i<rulz.length; i++)
    {
      contents = rulz[i].match(/\[(.*?)\]/)[1];
      parts = contents.split('|');
      
      tag = parts[0];
      val = parts[1] || parts[0];
            
      if (tag == "wl")
      {
        val = Number(val);
        if (val > 0)
          rules.max_words = val;
      }
      else if (tag == "ll")
      {
        rules.max_lines = Number(val);
      }
      else if (tag == "cl")
      {
        rules.max_chars = Number(val);
      }
      
      else if (tag == "cpl")
      {
        rules.chars_per_line = Number(val);
      }
      else if (tag == "cco")
      {
        rules.char_count_offset = Number(val);
      }
      
      
      else if (tag == "<=")
      {
        rules.max_value = Number(val);
      }
      else if (tag == ">=")
      {
        rules.min_value = Number(val);
      }
      
      else if (tag == "hint")
      {
        rules.manual_hint = val;
      }

    }
    
    
    
    if (rules.chars_per_line != null  &&  rules.max_lines != null)
    {
      rules.max_chars = rules.chars_per_line * rules.max_lines;
      if (rules.char_count_offset != null)
        rules.max_chars -= rules.char_count_offset;
    }
  }
  
  
  $.input_validation.ErrorExists = function(type, errors)
  {
    for (var i=0; i<errors.length; i++)
    {
      if (errors[i]['type'] == type)
        return true;
    }
    return false;
  }
  $.input_validation.HelpText = function(desc_label, rules, errors)
  {
    if (errors == null)
      errors = [];
      
    if (!desc_label)
      return;

      
    desc_label.html("");

    if (rules.manual_hint != null)
    {
      desc_label.append("<span class=\"type\">"+rules.manual_hint+"</span>");
    }
    else
    {
      if (rules.type == "Currency")
      {
        desc_label.append("<span class=\"type\">Currency USD</span>");
      }
      else if (rules.type == "Number")
      {      
        desc_label.append("<span class=\"type\">Number</span>");
      }
      else if (rules.type == "Phone")
      {
        desc_label.append("<span class=\"type\">Phone - ex. (555) 555-5555</span>");
      }
      else if (rules.type == "URL")
      {
        desc_label.append("<span class=\"type\">URL</span>");
      }
    }
    


    if ($.input_validation.ErrorExists("check_type", errors))
      desc_label.find(".type").effect('highlight', {color: 'red'});
    
    
    // Max Words?
    if (rules.max_words != null)
    {
      desc_label.append("<span class=\"range\">" + rules.word_count + " of " + rules.max_words + " words used" + "</span>");
    } 
    // Max Lines
    if (rules.max_lines != null)
    {
      desc_label.append("<span class=\"range\">" + rules.line_count + " of " + rules.max_lines + " lines used" + "</span>");
    }
    // Max Characters?
    if (rules.max_chars != null)
    {
      desc_label.append("<span class=\"range\">" + rules.char_count + " of " + rules.max_chars + " characters used" + "</span>");
    }
    
    
    // Max Characters?
    if (rules.min_value != null || rules.max_value != null)
    {
      desc_label.html("");
      if (rules.min_value != null && rules.max_value != null)
        desc_label.append("<span class=\"range\">Enter Number " + rules.min_value + " - " + rules.max_value + "</span>");
      else if (rules.min_value != null && rules.max_value == null)
        desc_label.append("<span class=\"range\">Enter Number Less than or equal to " + rules.min_value + "</span>");
      else if (rules.min_value == null && rules.max_value != null)
        desc_label.append("<span class=\"range\">Enter Number Greater than or equal to " + rules.max_value + "</span>");
    }
    
    if ($.input_validation.ErrorExists("range", errors) || (rules.type == "Number" && $.input_validation.ErrorExists("check_type", errors)))
      desc_label.find(".range").effect('highlight', {color: 'red'});

  }
  
  
  
  
  
  
  
  //  FinalizeNumber
  //
  //  Check our content for correctiveness
  ////
  $.input_validation.NumberEnforce = function(blob, rules, isFinal)
  {
    if (isFinal == null)
      isFinal = false;
      
    // Enforce our blob as a string
    if (blob == null)
      blob = "";
    blob = String(blob);
    
    
    var added_sign = false;
    var _errors = [];
    var res = "";
    var ch;
    var bFoundFloatingPoint = false;
    var bFoundValueSign = false;
    // Go through each character
    for (var ci=0; ci<blob.length; ci++)
    {
      ch = blob.substr(ci, 1);
      
      // First Index Checks
      if (ci == 0)
      {
        // Currency enforces the dollar sign
        if (rules.type == "Currency")
        {
          // if (ch != "$")
            // res += "$";
          // continue;
        }
        
        // Check for negative sign
        if (rules.allow_negative && ch == "-")
        {
          res += ch;
          continue;
        }
      }

      // Need to insert a 0 before our first floating point seperator
      if (ci == 1)
      {
        if (res == "-" && ch == ".")
          res += "0";
      }
      
      // Numbers Only
      if ((ch >= '0' && ch <= '9'))
      {
        res += ch;
      }
      // Found a period seperator?
      else if (ch == '.')
      {
        if (!rules.allow_floating_point || bFoundFloatingPoint)
        {
          _errors.push({type: "check_type"});
          break;
        }
        else
        {
          res += ch;
          bFoundFloatingPoint = true;
        }
      }
      else
      {
        if (ch == "$" && rules.type == "Currency")
          continue;
        
        _errors.push({type: "check_type"});
      }
    }
    
    if (isFinal)
      res = isNaN(res) ? 0 : Number(res);

    var res_val = isNaN(res) ? 0 : Number(res);

    if (rules.min_value != null  &&  res_val < rules.min_value)
    {
      res = rules.min_value;
      _errors.push({type: "range"});
    }
    else if (rules.max_value != null  &&  res_val > rules.max_value)
    {
      res = rules.max_value;
      _errors.push({type: "range"});
    }
    
    res = String(res);
    
    if (!isFinal)
    {
      if (Number(res) == Number(blob))
        res = blob;
    }
    

    if (rules.type == "Currency")
    {
      if (res.substr(0,1) != "$")
      {
        res = "$" + res;
        added_sign = true;
      }
    }

    return {result: String(res), errors: _errors, offset_sign: added_sign};
  }
  
  
  
  
  
  $.input_validation.StringEnforce = function(txt, rules)
  {
    // Ensure non-null
    txt = txt || "";
    

    
    var _errors = [];
    var ch;
    var bInWord = false;
    
    var nCharOffset = rules.char_count_offset || 0;
    var nLastSpace = nCharOffset;
    var nStartOfLine = nCharOffset;
    var nCharCount = 0;
    var tmp;
    var tLength = txt.length;
        
    txt = txt.replace("’", "'");
    for (var ci=0; ci<tLength; ci++)
    {
      ch = txt.substr(ci, 1);

      nCharCount++;
      nCharOffset++;

      if (ch == " ")
        nLastSpace = nCharOffset;
      
      if (ch == "\n")
      {
        if (rules.chars_per_line != null)
        {
          tmp = rules.chars_per_line - (nCharOffset - nStartOfLine);
          nCharCount += tmp;
          nCharOffset += tmp;
        } 
        
        nStartOfLine += rules.chars_per_line;
        nLastSpace = nStartOfLine;// = nCharOffset;
        rules.line_count++;
      }
      
      if (ch == "\n")
        nLastSpace = nStartOfLine = nCharOffset;
      
      if (ch != "\n")
      {
        if (rules.chars_per_line != null  &&  (nCharOffset - nStartOfLine > rules.chars_per_line))
        {
          //console.info("NEW LINE CO("+nCharOffset+") SOL("+nStartOfLine+") LS("+nLastSpace+")");
          
          // if (nCharOffset - nLastSpace > rules.chars_per_line)
            // nLastSpace = nStartOfLine + rules.chars_per_line;

          tmp = rules.chars_per_line - (nLastSpace - nStartOfLine);
          
          nCharCount += tmp;
          nCharOffset += tmp;
          nStartOfLine += rules.chars_per_line;
          //nLastSpace = nLastSpace ;
          rules.line_count++;
        }
        
        if (ch == " ")
        {
          bInWord = false;
          // Max Words?
          if (rules.max_words != null && rules.word_count >= rules.max_words)
          {
            _errors.push({type: "range"});
            break;
          }
        }
        else
        {
          if (!bInWord)
          {
            bInWord = true;
            rules.word_count++;
          }
        }
      }
        
        
      // Max Lines
      if (rules.max_lines != null && rules.line_count > rules.max_lines)
      {
        // if (_errors.length < 1)
        rules.line_count = rules.max_lines;
          _errors.push({type: "range"});
      }
      // Max Characters?
      if (rules.max_chars != null && nCharCount > rules.max_chars)
      {
        // if (_errors.length < 1)
        nCharCount = rules.max_chars;
          _errors.push({type: "range"});
      }      
      
      if (_errors.length > 0)
        break;
    }
    
    rules.char_count = nCharCount;
    

    return {result: txt.substr(0, ci), errors: _errors};
  }
  

})(jQuery);