/*
 *    tcFormChecker
 *    Объект - одиночка для проверки формы по описателю проверок.
 */

tcFormChecker =
{
 /* Проверка формы по описателю полей.
  * Аргументы:
  *   form - ссылка на объект - форму (наиболее вероятно, берется из this в обработчике onsubmit формы)
  *   inputs - описатель полей формы
  *   Возвращает true, если форма прошла все проверки и готова к отправке, иначе false.
  *   Производит визуальное отображение сообщений об ошибках.
  */
  check: function ( form, inputs )
  {
    var all_constraints_apply = true;

    // Инициализируем интерпретатор выражений - очистим таблицу символов и загрузим символы формы
    tcConstraintExpressionInterpreter.clearSymbols();
    tcConstraintExpressionInterpreter.loadSymbols( this.generateFormSymbols( form, inputs ) );

    // Пройдемся по полям ввода
    for( var i in inputs )
    {
      var input = inputs[ i ];

      // Пройдемся по ограничениям каждого поля и проверим их
      for( var i in input.constraints )
      {
        var constraint = input.constraints[ i ];

        constraint.applies = this.constraintApplies( form, inputs, input, constraint.definition );
        
        all_constraints_apply = all_constraints_apply && constraint.applies;
      }
    }

    // Отобразим ограничения на поля ввода.
    this.visualizeConstraints( form, inputs );

    return all_constraints_apply;
  },

  /* Генерация таблицы символов формы */  
  generateFormSymbols : function( form, inputs )
  {
    var symbols = {};

    // Тупо пройдемся по полям формы и сгенерим для каждого value, length и count      
    for( var i in inputs )
    {
      var value = this.getInputValue( form, inputs[ i ] );
      var count = 1;
      if( typeof( value ) == 'object' )
      {
       count = value.length;
       value = value.join( "\0" );
      }

      symbols[ 'form->' + inputs[i].name + '->value' ]  = value;  
      symbols[ 'form->' + inputs[i].name + '->count' ]  = count;
      symbols[ 'form->' + inputs[i].name + '->length' ] = value ? value.length : 0;
    }
    
    return symbols;
  },

 /* Визуализация ограничений полей формы. */
  visualizeConstraints: function( form, inputs )
  {
    // Пройдемся по полям ввода
    for( var i in inputs )
    {
      var input = inputs[ i ];
      var inputObject = this.getInputObject( form, input );

      // Массив сообщений о нарушениях ограничений для текущего поля
      var constraintMessages = new Array;

      // Пройдемся циклом по constraint-ам поля ввода
      for( var i in input.constraints )
      {
        var constraint = input.constraints[ i ];

        // Если текущее ограничение не выполнено, добавим текст его сообщения в массив
        if( ! constraint.applies )
        {
          constraintMessages[ constraintMessages.length ] = constraint.message;
        }
      }

      if( constraintMessages.length )
      {
        this.showConstraintMessages( form, input, constraintMessages );
      }
      else
      {
        this.hideConstraintMessages( form, input );
      }
    }
  },

 /* Отображение сообщений ограничений указанного поля ввода.
  *   form - форма в дереве документа
  *   input - описатель поля ввода
  *   messages - массив сообщений (строк)
  * Не возвращает ничего.
  */
  showConstraintMessages : function( form, input, messages )
  {
    // Получим объект поля в дереве документа
    var inputObject = this.getInputObject( form, input );
    if( ! inputObject ) return; 

    // Спрячем сообщения, чтобы не сдублировать их
    this.hideConstraintMessages( form, input );
    
    // Получим координаты поля ввода
    pos = this.getElementPosition( inputObject );

    var div = document.createElement('div');
    div.id = inputObject.name+'_e';

    div.style.left = pos.left + inputObject.offsetWidth;
    div.style.top = pos.top - 12;
    div.innerHTML = '<p>'+messages.join('<br />');+'</p><!--[if lte IE 6.5]><iframe></iframe><![endif]-->';
    div.className = 'select-free error';

    document.body.appendChild(div);
    
/*
    // Для сообщений сделаем отдельный DIV, в него положим текст и вставим этот div непосредственно после поля ввода
    // Вполне возможно, что для checkbox-а такой метод неприемлем, но кому нужно проверять checkbox-ы? ^^
    var msgs = document.createElement( 'DIV' );
    msgs.className = 'constraint-message';
    msgs.innerHTML = messages.join( '<br />' );
    if( inputObject.nextSibling )
    {
      inputObject.parentNode.insertBefore( msgs, inputObject.nextSibling );
    }
    else
    {
      inputObject.parentNode.appendChild( msgs );
    }
*/    
  },

 /* Сокрытие сообщений ограничений указанного поля ввода.
  *   form - форма в дереве документа
  *   input - описатель поля ввода
  *   messages - массив сообщений (строк)
  * Не возвращает ничего.
  */
  hideConstraintMessages : function( form, input )
  {
    // Получим объект поля в дереве документа
    var inputObject = this.getInputObject( form, input );

    if( ! inputObject ) return; 

    var i = document.getElementById(inputObject.name+'_e');
    
    if(i)
    {
        document.body.removeChild( i );
    }
/*    if( inputObject.nextSibling )
    {
      if( inputObject.nextSibling.className == 'constraint-message' )
      {
        inputObject.parentNode.removeChild( inputObject.nextSibling );
      }
    }
*/    
  },
  
 /* Проверка ограничений на поле.
  * Аргументы:
  *   form - форма в дереве документа
  *   input - описание поля ввода
  *   constraint - описание ограничения, которое нужно проверить
  * Возвращает true, если ограничения выполняется иначе — false.
  */
  constraintApplies : function( form, inputs, input, constraint )
  {
    /*
      Лирическое отступление.
      Все constraint-ы состоят из одного и более блока проверки
      и текстового сообщения, которое выводится при нарушении constraint-а.
      Каждый блок проверки имеет идентификатор типа (на данный момент
      их три - match, notmatch и expression) и, собственно, описание блока.
      Описание блока проверки - обычный текст, описывающий собственно ограничение.
      Описание блока проверки может быть вычислено только в контексте типа блока.
    */
    
    // Ограничение выполняется, пока не доказано обратного.
    var applies = true;

    // Пройдемся по блокам проверки и вычислим их
    for( var i in constraint )
    {
      applies = applies && this.constraintBlockApplies( form, inputs, input, constraint[i] );
    }

    return applies; 
  },
  
 /* Проверка блока ограничения на поле.
  * Аргументы:
  *   form - форма в дереве документа
  *   input - описатель поля
  *   block - описатель блока ограничения
  * Возвращает true, если блок ограничения выполняется, иначе false.
  */
  constraintBlockApplies : function( form, inputs, input, block )
  {
    switch( block.type )
    {
      case 'match':
        return this.constraintMatch( form, inputs, input, block );
        break;
      
      case 'notmatch':
        return this.constraintNotMatch( form, inputs, input, block );
        break;

      case 'expression':
        return this.constraintExpression( form, inputs, input, block );
        break;
    }
    return false;
  },
  
 /* Проверка блока ограничения типа match.
  * Аргументы:
  *   form - форма
  *   input - описатель поля ввода
  *   block - описатель блока
  * Возвращает true, если блок выполняется, иначе false.
  */
  constraintMatch : function( form, inputs, input, block )
  {
    // Получим значение поля и тупо проверим его регулярным выражением :]
    var value = this.getInputValue( form, input );
    // Для проверки слепим поля multiselect-а через \0, как это принято в HTTP
    if( typeof( value ) == 'object' ) value = value.join( "\0" );
    
    // Создадим регулярное выражение
    var regexp = new RegExp( block.text );
    
    return regexp.test( value );
  },
  
 /* Проверка блока ограничения типа notmatch.
  * Аргументы:
  *   form - форма
  *   input - описатель поля ввода
  *   block - описатель блока
  * Возвращает true, если блок выполняется, иначе false.
  */
  constraintNotMatch : function( form, inputs, input, block )
  {
    // Получим значение поля и тупо проверим его регулярным выражением :]
    var value = this.getInputValue( form, input );
    // Для проверки слепим поля multiselect-а через \0, как это принято в HTTP
    if( typeof( value ) == 'object' ) value = value.join( "\0" );
    
    // Создадим регулярное выражение
    var regexp = new RegExp( block.text );
    
    return regexp.test( value ) ? false : true;
  },

 /* Проверка блока ограничения типа expression.
  * Аргументы:
  *   form - форма
  *   input - описатель поля ввода
  *   block - описатель блока
  * Возвращает true, если блок выполняется, иначе false.
  */
  constraintExpression : function( form, inputs, input, block )
  {
    // Установим в интерпретаторе псевдоним this для текущего поля
    tcConstraintExpressionInterpreter.setSymbolAlias( 'this->value', 'form->' + input.name + '->value' ); 
    tcConstraintExpressionInterpreter.setSymbolAlias( 'this->length', 'form->' + input.name + '->length' ); 
    tcConstraintExpressionInterpreter.setSymbolAlias( 'this->count', 'form->' + input.name + '->count' ); 
  
    return tcConstraintExpressionInterpreter.eval( block.text );
  },

 /* Получение значения поля.
  * Аргументы
  *   form - форма
  *   input - описатель поля ввода
  * Возвращает значение поля. Для поля multiselect возвращает массив значений
  */
  getInputValue : function( form, input )
  {
    var inputObject = this.getInputObject( form, input );
    
    if( ! inputObject ) return '';
    
    switch( input.type )
    {
      case 'text':
      case 'password':
      case 'textarea':
      case 'hidden':
        return inputObject.value;
        break;
        
      case 'checkbox':
        return inputObject.checked ? inputObject.value : false; 
        break;
    
      case 'select':
        return inputObject.options[ inputObject.selectedIndex ].value; 
        break;
        
      case 'multiselect':
        var selectedItems = new Array;
        
        for( var i in inputObject.options )
        {
          if( inputObject.options[i].selected )
          {
            selectedItems[ selectedItems.length ] = inputObject.options[i].value;
          }
        }
        
        return selectedItems;
        break;
    }  
  },
  
 /* Получение поля в дереве документа по описателю и форме.
  * Аргументы:
  *   form - форма в дереве документа
  *   input - описатель поля ввода
  * Возвращает ссылку на поле в дереве документа или null, если таковой не найден.
  */
  getInputObject : function( form, input )
  {
    // Воизбежание хуйни проверим наличие имени у поля ввода
    if( ! input.name ) return null;
  
    var object = form[ input.name ];
    
    return object ? object : null;
  },
  
   /* Получение координат элемента.
    * Аргументы:
    *   element - элемент документа, координаты которого нужно получить.
    * Возвращает ссылку на объект с полями left и top, содержащими вычисленные значения.  
    */  
    getElementPosition : function( element ) 
    {
        var curleft = curtop = 0;
        if( element.offsetParent )
        {
            curleft = element.offsetLeft;
            curtop = element.offsetTop;
            while( element = element.offsetParent ) 
            {
                curleft += element.offsetLeft;
                curtop += element.offsetTop;
            }
        }
        return { left:curleft, top:curtop };
    }  
};

