/*
 * Copyright (C) 2008, 2012 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 3 or version 2
 *  as published by the Free Software Foundation.
 *
 *  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.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 02-Nov-2008
 */
package uk.me.parabola.mkgmap.osmstyle;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Formatter;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.osmstyle.actions.Action;
import uk.me.parabola.mkgmap.osmstyle.actions.ActionList;
import uk.me.parabola.mkgmap.osmstyle.actions.ActionReader;
import uk.me.parabola.mkgmap.osmstyle.actions.AddTagAction;
import uk.me.parabola.mkgmap.osmstyle.actions.DeleteAction;
import uk.me.parabola.mkgmap.osmstyle.eval.EqualsOp;
import uk.me.parabola.mkgmap.osmstyle.eval.ExpressionReader;
import uk.me.parabola.mkgmap.osmstyle.eval.NotOp;
import uk.me.parabola.mkgmap.osmstyle.eval.Op;
import uk.me.parabola.mkgmap.osmstyle.eval.ValueOp;
import uk.me.parabola.mkgmap.osmstyle.function.GetTagFunction;
import uk.me.parabola.mkgmap.reader.osm.FeatureKind;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.Rule;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.TokType;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokenScanner;

/**
 * Read a rules file.  A rules file contains a list of rules and the
 * resulting garmin type, should the rule match.
 *
 * @author Steve Ratcliffe
 */
public class RuleFileReader {
	private static final Logger log = Logger.getLogger(RuleFileReader.class);

	private final FeatureKind kind;
	private final TypeReader typeReader;

	private final RuleSet rules;
	private RuleSet finalizeRules;
	private final boolean performChecks;
	private final boolean forRoutableMap;
	private final Map<Integer, List<Integer>> overlays;

	private final Deque<Op[]> ifStack = new LinkedList<>();
	public static final String IF_PREFIX = "mkgmap:if:"; 

	private boolean inFinalizeSection;

	private final ExpressionArranger arranger = new ExpressionArranger();

	public RuleFileReader(FeatureKind kind, LevelInfo[] levels, RuleSet rules, boolean performChecks, 
			Map<Integer, List<Integer>> overlays) {
		this(kind, levels, rules, performChecks, false, overlays);
	}

	public RuleFileReader(FeatureKind kind, LevelInfo[] levels, RuleSet rules, boolean performChecks, boolean forRoutableMap,
			Map<Integer, List<Integer>> overlays) {
		this.kind = kind;
		this.rules = rules;
		this.performChecks = performChecks;
		this.forRoutableMap = forRoutableMap;
		this.overlays = overlays;
		typeReader = new TypeReader(kind, levels);
	}

	/**
	 * Read a rules file.
	 * @param loader A file loader.
	 * @param name The name of the file to open.
	 * @throws FileNotFoundException If the given file does not exist.
	 */
	public void load(StyleFileLoader loader, String name) throws FileNotFoundException {
		loadFile(loader, name);
		rules.prepare();
		if (finalizeRules != null) {
			finalizeRules.prepare();
			rules.setFinalizeRule(finalizeRules);
		}
	}

	/**
	 * Load a rules file.  This should be used when calling recursively when including
	 * files.
	 */
	private void loadFile(StyleFileLoader loader, String name) throws FileNotFoundException {
		Reader r = loader.open(name);
		TokenScanner scanner = new TokenScanner(name, r);
		scanner.setExtraWordChars("-:.");

		ExpressionReader expressionReader = new ExpressionReader(scanner, kind);
		ActionReader actionReader = new ActionReader(scanner);

		// Read all the rules in the file.
		scanner.skipSpace();
		while (!scanner.isEndOfFile()) {
			if (checkCommand(loader, scanner, expressionReader))
				continue;

			if (scanner.isEndOfFile())
				break;

			Op expr = expressionReader.readConditions(ifStack);
			expr = arranger.arrange(expr);

			ActionList actionList = actionReader.readActions();
			checkIfStack(actionList);

			if (performChecks && this.kind == FeatureKind.RELATION) {
				String actionsString = actionList.getList().toString();
				if (actionsString.contains("set mkgmap:stylefilter") || actionsString.contains("add mkgmap:stylefilter")) {
					log.error("Style file", name, "should not set or add the special tag mkgmap:stylefilter:", actionsString);
				}
			}

			List<GType> types = new ArrayList<>();
			while (scanner.checkToken("[")) {
				GType type = typeReader.readType(scanner, performChecks, forRoutableMap, overlays);
				types.add(type);
				scanner.skipSpace();
			}
			
			// If there is an action list, then we don't need a type
			if (types.isEmpty() && actionList.isEmpty())
				throw new SyntaxException(scanner, "No type definition given");

			if (types.isEmpty())
				saveRule(scanner, expr, actionList, null);
			
			if (types.size() >= 2 && actionList.isModifyingTags()) {
				throw new SyntaxException(scanner, "Combination of multiple type definitions with tag modifying action is not yet supported.");
			}

			for (int i = 0; i < types.size(); i++) {
				GType type = types.get(i);
				if (i + 1 < types.size()) {
					type.setContinueSearch(true);
				}
				// No need to create a deep copy of expr
				saveRule(scanner, expr, actionList, type);
				actionList = new ActionList(Collections.emptyList(), Collections.emptySet());
			}
		}

		rules.addUsedTags(expressionReader.getUsedTags());
		rules.addUsedTags(actionReader.getUsedTags());
	}

