/*
 * TV-Browser
 * Copyright (C) 04-2003 Martin Oberhauser (darras@users.sourceforge.net)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * CVS information:
 *  $RCSfile$
 *   $Source$
 *     $Date: 2014-09-01 18:51:01 +0200 (Mo, 01 Sep 2014) $
 *   $Author: ds10 $
 * $Revision: 8229 $
 */

package tvbrowser.core.filters;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Pattern;

import tvbrowser.core.filters.filtercomponents.BeanShellFilterComponent;
import tvbrowser.core.filters.filtercomponents.FavoritesFilterComponent;
import tvbrowser.core.filters.filtercomponents.PluginFilterComponent;
import tvbrowser.core.filters.filtercomponents.ProgramMarkingPriorityFilterComponent;
import tvbrowser.core.filters.filtercomponents.ReminderFilterComponent;
import util.exc.ErrorHandler;
import util.io.stream.ObjectInputStreamProcessor;
import util.io.stream.ObjectOutputStreamProcessor;
import util.io.stream.StreamUtilities;
import devplugin.ProgramFilter;

class Token {

  final static int OR = 0, AND = 1, NOT = 2, ITEM = 3, LEFT_BRACKET = 4,
      RIGHT_BRACKET = 5;

  private static final util.ui.Localizer mLocalizer = util.ui.Localizer
      .getLocalizerFor(Token.class);

  public int type;
  public String value;
  public int pos;

  public Token() {
  }

  public String toString() {
    switch (type) {
    case OR:
      return "OR";
    case AND:
      return "AND";
    case NOT:
      return "NOT";
    case LEFT_BRACKET:
      return "(";
    case RIGHT_BRACKET:
      return ")";
    case ITEM:
      return value;
    default:
      return mLocalizer.msg("invalidToken", "invalid token");
    }
  }
} // class Token

public class UserFilter implements devplugin.ProgramFilter {

  private static final util.ui.Localizer mLocalizer = util.ui.Localizer
      .getLocalizerFor(UserFilter.class);

  private String mName, mRule;
  private File mFile;

  private static Token[] mTokenList = null;
  private static int mCurInx, curTokenInx;
  private static char[] mRuleLine;
  private static Token mCurToken;

  private Node mRoot;

  public UserFilter(String name) {
    mName = name;
  }

  public UserFilter(File file) throws ParserException {
    mFile = file;
    try {
      StreamUtilities.objectInputStream(file, 0x1000,
          new ObjectInputStreamProcessor() {

            @Override
            public void process(final ObjectInputStream in) throws IOException {
              in.readInt(); // version not yet used
              try {
                mName = (String) in.readObject();
                mRule = (String) in.readObject();
                mRule = modifyRule(mRule, false);
              } catch (ClassNotFoundException e) {
                ErrorHandler.handle("Could not read filter from file", e);
              }
            }
          });
    } catch (IOException e) {
      ErrorHandler.handle("Could not read filter from file", e);
    }

    createTokenTree();
  }
  
  public void store() {
    store(tvbrowser.core.filters.FilterList.FILTER_DIRECTORY,mName);
  }

  public void store(String directoy, String fileName) {
    try {
      mRule = modifyRule(mRule, true);
      final File file = new File(directoy, fileName + ".filter");
      StreamUtilities.objectOutputStream(file,
          new ObjectOutputStreamProcessor() {
            public void process(ObjectOutputStream out) throws IOException {
              out.writeInt(1); // version
              out.writeObject(mName);
              out.writeObject(mRule);
            }
          });
    } catch (IOException e) {
      ErrorHandler.handle("Could not write filter to file", e);
    }
  }

  private String modifyRule(String rule, boolean normalize) {
    if (rule == null) {
      return rule;
    }
    for (String keyWord : new String[]{"or","and","not"}) {
      String localized = mLocalizer.msg(keyWord, keyWord);
      if (localized.length() > 0) {
        String normalized = keyWord.toUpperCase();
        if (normalize) {
          rule = rule.replaceAll("\\b" + Pattern.quote(localized) + "\\b", normalized);
        }
        else {
          rule = rule.replaceAll("\\b" + Pattern.quote(normalized) + "\\b", localized);
        }
      }
    }
    return rule;
  }

  public void delete() {
    if (mFile != null) {
      mFile.delete();
    }
  }

  private void createTokenTree() throws ParserException {
    mTokenList = createTokenList(mRule);
    curTokenInx = -1;
    mCurToken = getNextToken();
    if (mCurToken != null) {
      Node rule = rule();
      if (mCurToken != null) {
        // throw new
        // ParserException(curToken.pos,mLocalizer.msg("EOLExpected","end of rule expected"));
      }
      mRoot = rule;
    }
  }

  private static Token getNextToken() throws ParserException {
    curTokenInx++;
    if (mTokenList.length > curTokenInx) {
      return mTokenList[curTokenInx];
    }
    return null;
  }

