/*
  Copyright (C) 2006 Helge Hess

  This file is part of JOPE.

  JOPE 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 2, or (at your option) any
  later version.

  JOPE 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 JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

package org.opengroupware.jope.eocontrol;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.foundation.NSObject;

public class EOQualifier extends NSObject {
  protected static final Log log = LogFactory.getLog("EOQualifier");
  
  /* parsing */
  
  public static EOQualifier qualifierWithQualifierFormat
    (String _fmt, Object... _args)
  {
    EOQualifierParser parser = new EOQualifierParser(_fmt.toCharArray(), _args);
    EOQualifier q = parser.parseQualifier();
    // TODO: check error
    parser.reset();
    return q;
  }
  public static EOQualifier parse(String _fmt, Object... _args) {
    EOQualifierParser parser = new EOQualifierParser(_fmt.toCharArray(), _args);
    EOQualifier q = parser.parseQualifier();
    // TODO: check error
    parser.reset();
    return q;
  }
  
  /* factory */
  
  public static EOQualifier qualifierToMatchAllValues(Map _values) {
    if (_values == null) return null;
    int size = _values.size();
    if (size == 0) return null;
    
    EOQualifier[] qs = new EOQualifier[size];
    for (Object key: _values.keySet()) {
      size--;
      qs[size] = new EOKeyValueQualifier((String)key, _values.get(key));
    }
    if (size == 1)
      return qs[0];
    return new EOAndQualifier(qs);
  }

  public static EOQualifier qualifierToMatchAnyValue(Map _values) {
    if (_values == null) return null;
    int size = _values.size();
    if (size == 0) return null;
    
    EOQualifier[] qs = new EOQualifier[size];
    for (Object key: _values.keySet()) {
      size--;
      qs[size] = new EOKeyValueQualifier((String)key, _values.get(key));
    }
    if (size == 1)
      return qs[0];
    return new EOOrQualifier(qs);
  }
  
  public static EOQualifier qualifierToMatchAnyValue(String _key, Object[] _vs){
    if (_vs == null || _vs.length == 0)
      return null;
    
    if (_vs.length == 1)
      return new EOKeyValueQualifier(_key, _vs[0]);
    
    EOQualifier[] qs = new EOQualifier[_vs.length];
    for (int i = 0; i < _vs.length; i++)
      qs[i] = new EOKeyValueQualifier(_key, _vs[i]);
    return new EOOrQualifier(qs);
  }
  
  /* keys */
  
  public List<String> allQualifierKeys() {
    Set<String> keys = new HashSet<String>(16);
    this.addQualifierKeysToSet(keys);
    return new ArrayList<String>(keys);
  }
  public void addQualifierKeysToSet(Set<String> _keys) {
  }
  
  /* bindings */

  public List<String> bindingKeys() {
    Set<String> keys = new HashSet<String>(16);
    this.addBindingKeysToSet(keys);
    return new ArrayList<String>(keys);
  }
  public void addBindingKeysToSet(Set<String> _keys) {
  }
  
  public String keyPathForBindingKey(String _variable) {
    return null;
  }
  
  public EOQualifier qualifierWithBindings(Object _vals, boolean _requiresAll) {
    return this;
  }
  
  /* utility */
  
  public List filterCollection(Collection _in) {
    if (_in == null)
      return null;
    
    EOQualifierEvaluation eval = (EOQualifierEvaluation)this;
    ArrayList<Object> result = new ArrayList<Object>(_in.size());
    for (Object item: _in) {
      if (eval.evaluateWithObject(item))
        result.add(item);
    }
    
    result.trimToSize();
    return result;
  }
  
  /* string representation */
  
  public boolean appendStringRepresentation(StringBuffer _sb) {
    return false;
  }
  
  public String stringRepresentation() {
    StringBuffer sb = new StringBuffer(256);
    this.appendStringRepresentation(sb);
    return sb.toString();
  }
  
  protected void appendIdentifierToStringRepresentation
    (StringBuffer _sb, String _id)
  {
    // TODO: should we surround ids by double-quotes like in SQL?
    _sb.append(_id);
  }
  
  protected boolean appendConstantToStringRepresentation
    (StringBuffer _sb, Object _o)
  {
    if (_o == null)
      _sb.append("NULL");
    else if (_o instanceof EOQualifierVariable) {
      _sb.append("$");
      _sb.append(((EOQualifierVariable)_o).key());
    }
    else if (_o instanceof Number)
      _sb.append(_o);
    else if (_o instanceof Boolean) {
      if (((Boolean)_o).booleanValue())
        _sb.append("true");
      else
        _sb.append("false");
    }
    else if (_o instanceof String) {
      String s = ((String)_o).replace("'", "\\'");
      _sb.append("'");
      _sb.append(s);
      _sb.append("'");
    }
    else {
      // TODO: log error
      this.appendConstantToStringRepresentation(_sb, _o.toString());
    }
    
    return true;
  }
  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
  }
  
  
  /* comparison interface */
  
  public static interface Comparison {
    
    public boolean isEqualTo(Object _other); 
    public boolean isNotEqualTo(Object _other); 
    public boolean isGreaterThan(Object _other);
    public boolean isGreaterThanOrEqualTo(Object _other);
    public boolean isLessThan(Object _other);
    public boolean isLessThanOrEqualTo(Object _other);

    public boolean doesContain(Object _other);
    public boolean doesLike(Object _other);
    public boolean doesCaseInsensitiveLike(Object _other);
  }
  
  
  /* comparison operations */
  
  public enum ComparisonOperation {
    UNKNOWN,
    EQUAL_TO,
    NOT_EQUAL_TO,
    GREATER_THAN,
    GREATER_THAN_OR_EQUAL,
    LESS_THAN,
    LESS_THAN_OR_EQUAL,
    CONTAINS,
    LIKE,
    CASE_INSENSITIVE_LIKE
  }
  
  public static ComparisonOperation operationForString(String _s) {
    if (_s == null) return ComparisonOperation.UNKNOWN;
    
    int len = _s.length();
    
    if (len == 1) {
      char c0 = _s.charAt(0);
      if (c0 == '=') return ComparisonOperation.EQUAL_TO;
      if (c0 == '>') return ComparisonOperation.GREATER_THAN;
      if (c0 == '<') return ComparisonOperation.LESS_THAN;
      return ComparisonOperation.UNKNOWN;
    }
    
    if (len == 2) {
      char c0 = _s.charAt(0);
      char c1 = _s.charAt(1);
      
      if (c0 == '!' && c1 == '=')
        return ComparisonOperation.NOT_EQUAL_TO;
      
      if ((c0 == '>' && c1 == '=') || (c0 == '=' && c1 == '>'))
        return ComparisonOperation.GREATER_THAN_OR_EQUAL;

      if ((c0 == '<' && c1 == '=') || (c0 == '=' && c1 == '<'))
        return ComparisonOperation.LESS_THAN_OR_EQUAL;

      if (c0 == '=' && c1 == '=')
        return ComparisonOperation.EQUAL_TO;

      if (c0 == 'I' && c1 == 'N')
        return ComparisonOperation.CONTAINS;
      
      return ComparisonOperation.UNKNOWN;
    }

    if (len == 4 && "like".compareToIgnoreCase(_s) == 0)
      return ComparisonOperation.LIKE;
    if ("caseInsensitiveLike:".compareToIgnoreCase(_s) == 0)
      return ComparisonOperation.CASE_INSENSITIVE_LIKE;
    if ("caseInsensitiveLike".compareToIgnoreCase(_s) == 0)
      return ComparisonOperation.CASE_INSENSITIVE_LIKE;
    
    return ComparisonOperation.UNKNOWN;
  }
  public static String stringForOperation(ComparisonOperation _op) {
    switch (_op) {
      case EQUAL_TO:              return "=";
      case NOT_EQUAL_TO:          return "!=";
      case GREATER_THAN:          return ">";
      case GREATER_THAN_OR_EQUAL: return ">=";
      case LESS_THAN:             return "<";
      case LESS_THAN_OR_EQUAL:    return "<=";
      case CONTAINS:              return "IN";
      case LIKE:                  return "LIKE";
      case CASE_INSENSITIVE_LIKE: return "caseInsensitiveLike:";
      default: return null;
    }
  }
  
  /* comparison support */
  
  protected static Map<Class, ComparisonSupport> classToSupport;
  protected static final ComparisonSupport       defaultSupport;
  
  /* Note: be careful not to retain classes which might be unloaded by the
   *       servlet container.
   */ 
  public static void setSupportForClass(ComparisonSupport _sup, Class _cls) {
    classToSupport.put(_cls, _sup);
  }
  public static ComparisonSupport supportForClass(Class _cls) {
    if (_cls == null) return defaultSupport;
    ComparisonSupport sup = classToSupport.get(_cls);
    return sup != null ? sup : defaultSupport;
  }
  
  // TODO: check for "Comparison" support?
  public static class ComparisonSupport {
    
    public boolean compareOperation
      (ComparisonOperation _op, Object _lhs, Object _rhs)
    {
      switch (_op) {
        case EQUAL_TO:              return this.isEqualTo(_lhs, _rhs);
        case NOT_EQUAL_TO:          return this.isNotEqualTo(_lhs, _rhs);
        case GREATER_THAN:          return this.isGreaterThan(_lhs, _rhs);
        case GREATER_THAN_OR_EQUAL:
          return this.isGreaterThanOrEqualTo(_lhs, _rhs);
        case LESS_THAN:             return this.isLessThan(_lhs, _rhs);
        case LESS_THAN_OR_EQUAL:    return this.isLessThanOrEqualTo(_lhs, _rhs);
        case CONTAINS:              return this.doesContain(_lhs, _rhs);
        case LIKE:                  return this.doesLike(_lhs, _rhs);
        case CASE_INSENSITIVE_LIKE:
          return this.doesCaseInsensitiveLike(_lhs, _rhs);
        default:
          return false;
      }
    }
    
    public boolean isEqualTo(Object _lhs, Object _rhs) {
      if (_lhs == _rhs) return true;
      if (_lhs == null || _rhs == null) return false;
      return _lhs.equals(_rhs);
    }
    
    public boolean isNotEqualTo(Object _lhs, Object _rhs) {
      if (_lhs == _rhs) return false;
      if (_lhs == null || _rhs == null) return true;
      return !this.isEqualTo(_lhs, _rhs);
    }
    
    @SuppressWarnings("unchecked")
    public boolean isGreaterThan(Object _lhs, Object _rhs) {
      if (_lhs == _rhs) return false;
      if (this.isEqualTo(_lhs, _rhs)) return false;
      
      /* Note: at least Integer.compareTo() doesn't accept null */
      if (_rhs == null) return true;
      if (_lhs == null) return false;
      
      if (_lhs instanceof Comparable)
        return ((Comparable)_lhs).compareTo(_rhs) > 0;
      if (_rhs instanceof Comparable)
        return ((Comparable)_rhs).compareTo(_lhs) < 0;
      
      return false;
    }
    public boolean isGreaterThanOrEqualTo(Object _lhs, Object _rhs) {
      if (_lhs == _rhs) return true;
      if (isEqualTo(_lhs, _rhs)) return true;
      return this.isGreaterThan(_lhs, _rhs);
    }
    
    public boolean isLessThan(Object _lhs, Object _rhs) {
      if (_lhs == _rhs) return false;
      if (this.isEqualTo(_lhs, _rhs)) return false;
      
      return !this.isGreaterThan(_lhs, _rhs);
    }
    public boolean isLessThanOrEqualTo(Object _lhs, Object _rhs) {
      if (_lhs == _rhs) return true;
      if (isEqualTo(_lhs, _rhs)) return true;
      return this.isLessThan(_lhs, _rhs);
    }

    public boolean doesContain(Object _item, Object _col) {
      if (_col == null || _item == null) return false;
      
      if (_col instanceof Collection)
        return ((Collection)_col).contains(_item);
      
      return false;
    }
    public boolean doesLike(Object _object, Object _pattern) {
      if (_object == null || _pattern == null)
        return false;
      
      String spat = _pattern.toString();
      if (spat.equals("*")) /* match everything */
        return true;
      
      // TODO: we should support much more, we only support prefix/suffix/infix
      
      boolean startsWithStar = spat.charAt(0) == '*';
      boolean endsWithStar   = spat.charAt(spat.length() - 1) == '*';
      
      String os = _object.toString();
      if (startsWithStar && endsWithStar)
        spat = spat.substring(1, spat.length() - 1);
      else if (startsWithStar)
        spat = spat.substring(1);
      else if (endsWithStar)
        spat = spat.substring(0, spat.length() - 1);
      else
        ;
      
      if (spat.indexOf('*') != -1)
        log.warn("LIKE pattern contains unprocessed patterns: " + _pattern);
      
      if (startsWithStar && endsWithStar)
        return os.indexOf(spat) != -1;
      
      if (startsWithStar)
        return os.endsWith(spat);

      if (endsWithStar)
        return os.startsWith(spat);
      
      return os.equals(spat);
    }
    public boolean doesCaseInsensitiveLike(Object _object, Object _pattern) {
      return false;
    }
  }
  
  public static class StringComparisonSupport extends ComparisonSupport {
    // TODO: implement me
    
    public boolean doesContain(Object _item, Object _col) {
      if (_col == null || _item == null) return false;

      if (_col instanceof Collection)
        return ((Collection)_col).contains(_item);
      
      return ((String)_col).indexOf((String)_item) != -1;
    }
    
    // TODO: implement doesLike

    public boolean doesCaseInsensitiveLike(Object _object, Object _pattern) {
      if (_object == null || _pattern == null) return false;
      return this.doesLike(((String)_object).toLowerCase(),
                           ((String)_pattern).toLowerCase());
    }
  }
  
  public static class DateComparisonSupport extends ComparisonSupport {
    // TODO: implement me
  }

  public static class CollectionComparisonSupport extends ComparisonSupport {

    public boolean doesContain(Object _item, Object _col) {
      if (_col == null || _item == null) return false;
      return ((Collection)_col).contains(_item);
    }
  }
  
  /* static init */
  
  static {
    classToSupport = new ConcurrentHashMap<Class, ComparisonSupport>(4);
    defaultSupport = new ComparisonSupport();
    
    classToSupport.put(Date.class,   new DateComparisonSupport());
    classToSupport.put(String.class, new StringComparisonSupport());
  }
}