	/**
	 * Check for a keyword that introduces a command.
	 *
	 * Commands are context sensitive, if a keyword is used is part of an expression, then it must still
	 * work. In other words the following is valid:
	 * <pre>
	 *     include 'filename';
	 *
	 *     include=yes [0x02 ...]
	 * </pre>
	 * To achieve this the keyword is a) not quoted, b) is followed by text or quoted text or some symbol that cannot
	 * be part of an expression.
	 *
	 * Called before reading an expression, must put back any token (apart from whitespace) if there is
	 * not a command.
	 * @return true if a command was found. The caller should check again for a command.
	 * @param currentLoader The current style loader. Any included files are loaded from here, if no other
	 * style is specified.
	 * @param scanner The current token scanner.
	 * @param expressionReader The current expression reader
	 */
	private boolean checkCommand(StyleFileLoader currentLoader, TokenScanner scanner, ExpressionReader expressionReader) {
		scanner.skipSpace();
		if (scanner.isEndOfFile())
			return false;

		if (scanner.checkToken("include")) {
			if (readInclude(currentLoader, scanner)) return true;

		} else if (scanner.checkToken("if")) {
			if (readIf(scanner, expressionReader)) return true;

		} else if (scanner.checkToken("else")) {
			if (readElse(scanner)) return true;

		} else if (scanner.checkToken("end")) {
			if (readEnd(scanner)) return true;

		} else if (scanner.checkToken("<")) {
			// check if it is the start label of the <finalize> section
			if (readFinalize(scanner)) return true;
		}
		scanner.skipSpace();
		return false;
	}

	private boolean readIf(TokenScanner scanner, ExpressionReader expressionReader) {
		// Take the 'if' token
		Token tok = scanner.nextToken();
		scanner.skipSpace();

		// If 'if'' is being used as a keyword then it is followed by a '('.
		Token next = scanner.peekToken();
		if (next.getType() == TokType.SYMBOL && next.isValue("(")) {
			Op origExpr = expressionReader.readConditions();
			scanner.validateNext("then");
			
			// add rule expr { set <ifVar> = true } 
			String ifVar = getNextIfVar();
			ArrayList<Action> actions = new ArrayList<>(1);
			actions.add(new AddTagAction(ifVar,"true", true));
			ActionList actionList = new ActionList(actions, Collections.singleton(ifVar+"=true"));
			saveRule(scanner, origExpr, actionList, null);
			// create expression (<ifVar> = true) 
			EqualsOp safeExpr = new EqualsOp();
			safeExpr.setFirst(new GetTagFunction(ifVar));
			safeExpr.setSecond(new ValueOp("true"));
			Op[] ifExpressions = {origExpr, safeExpr};  
			ifStack.addLast(ifExpressions);
			
			return true;
		} else {
			// Wrong syntax for if statement, so push back token to allow a possible expression to be read
			scanner.pushToken(tok);
		}
		return false;
	}

	private boolean readElse(TokenScanner scanner) {
		Token tok = scanner.nextToken();
		scanner.skipSpace();

		Token next = scanner.peekToken();
		if (next.getType() == TokType.SYMBOL && !next.isValue("(") && !next.isValue("!")) {
			scanner.pushToken(tok);
			return false;
		}

		Op[] ifExpressions = ifStack.removeLast();
		for (int i = 0; i < ifExpressions.length; i++) {
			Op op = ifExpressions[i];
			NotOp not = new NotOp();
			not.setFirst(op);
			ifExpressions[i] = not;
		}
		ifStack.addLast(ifExpressions);

		return true;
	}

	private boolean readEnd(TokenScanner scanner) {
		Token tok = scanner.nextToken();
		scanner.skipSpace();
		if (ifStack.isEmpty()) {
			scanner.pushToken(tok);
			return false;
		}

		ifStack.removeLast();
		return true;
	}

	/**
	 * Check if one of the actions in the actionList would change the result of a previously read if expression.
	 * If so, use the alternative expression with the generated tag.
	 */
	private void checkIfStack(ActionList actionList) {
		if (actionList.isEmpty())
			return;
		for (Op[] ops : ifStack) {
			if (ops[0] != ops[1]) {
				// check if this action can change the result of the initial if expression 
				if (possiblyChanged(ops[0], actionList)) {
					// the result may be changed, use the generated tag for all further rules in this if / else block
					ops[0] = ops[1];
				} 
			}
		}
	}