  public static void testTokenTree(String rule) throws ParserException {
    mTokenList = createTokenList(rule);
    curTokenInx = -1;
    mCurToken = getNextToken();
    if (mCurToken != null) {
      rule();
      if (mCurToken != null) {
        throw new ParserException(mCurToken.pos, mLocalizer.msg("EOLExpected",
            "end of rule expected"));
      }
    }
  }

  private static Token[] createTokenList(String rule) {

    mRuleLine = rule.toCharArray();
    mCurInx = 0;
    Token curToken = null;
    ArrayList<Token> list = new ArrayList<Token>();
    do {
      curToken = readNextToken();
      if (curToken != null) {
        list.add(curToken);
      }
    } while (curToken != null);

    Token[] result = new Token[list.size()];
    for (int i = 0; i < result.length; i++) {
      result[i] = list.get(i);
    }
    return result;
  }

  private static Token readNextToken() {

    ignoreSpaces();
    int i = mCurInx;

    if (mCurInx == mRuleLine.length) {
      return null;
    }

    Token result = new Token();
    result.pos = mCurInx;

    if (mRuleLine[mCurInx] == '(') {
      result.type = Token.LEFT_BRACKET;
      mCurInx++;
    } else if (mRuleLine[mCurInx] == ')') {
      result.type = Token.RIGHT_BRACKET;
      mCurInx++;
    } else {
      readString();
      result.value = new String(mRuleLine, i, mCurInx - i);
      if ("or".equalsIgnoreCase(result.value)
          || "oder".equalsIgnoreCase(result.value)
          || mLocalizer.msg("or", "or").equalsIgnoreCase(result.value)) {
        result.type = Token.OR;
      } else if ("and".equalsIgnoreCase(result.value)
          || "und".equalsIgnoreCase(result.value)
          || mLocalizer.msg("and", "and").equalsIgnoreCase(result.value)) {
        result.type = Token.AND;
      } else if ("not".equalsIgnoreCase(result.value)
          || "nicht".equalsIgnoreCase(result.value)
          || mLocalizer.msg("not", "not").equalsIgnoreCase(result.value)) {
        result.type = Token.NOT;
      } else {
        result.type = Token.ITEM;
      }
    }
    return result;
  }

  private static void readString() {
    while (mCurInx < mRuleLine.length
        && !Character.isWhitespace(mRuleLine[mCurInx]) && mRuleLine[mCurInx] != '('
        && mRuleLine[mCurInx] != ')') {
      mCurInx++;
    }
  }

  private static void ignoreSpaces() {
    while (mCurInx < mRuleLine.length && Character.isWhitespace(mRuleLine[mCurInx])) {
      mCurInx++;
    }
  }

  private static void expectToken(int[] expectedTypes, Token foundToken) throws ParserException {
    if (foundToken == null) {
      throw new ParserException(mLocalizer.msg("unexpectedEOL",
          "unexpected end of rule"));
    }

    for (int expectedType : expectedTypes) {
      if (expectedType == foundToken.type) {
        return;
      }
    }

    StringBuilder msg = new StringBuilder();
    for (int i = 0; i < expectedTypes.length; i++) {
      if (expectedTypes[i] == Token.AND) {
        msg.append("'").append(mLocalizer.msg("and", "and")).append("'");
      } else if (expectedTypes[i] == Token.ITEM) {
        msg.append(mLocalizer.msg("componentName", "component name"));
      } else if (expectedTypes[i] == Token.LEFT_BRACKET) {
        msg.append("'('");
      } else if (expectedTypes[i] == Token.RIGHT_BRACKET) {
        msg.append("')'");
      } else if (expectedTypes[i] == Token.NOT) {
        msg.append("'").append(mLocalizer.msg("not", "not")).append("'");
      } else if (expectedTypes[i] == Token.OR) {
        msg.append("'").append(mLocalizer.msg("or", "or")).append("'");
      }
      if (i < expectedTypes.length - 1) {
        msg.append(", ");
      }
    }
    msg.append(mLocalizer.msg("expected", "expected"));
    throw new ParserException(foundToken.pos, msg.toString());
  }

  private static Node rule() throws ParserException {
    Node result = new OrNode();
    result.addNode(condTerm());

    while (mCurToken != null && mCurToken.type == Token.OR) {
      mCurToken = getNextToken();
      if (mCurToken == null) {
        // throw new
        // ParserException(mLocalizer.msg("unexpectedEOL","unexpected end of rule"));
      }
      result.addNode(condTerm());
    }

    return result.optimize();
  }

  private static Node condTerm() throws ParserException {
    Node result = new AndNode();

    result.addNode(condFact());
    while (mCurToken != null && mCurToken.type == Token.AND) {
      mCurToken = getNextToken();
      if (mCurToken == null) {
        // throw new
        // ParserException(mLocalizer.msg("unexpectedEOL","unexpected end of rule"));
      }
      result.addNode(condFact());
    }
    return result;
  }