/*
 *    tcConstraintExpressionInterpreter
 *    Объект - одиночка для интерпретации constraint expression.
 */
  
  
tcConstraintExpressionInterpreter = 
{
  symbols : {},
  aliases : {},

 /* Очистка таблиц символов и псевдонимов. */
  clearSymbols : function()
  {
    this.symbols = {};
    this.aliases = {};
  },
  
  /* Загрузка символов, текущее содержимое таблицы символов дополняется переданными */
  loadSymbols : function( symbols )
  {
    for( var i in symbols ) this.symbols[ i ] = symbols[ i ]; 
  },

  /* Установка псевдонима. */
  setSymbolAlias : function( alias, symbol )
  {
    if( typeof( this.aliases[ symbol ] ) != 'undefined' || alias == symbol ) return false;
    this.aliases[ alias ] = symbol;
  },  

 /* Вычисление значения символа.
  * Возвращает значение символа или null, если символ не найден.
  */
  evalSymbol : function( code )
  {
    // Попытаемся найти символ в таблице символов
    if( typeof( this.symbols[ code ] ) != 'undefined' ) return this.symbols[ code ];
    // Теперь попытаемся посмотреть по псевдонимам
    if( typeof( this.aliases[ code ] ) != 'undefined' ) return this.evalSymbol( this.aliases[ code ] );

    return null;
  },
  
 /* Выполнение кода */
  eval: function( code )
  { 
    /* Для начала удостоверимся, что code - строка */
    code = new String( code );
    
    /* При желании здесь можно сделать alert или throw 'Синтаксическая ошибка'... x) */
    return this.logical_or_expr( code );
  },
  
  logical_or_expr: function( code )
  {
      if(code.match( /^\s*([^|]+?)\s+(\&\&)\s+(([\w\->_.]+)\s+([<!=>]=|[<>])\s+([\w\->_.]+))\s*$/i ) || code.match( /^\s*([\w\->_.]+)\s+([<!=>]=|[<>])\s+([\w\->_.]+)\s*$/i ))
      {
        return this.logical_and_expr( code );
      }
      else if(code.match( /^\s*(.+?)\s+(\|\|)\s+([^|]+)\s*$/ ))
      {
        var l = RegExp.$1;
        var r = RegExp.$3;
        return ( this.logical_or_expr(l) || this.logical_and_expr(r) );
      }
      else
      {
        return null;
      }
  },

  logical_and_expr: function( code )
  {

      if(code.match( /^\s*([\w\->_.]+)\s+([<!=>]=|[<>])\s+([\w\->_.]+)\s*$/i ))
      {
        return this.simple_expr( code );
      }
      else if(code.match( /^\s*(.+?)\s+(\&\&)\s+(([\w\->_.]+)\s+([<!=>]=|[<>])\s+([\w\->_.]+))\s*$/i ))
      {
        var l = RegExp.$1;
        var r = RegExp.$3;      
        return ( this.logical_and_expr(l) && this.simple_expr(r) ); 
      }
      else
      {
        return null;
      }
  },

  simple_expr: function( code )
  {
      if(code.match( /^\s*([\w\->_.]+)\s+([<!=>]=|[<>])\s+([\w\->_.]+)\s*$/i ))
      {
        var leftExpr = RegExp.$1;
        var op = RegExp.$2;
        var rightExpr = RegExp.$3;
    
        /* Вычислим значения операндов */
        leftExpr = this.codeExpr( leftExpr );
        rightExpr = this.codeExpr( rightExpr );
        
        /* Собственно, вычислим выражение */    
        switch( op )
        {
          case '==':  return leftExpr == rightExpr;
          case '!=':  return leftExpr != rightExpr;
          case '>=':  return leftExpr >= rightExpr;
          case '<=':  return leftExpr <= rightExpr;
          case '>':   return leftExpr >  rightExpr;
          case '<':   return leftExpr <  rightExpr;
        } 
      }
      else
      {
        return null;
      }
  },
  
  
  /* Вычисление значения выражения */
  codeExpr : function( code )
  {
    if( code.match( /^\d+$/ ) ) return code;
    return this.evalSymbol( code );
  }
};

