/*
 * Copyright (c) 2003-2013, KNOPFLERFISH project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * - Neither the name of the KNOPFLERFISH project nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.knopflerfish.framework;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;

import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;

public class LDAPExpr {
  public static final int AND = 0;
  public static final int OR = 1;
  public static final int NOT = 2;
  public static final int EQ = 4;
  public static final int LE = 8;
  public static final int GE = 16;
  public static final int APPROX = 32;
  public static final int COMPLEX = AND | OR | NOT;
  public static final int SIMPLE = EQ | LE | GE | APPROX;

  private static final char WILDCARD = 65535;
  private static final String WILDCARD_STRING = new String(new char[] { WILDCARD });

  private static final String NULL = "Null query";
  private static final String GARBAGE = "Trailing garbage";
  private static final String EOS = "Unexpected end of query";
  private static final String MALFORMED = "Malformed query";
  private static final String OPERATOR = "Undefined operator";

  private static Class<?> classBigDecimal;
  private static Constructor<?> consBigDecimal;
  private static Method compBigDecimal;

  private static Class<?> classBigInteger;
  private static Constructor<?> consBigInteger;
  private static Method compBigInteger;

  public int operator;
  public LDAPExpr[] args;
  public String attrName;
  public String attrValue;


  public LDAPExpr(String filter) throws InvalidSyntaxException {

    final ParseState ps = new ParseState(filter);
    LDAPExpr expr = null;
    try {
      expr = parseExpr(ps);
    } catch (final StringIndexOutOfBoundsException e) {
      ps.error(EOS);
    }
    if (ps.rest().trim().length() != 0)
      ps.error(GARBAGE + " '" + ps.rest() + "'");
    operator = expr.operator;
    args = expr.args;
    attrName = expr.attrName;
    attrValue = expr.attrValue;
  }


  private LDAPExpr(int operator, LDAPExpr[] args) {
    this.operator = operator;
    this.args = args;
    this.attrName = null;
    this.attrValue = null;
  }


  private LDAPExpr(int operator, String attrName, String attrValue) {
    this.operator = operator;
    this.args = null;
    this.attrName = attrName;
    this.attrValue = attrValue;
  }


  /**
   * Get a set of object classes that might match this LDAP expression. This will not work
   * with wildcards and NOT expressions. If a set can not be determined return
   * null - meaning that all object classes must be considered when looking for matches.
   *
   * @return A set of classes that might match, otherwise <code>null</code>.
   */
  public Set<String> getMatchedObjectClasses() {
    Set<String> objClasses = null;
    if (operator == EQ) {
      if (attrName.equalsIgnoreCase(Constants.OBJECTCLASS) && attrValue.indexOf(WILDCARD) < 0) {
        objClasses = new OneSet<String>(attrValue);
      }
    } else if (operator == AND) {
      for (final LDAPExpr arg : args) {
        final Set<String> r = arg.getMatchedObjectClasses();
        if (r != null) {
          if (objClasses == null) {
            objClasses = new TreeSet<String>();
          }
          objClasses.addAll(r);
        }
      }
    } else if (operator == OR) {
      for (final LDAPExpr arg : args) {
        final Set<String> r = arg.getMatchedObjectClasses();
        if (r != null) {
          if (objClasses == null) {
            objClasses = new TreeSet<String>();
          }
          objClasses.addAll(r);
        } else {
          objClasses = null;
          break;
        }
      }
    }
    return objClasses;
  }


  /**
   * Checks if this LDAP expression is "simple". The definition of a simple
   * filter is:
   * <ul>
   * <li>{@code (<it>name</it>=<it>value</it>)} is simple if <it>name</it>
   * is a member of the provided {@code keywords}, and <it>value</it> does
   * not contain a wildcard character;</li>
   * <li>{@code (| EXPR+ )} is simple if all {@code EXPR} expressions
   * are simple;</li>
   * <li>No other expressions are simple.</li>
   * </ul>
   * If the filter is found to be simple, the {@code cache} is filled with
   * mappings from the provided keywords to lists of attribute values. The
   * keyword-value-pairs are the ones that satisfy this expression, for the
   * given keywords.
   *
   * @param keywords The keywords to look for.
   * @param cache An array (indexed by the keyword indexes) of lists to fill in
   *          with values saturating this expression.
   * @return {@code true} if this expression is simple, {@code false}
   *         otherwise.
   */
  public boolean isSimple(List<String> keywords, List<Object>[] cache, boolean matchCase) {
    if (operator == EQ) {
      int index;
      if ((index = keywords.indexOf(matchCase ? attrName : attrName.toLowerCase())) >= 0
          && attrValue.indexOf(WILDCARD) < 0) {
        if (cache[index] == null) {
          cache[index] = new ArrayList<Object>();
        }
        cache[index].add(attrValue);
        return true;
      }
    } else if (operator == OR) {
      for (int i = 0; i < args.length; i++) {
        if (!args[i].isSimple(keywords, cache, matchCase))
          return false;
      }
      return true;
    }
    return false;
  }


  public static boolean query(String filter, Dictionary<String, Object> pd)
      throws InvalidSyntaxException
  {
    return new LDAPExpr(filter).evaluate(pd, false);
  }

  /**
   * Evaluate this LDAP filter.
   */
  public boolean evaluate(Dictionary<String, ?> p, boolean matchCase) {
    if ((operator & SIMPLE) != 0) {
      return compare(p.get(matchCase ? attrName : attrName.toLowerCase()), operator, attrValue);
    } else { // (operator & COMPLEX) != 0
      switch (operator) {
      case AND:
        for (int i = 0; i < args.length; i++) {
          if (!args[i].evaluate(p, matchCase))
            return false;
        }
        return true;
      case OR:
        for (final LDAPExpr arg : args) {
          if (arg.evaluate(p, matchCase))
            return true;
        }
        return false;
      case NOT:
        return !args[0].evaluate(p, matchCase);
      default:
        return false; // Cannot happen
      }
    }
  }


  /**** Private methods ****/

  protected boolean compare(Object obj, int op, String s) {
    if (obj == null)
      return false;
    if (op == EQ && s.equals(WILDCARD_STRING))
      return true;
    try {
      if (obj instanceof String) {
        return compareString((String)obj, op, s);
      } else if (obj instanceof Character) {
        return compareString(obj.toString(), op, s);
      } else if (obj instanceof Boolean) {
        if (op == LE || op == GE)
          return false;
        if (((Boolean)obj).booleanValue()) {
          return s.equalsIgnoreCase("true");
        } else {
          return s.equalsIgnoreCase("false");
        }
      } else if (obj instanceof Number) {
        if (obj instanceof Byte) {
          switch (op) {
          case LE:
            return ((Byte)obj).byteValue() <= Byte.parseByte(s);
          case GE:
            return ((Byte)obj).byteValue() >= Byte.parseByte(s);
          default: /* APPROX and EQ */
            return (new Byte(s)).equals(obj);
          }
        } else if (obj instanceof Integer) {
          switch (op) {
          case LE:
            return ((Integer)obj).intValue() <= Integer.parseInt(s);
          case GE:
            return ((Integer)obj).intValue() >= Integer.parseInt(s);
          default: /* APPROX and EQ */
            return (new Integer(s)).equals(obj);
          }
        } else if (obj instanceof Short) {
          switch (op) {
          case LE:
            return ((Short)obj).shortValue() <= Short.parseShort(s);
          case GE:
            return ((Short)obj).shortValue() >= Short.parseShort(s);
          default: /* APPROX and EQ */
            return (new Short(s)).equals(obj);
          }
        } else if (obj instanceof Long) {
          switch (op) {
          case LE:
            return ((Long)obj).longValue() <= Long.parseLong(s);
          case GE:
            return ((Long)obj).longValue() >= Long.parseLong(s);
          default: /* APPROX and EQ */
            return (new Long(s)).equals(obj);
          }
        } else if (obj instanceof Float) {
          switch (op) {
          case LE:
            return ((Float)obj).floatValue() <= (new Float(s)).floatValue();
          case GE:
            return ((Float)obj).floatValue() >= (new Float(s)).floatValue();
          default: /* APPROX and EQ */
            return (new Float(s)).equals(obj);
          }
        } else if (obj instanceof Double) {
          switch (op) {
          case LE:
            return ((Double)obj).doubleValue() <= (new Double(s)).doubleValue();
          case GE:
            return ((Double)obj).doubleValue() >= (new Double(s)).doubleValue();
          default: /* APPROX and EQ */
            return (new Double(s)).equals(obj);
          }
        } else if (classBigInteger != null && classBigInteger.isInstance(obj)) {
          final Object n = consBigInteger.newInstance(new Object[] { s });
          final int c = ((Integer)compBigInteger.invoke(obj, new Object[] { n })).intValue();

          switch (op) {
          case LE:
            return c <= 0;
          case GE:
            return c >= 0;
          default: /* APPROX and EQ */
            return c == 0;
          }
        } else if (classBigDecimal != null && classBigDecimal.isInstance(obj)) {
          final Object n = consBigDecimal.newInstance(new Object[] { s });
          final int c = ((Integer)compBigDecimal.invoke(obj, new Object[] { n })).intValue();

          switch (op) {
          case LE:
            return c <= 0;
          case GE:
            return c >= 0;
          default: /* APPROX and EQ */
            return c == 0;
          }
        }
      } else if (obj instanceof Collection) {
        for (final Object name : ((Collection<?>) obj))
          if (compare(name, op, s))
            return true;
      } else if (obj.getClass().isArray()) {
        final int len = Array.getLength(obj);
        for (int i = 0; i < len; i++)
          if (compare(Array.get(obj, i), op, s))
            return true;
      } else {
        // Extended comparison
        // Allow simple EQ comparison on all classes having
        // a string constructor, and use compareTo if they
        // implement Comparable
        final Class<?> clazz = obj.getClass();
        final Constructor<?> cons = getConstructor(clazz);

        if (cons != null) {
          final Object other = cons.newInstance(new Object[] { s });
          if (obj instanceof Comparable) {
            @SuppressWarnings({ "unchecked" })
            final int c = ((Comparable<Object>)obj).compareTo(other);
            switch (op) {
            case LE:
              return c <= 0;
            case GE:
              return c >= 0;
            default: /* APPROX and EQ */
              return c == 0;
            }
          } else {
            boolean b = false;
            if (op == LE || op == GE || op == EQ || op == APPROX) {
              b = obj.equals(other);
            }
            return b;
          }
        }
      }
    } catch (final Exception ignored_but_evals_to_false) {
      // This might happen if a string-to-datatype conversion fails
      // Just consider it a false match and ignore the exception
    }
    return false;
  }

  // Clazz -> Constructor(String)
  private static HashMap<Class<?>, Constructor<?>> constructorMap
    = new HashMap<Class<?>, Constructor<?>>();


  /**
   * Get cached String constructor for a class
   */
  private static Constructor<?> getConstructor(Class<?> clazz) {
    synchronized (constructorMap) {

      // This might be null
      Constructor<?> cons = constructorMap.get(clazz);

      // ...check if we have tried before. A failed try
      // is stored as null
      if (!constructorMap.containsKey(clazz)) {
        try {
          cons = clazz.getConstructor(new Class[] { String.class });
        } catch (final Exception e) {
          // remember by storing null in map
        }
        constructorMap.put(clazz, cons);
      }
      return cons;
    }
  }

  static {
    try {
      classBigDecimal = Class.forName("java.math.BigDecimal");
      consBigDecimal = getConstructor(classBigDecimal);
      compBigDecimal = classBigDecimal.getMethod("compareTo", new Class[] { classBigDecimal });
    } catch (final Exception ignore) {
      classBigDecimal = null;
    }
    try {
      classBigInteger = Class.forName("java.math.BigInteger");
      consBigInteger = getConstructor(classBigInteger);
      compBigInteger = classBigInteger.getMethod("compareTo", new Class[] { classBigInteger });
    } catch (final Exception ignore) {
      classBigInteger = null;
    }
  }


  private static boolean compareString(String s1, int op, String s2) {
    switch (op) {
    case LE:
      return s1.compareTo(s2) <= 0;
    case GE:
      return s1.compareTo(s2) >= 0;
    case EQ:
      return patSubstr(s1, s2);
    case APPROX:
      return fixupString(s2).equals(fixupString(s1));
    default:
      return false;
    }
  }


  private static String fixupString(String s) {
    final StringBuffer sb = new StringBuffer();
    final int len = s.length();
    for (int i = 0; i < len; i++) {
      char c = s.charAt(i);
      if (!Character.isWhitespace(c)) {
        if (Character.isUpperCase(c))
          c = Character.toLowerCase(c);
        sb.append(c);
      }
    }
    return sb.toString();
  }


  private static boolean patSubstr(String s, String pat) {
    return s == null ? false : patSubstr(s.toCharArray(), 0, pat.toCharArray(), 0);
  }


  private static boolean patSubstr(char[] s, int si, char[] pat, int pi) {
    if (pat.length - pi == 0)
      return s.length - si == 0;
    if (pat[pi] == WILDCARD) {
      pi++;
      for (;;) {
        if (patSubstr(s, si, pat, pi))
          return true;
        if (s.length - si == 0)
          return false;
        si++;
      }
    } else {
      if (s.length - si == 0) {
        return false;
      }
      if (s[si] != pat[pi]) {
        return false;
      }
      return patSubstr(s, ++si, pat, ++pi);
    }
  }


  private static LDAPExpr parseExpr(ParseState ps) throws InvalidSyntaxException {
    ps.skipWhite();
    if (!ps.prefix("("))
      ps.error(MALFORMED);

    int operator;
    ps.skipWhite();
    switch (ps.peek()) {
    case '&':
      operator = AND;
      break;
    case '|':
      operator = OR;
      break;
    case '!':
      operator = NOT;
      break;
    default:
      return parseSimple(ps);
    }
    ps.skip(1); // Ignore the operator
    final List<LDAPExpr> v = new ArrayList<LDAPExpr>();
    do {
      v.add(parseExpr(ps));
      ps.skipWhite();
    } while (ps.peek() == '(');
    final int n = v.size();
    if (!ps.prefix(")") || n == 0 || (operator == NOT && n > 1))
      ps.error(MALFORMED);
    final LDAPExpr[] args = new LDAPExpr[n];
    v.toArray(args);
    return new LDAPExpr(operator, args);
  }


  private static LDAPExpr parseSimple(ParseState ps) throws InvalidSyntaxException {
    final String attrName = ps.getAttributeName();
    if (attrName == null)
      ps.error(MALFORMED);
    int operator = 0;
    if (ps.prefix("="))
      operator = EQ;
    else if (ps.prefix("<="))
      operator = LE;
    else if (ps.prefix(">="))
      operator = GE;
    else if (ps.prefix("~="))
      operator = APPROX;
    else {
      // System.out.println("undef op='" + ps.peek() + "'");
      ps.error(OPERATOR); // Does not return
    }
    final String attrValue = ps.getAttributeValue();
    if (!ps.prefix(")"))
      ps.error(MALFORMED);
    return new LDAPExpr(operator, attrName, attrValue);
  }


  @Override
  public String toString() {
    final StringBuffer res = new StringBuffer();
    res.append("(");
    if ((operator & SIMPLE) != 0) {
      res.append(attrName);
      switch (operator) {
      case EQ:
        res.append("=");
        break;
      case LE:
        res.append("<=");
        break;
      case GE:
        res.append(">=");
        break;
      case APPROX:
        res.append("~=");
        break;
      }
      for (int i = 0; i < attrValue.length(); i++) {
        char c = attrValue.charAt(i);
        if (c == '(' || c == ')' || c == '*' || c == '\\') {
          res.append('\\');
        } else if (c == WILDCARD) {
          c = '*';
        }
        res.append(c);
      }
    } else {
      switch (operator) {
      case AND:
        res.append("&");
        break;
      case OR:
        res.append("|");
        break;
      case NOT:
        res.append("!");
        break;
      }
      for (final LDAPExpr arg : args) {
        res.append(arg.toString());
      }
    }
    res.append(")");
    return res.toString();
  }

  /**
   * Contains the current parser position and parsing utility methods.
   */
  private static class ParseState {
    int pos;
    String str;


    public ParseState(String str) throws InvalidSyntaxException {
      this.str = str;
      if (str.length() == 0)
        error(NULL);
      pos = 0;
    }


    public boolean prefix(String pre) {
      if (!str.startsWith(pre, pos))
        return false;
      pos += pre.length();
      return true;
    }


    public char peek() {
      return str.charAt(pos);
    }


    public void skip(int n) {
      pos += n;
    }


    public String rest() {
      return str.substring(pos);
    }


    public void skipWhite() {
      while (Character.isWhitespace(str.charAt(pos))) {
        pos++;
      }
    }


    public String getAttributeName() {
      final int start = pos;
      int end = -1;
      for (;; pos++) {
        final char c = str.charAt(pos);
        if (c == '(' || c == ')' || c == '<' || c == '>' || c == '=' || c == '~') {
          break;
        } else if (!Character.isWhitespace(c)) {
          end = pos;
        }
      }
      if (end == -1) {
        return null;
      }
      return str.substring(start, end + 1);
    }


    public String getAttributeValue() {
      final StringBuffer sb = new StringBuffer();
      label: for (;; pos++) {
        final char c = str.charAt(pos);
        switch (c) {
        case '(':
        case ')':
          break label;
        case '*':
          sb.append(WILDCARD);
          break;
        case '\\':
          sb.append(str.charAt(++pos));
          break;
        default:
          sb.append(c);
          break;
        }
      }
      return sb.toString();
    }


    public void error(String m) throws InvalidSyntaxException {
      throw new InvalidSyntaxException(m, (str == null) ? "" : str.substring(pos));
    }
  }

  /**
   * Set with one element maximum.
   */
  private static class OneSet<E> extends AbstractSet<E> {

    final private E elem;


    OneSet(E o) {
      elem = o;
    }


    @Override
    public Iterator<E> iterator() {
      return new Iterator<E>() {
        E ielem = elem;


        public boolean hasNext() {
          return ielem != null;
        }


        public E next() {
          if (ielem != null) {
            final E r = ielem;
            ielem = null;
            return r;
          } else {
            throw new NoSuchElementException();
          }
        }


        public void remove() {
          throw new UnsupportedOperationException();
        }
      };
    }


    @Override
    public int size() {
      return elem == null ? 0 : 1;
    }
  }

}