	/**
	 * Check if the expression depends on tags modified by an action in the action list. 
	 * @param expr the expression to check
	 * @param actionList the ActionList 
	 * @return true if the value of the expression depends on one or more of the changeable tags
	 */
	private boolean possiblyChanged(Op expr, ActionList actionList) {
		Set<String> evaluated = expr.getEvaluatedTagKeys();
		if (evaluated.isEmpty())
			return false;
		for (String tagKey : evaluated) {
			for (String s : actionList.getChangeableTags()) {
				int pos = s.indexOf('=');
				String key = pos > 0 ? s.substring(0, pos) : s;
				if (tagKey.equals(key)) {
					return true;
				}
			}
			for (Action a : actionList.getList()) {
				if (a instanceof DeleteAction) {
					if (a.toString().contains(tagKey)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private boolean readInclude(StyleFileLoader currentLoader, TokenScanner scanner) {
		// Consume the 'include' token and skip spaces
		Token token = scanner.nextToken();
		scanner.skipSpace();

		// If include is being used as a keyword then it is followed by a word or a quoted word.
		Token next = scanner.peekToken();
		if (next.getType() == TokType.TEXT
				|| next.getType() == TokType.SYMBOL && (next.isValue("'") || next.isValue("\"")))
		{
			String filename = scanner.nextWord();

			StyleFileLoader loader = currentLoader;
			scanner.skipSpace();

			// The include can be followed by an optional 'from' clause. The file is read from the given
			// style-name in that case.
			if (scanner.checkToken("from")) {
				scanner.nextToken();
				String styleName = scanner.nextWord();
				if (Objects.equals(styleName, ";"))
					throw new SyntaxException(scanner, "No style name after 'from'");

				try {
					loader = StyleFileLoader.createStyleLoader(null, styleName);
				} catch (FileNotFoundException e) {
					throw new SyntaxException(scanner, "Cannot find style: " + styleName);
				}
			}

			if (scanner.checkToken(";"))
				scanner.nextToken();

			try {
				loadFile(loader, filename);
				return true;
			} catch (FileNotFoundException e) {
				throw new SyntaxException(scanner, "Cannot open included file: " + filename);
			} finally {
				if (loader != currentLoader)
					Utils.closeFile(loader);
			}
		} else {
			// Wrong syntax for include statement, so push back token to allow a possible expression to be read
			scanner.pushToken(token);
		}
		return false;
	}

	private boolean readFinalize(TokenScanner scanner) {
		Token token = scanner.nextToken();
		if (scanner.checkToken("finalize")) {
			Token finalizeToken = scanner.nextToken();
			if (scanner.checkToken(">")) {
				if (inFinalizeSection) {
					// there are two finalize sections which is not allowed
					throw new SyntaxException(scanner, "There is only one finalize section allowed");
				} else {
					// consume the > token
					scanner.nextToken();
					// mark start of the finalize block
					inFinalizeSection = true;
					finalizeRules = new RuleSet();
					return true;
				}
			} else {
				scanner.pushToken(finalizeToken);
				scanner.pushToken(token);
			}
		} else {
			scanner.pushToken(token);
		}
		return false;
	}

	/**
	 * Save the expression as a rule.  We need to extract an index such
	 * as highway=primary first and then add the rest of the expression as
	 * the condition for it.
	 *
	 * So in other words each condition is dropped into a number of different
	 * baskets based on the first 'tag=value' term.  We then only look
	 * for expressions that are in the correct basket.  For each expression
	 * in a basket we know that the first term is true so we can drop that
	 * from the expression.
	 */
	private void saveRule(TokenScanner scanner, Op op, ActionList actions, GType gt) {
		log.debug("EXP", op, ", type=", gt);

		// check if the type definition is allowed
		if (inFinalizeSection && gt != null)
			throw new SyntaxException(scanner, "Element type definition is not allowed in <finalize> section");

		Iterator<Op> it = arranger.prepareForSave(op);
		while (it.hasNext()) {
			Op prepared = it.next();
			String keystring = arranger.getKeystring(scanner, prepared);
			createAndSaveRule(keystring, prepared, actions, gt);
		}
	}

	private void createAndSaveRule(String keystring, Op expr, ActionList actions, GType gt) {

		Rule rule;
		if (actions.isEmpty()) 
			rule = new ExpressionRule(expr, gt);
		else
			rule = new ActionRule(expr, actions.getList(), gt);

		if (inFinalizeSection)
			finalizeRules.add(keystring, rule, actions.getChangeableTags());
		else
			rules.add(keystring, rule, actions.getChangeableTags());
	}

	
	private int ifCounter;
	/**
	 * 
	 * @return a new tag key unique 
	 */
	public String getNextIfVar (){
		return IF_PREFIX  + ++ifCounter;
	}
	
	public static void main(String[] args) throws FileNotFoundException {
		if (args.length > 0) {
			RuleSet rs = new RuleSet();
			RuleFileReader rr = new RuleFileReader(FeatureKind.POLYLINE,
					LevelInfo.createFromString("0:24 1:20 2:18 3:16 4:14"), rs, false,
					Collections.emptyMap());

			StyleFileLoader loader = new DirectoryFileLoader(
					new File(args[0]).getAbsoluteFile().getParentFile());
			String fname = new File(args[0]).getName();
			rr.load(loader, fname);


			StylePrinter.dumpRuleSet(new Formatter(System.out), "rules", rs);
		} else {
			System.err.println("Usage: RuleFileReader <file>");
		}
	}
}