/*
tcConstraintExpressionInterpreter = 
{
  symbols : {},
  aliases : {},

 /* Очистка таблиц символов и псевдонимов. * /
  clearSymbols : function()
  {
    this.symbols = {};
    this.aliases = {};
  },
  
  /* Загрузка символов, текущее содержимое таблицы символов дополняется переданными * /
  loadSymbols : function( symbols )
  {
    for( var i in symbols ) this.symbols[ i ] = symbols[ i ]; 
  },

  /* Установка псевдонима. * /
  setSymbolAlias : function( alias, symbol )
  {
    if( typeof( this.aliases[ symbol ] ) != 'undefined' || alias == symbol ) return false;
    this.aliases[ alias ] = symbol;
  },  

 /* Вычисление значения символа.
  * Возвращает значение символа или null, если символ не найден.
  * /
  evalSymbol : function( code )
  {
    // Попытаемся найти символ в таблице символов
    if( typeof( this.symbols[ code ] ) != 'undefined' ) return this.symbols[ code ];
    // Теперь попытаемся посмотреть по псевдонимам
    if( typeof( this.aliases[ code ] ) != 'undefined' ) return this.evalSymbol( this.aliases[ code ] );

    return null;
  },
  
 /* Выполнение кода * /
  eval: function( code )
  {
    /* Для начала удостоверимся, что code - строка * /
    code = new String( code );
    
    /* Запускаем Ебанический Синтаксический Анализатор! * /
    code.match( /^\s*([\w\->_]+)\s+([<!=>]=|[<>])\s+([\w\->_]+)\s*$/i );
    
    var leftExpr = RegExp.$1;
    var op = RegExp.$2;
    var rightExpr = RegExp.$3;
    /* Синтаксический анализ закончен xD * /

    if( typeof(leftExpr)!='undefined' && typeof(op)!='undefined' && typeof(rightExpr)!='undefined' )
    {
      /* Вычислим значения операндов * /
      leftExpr = this.codeExpr( leftExpr );
      rightExpr = this.codeExpr( rightExpr );
    
      /* Собственно, вычислим выражение * /    
      switch( op )
      {
        case '==':  return leftExpr == rightExpr;
        case '!=':  return leftExpr != rightExpr;
        case '>=':  return leftExpr >= rightExpr;
        case '<=':  return leftExpr <= rightExpr;
        case '>':   return leftExpr >  rightExpr;
        case '<':   return leftExpr <  rightExpr;
      }
    }
    
    /* При желании здесь можно сделать alert или throw 'Синтаксическая ошибка'... x) * /
    return false;
  },
  
  /* Вычисление значения выражения * /
  codeExpr : function( code )
  {
    if( code.match( /^\d+$/ ) ) return code;
    return this.evalSymbol( code );
  }
};
*/