  private static Node condFact() throws ParserException {
    Node result, notNode = null;

    if (mCurToken == null) {
      throw new ParserException(mLocalizer.msg("unexpectedEOL",
          "unexpected end of rule"));
    }

    if (mCurToken.type == Token.NOT) {
      notNode = new NotNode();
      mCurToken = getNextToken();
      if (mCurToken == null) {
        throw new ParserException(mLocalizer.msg("unexpectedEOL",
            "unexpected end of rule"));
      }

    }

    if (mCurToken.type == Token.LEFT_BRACKET) {
      mCurToken = getNextToken();
      result = rule();
      expectToken(new int[] { Token.RIGHT_BRACKET }, mCurToken);
      mCurToken = getNextToken();
    }

    else {
      result = item();
      mCurToken = getNextToken();
    }

    if (notNode != null) {
      notNode.addNode(result);
      return notNode;
    }

    return result;
  }

  private static Node item() throws ParserException {
    Token tk = mCurToken;
    if (tk.type != Token.ITEM) {
      throw new ParserException(mLocalizer.msg("compExpected",
          "component name expected."));
    }

    FilterComponent component = FilterComponentList.getInstance()
        .getFilterComponentByName(tk.value);
    if (component != null) {
      return new ItemNode(component);
    }
    throw new ParserException(mLocalizer.msg("invalidCompName",
        "{0} is not a valid component name", tk.value));
  }

  public boolean accept(devplugin.Program prog) {
    if (mRoot == null) {
      return false;
    }
    return mRoot.accept(prog);
  }

  public void setName(String name) {
    if (!name.equals(mName) && mFile != null) {
      mFile.delete();
    }
    mName = name;
  }

  public String getName() {
    return mName;
  }

  public String toString() {
    return mName;
  }

  public void setRule(String rule) throws ParserException {
    mRule = rule;
    createTokenTree();
  }

  public String getRule() {
    return mRule;
  }

  public boolean containsRuleComponent(String comp) {
    if (mRoot == null) {
      return false;
    }
    return mRoot.containsRuleComponent(comp);
  }

  public boolean equals(Object o) {
    if (o instanceof ProgramFilter) {
      return getClass().equals(o.getClass())
          && getName().equals(((ProgramFilter) o).getName());
    }

    return false;
  }
  
  public boolean acceptableForFilterFavorite() {
    boolean acceptable = false;
    Token[] tokens = createTokenList(mRule);
    
    if(tokens != null) {
      acceptable = true;
      
      for(Token token : tokens) {
        if(token.type == Token.ITEM) {
          FilterComponent component = FilterComponentList.getInstance().getFilterComponentByName(token.value);
          
          if(component != null &&
              (component instanceof BeanShellFilterComponent ||
                  component instanceof FavoritesFilterComponent ||
                  component instanceof ProgramMarkingPriorityFilterComponent ||
                  component instanceof ReminderFilterComponent ||
                  component instanceof PluginFilterComponent)) {
            acceptable = false;
            break;
          }
        }
      }
    }
    
    return acceptable;
  }
}

abstract class Node {

  protected HashSet<Node> mNodes;

  public Node() {
    mNodes = new HashSet<Node>();
  }

  public Node optimize() {
    return this;
  }

  public void addNode(Node n) {
    mNodes.add(n);
  }

  public abstract boolean accept(devplugin.Program prog);

  public boolean containsRuleComponent(String compName) {
    Iterator<Node> it = mNodes.iterator();
    while (it.hasNext()) {
      Node n = it.next();
      if (n.containsRuleComponent(compName)) {
        return true;
      }
    }
    return false;
  }
}

class OrNode extends Node {
  public OrNode() {
    super();
  }

  public boolean accept(devplugin.Program prog) {
    Iterator<Node> it = mNodes.iterator();
    while (it.hasNext()) {
      Node n = it.next();
      if (n.accept(prog)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public Node optimize() {
    if (mNodes.size() == 1) {
      return mNodes.iterator().next();
    }
    return super.optimize();
  }
}

class AndNode extends Node {
  public AndNode() {
    super();
  }

  public boolean accept(devplugin.Program prog) {
    Iterator<Node> it = mNodes.iterator();
    while (it.hasNext()) {
      Node n = it.next();
      if (!n.accept(prog)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public Node optimize() {
    if (mNodes.size() == 1) {
      return mNodes.iterator().next();
    }
    return super.optimize();
  }
}

class NotNode extends Node {
  private Node mNode = null;

  public NotNode() {
  }

  public void addNode(Node node) {
    this.mNode = node;
  }

  public boolean accept(devplugin.Program prog) {
    return !mNode.accept(prog);
  }
}

class ItemNode extends Node {
  private FilterComponent mRule;

  public ItemNode(FilterComponent rule) {
    mRule = rule;
  }

  public boolean accept(devplugin.Program prog) {
    return mRule.accept(prog);
  }

  public boolean containsRuleComponent(String compName) {
    return (mRule.getName().equalsIgnoreCase(compName));
  }  
}
