/*
 * Copyright (c) 2009.
 *
 * 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.
 */

package uk.me.parabola.mkgmap.osmstyle;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;

import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.Rule;
import uk.me.parabola.mkgmap.reader.osm.TypeResult;
import uk.me.parabola.mkgmap.reader.osm.Way;

import org.junit.Test;

import static func.lib.TestUtils.makeRuleSet;
import static org.junit.Assert.*;

/**
 * More tests for rule sets. Mostly concentrating on ordering issues and
 * not on the resulting type.
 * 
 * @see RuleFileReaderTest
 */
public class RuleSetTest {
	private final String MAXSPEED_EXAMPLE = "highway=* & maxspeed=40mph {set mcssl=40}" +
			"highway=primary & mcssl=40 [0x01]" +
			"highway=* & mcssl=40 [0x02]" +
			"highway=primary [0x3]";

	/**
	 * A test for matching in the correct order with a simple set
	 * of tags.  See also the next test.
	 */
	@Test
	public void testFirstMatch1() {
		RuleSet rs = makeRuleSet("c=d & a=b [0x1]" +
				"a=b & c=d [0x2]" +
				"a=b [0x3]");

		Way el = new Way(1);
		el.addTag("a", "b");
		el.addTag("c", "d");
		GType type = getFirstType(rs, el);
		assertNotNull("should be found", type);
		assertEquals("first matching rule wins", 1, type.getType());
	}
	/**
	 * As previous test but with order reversed.  Depending on the order
	 * that the tags iterate from the way, you might get different results.
	 */
	@Test
	public void testFirstMatch2() {
		RuleSet rs = makeRuleSet("a=b & c=d [0x1]" +
				"c=d & a=b [0x2]" +
				"a=b [0x3]");

		Way el = new Way(1);
		el.addTag("a", "b");
		el.addTag("c", "d");
		GType type = getFirstType(rs, el);
		assertNotNull("should be found", type);
		assertEquals("first matching rule wins", 1, type.getType());
	}

	/**
	 * An action variable is set on a rule that starts with an exists clause.
	 * We then attempt to match on value that it is 
	 * @throws Exception
	 */
	@Test
	public void testActionVarSetOnExistsRule1() throws Exception {
		RuleSet rs = makeRuleSet(MAXSPEED_EXAMPLE);

		Way el = new Way(1);
		el.addTag("highway", "primary");
		el.addTag("maxspeed", "40mph");
		el.addTag("ref", "A123");
		el.addTag("name", "Long Lane");

		GType type = getFirstType(rs, el);
		assertEquals("should match first", 1, type.getType());
	}

	@Test
	public void testActionVarSetOnExistsRule2() {
		RuleSet rs = makeRuleSet(MAXSPEED_EXAMPLE);

		Way el = new Way(1);
		el.addTag("highway", "unclassified");
		el.addTag("maxspeed", "40mph");
		el.addTag("ref", "A123");
		el.addTag("name", "Long Lane");

		GType type = getFirstType(rs, el);
		assertEquals("should match first", 2, type.getType());
	}

	/**
	 * Check that actions are run in the order given.  Use the add command
	 * to set a variable.  The first add that is run will be the value of
	 * the variable.
	 */
	@Test
	public void testActionOrder() {
		RuleSet rs = makeRuleSet("b=c {add fred=1}" +
				"a=b {add fred=2}" +
				"c=d {add fred=3}" +
				"a=b [0x1]");

		// All of the conditions are set.
		Way el = new Way(1);
		el.addTag("a", "b");
		el.addTag("b", "c");
		el.addTag("c", "d");

		getFirstType(rs, el);
		assertEquals("b=c was first action", "1", el.getTag("fred"));
	}

	/**
	 * Match on a tag that was set in a previous action rule and was not
	 * on the original element.
	 */
	@Test
	public void testMatchOnSetTag() {
		RuleSet rs = makeRuleSet("highway=yes {set abcxyz = 1}" +
				"abcxyz=1 [0x1]");

		Way el = new Way(1);
		el.addTag("highway", "yes");

		GType type = getFirstType(rs, el);
		assertNotNull("type matched on previously set tag", type);
	}

	/**
	 * A chain of rules, some of which contain tags from the element and
	 * some that contain only tags that are set in previous rules.
	 */
	@Test
	public void testOrderChain() {
		RuleSet rs = makeRuleSet("z=1 {add fred=1;}" +
				"fred=1 {add abba=1}" +
				"z=1 & abba=1 {add destiny=1}" +
				"destiny=1 [0x1]" +
				"z=1 [0x2]");

		Way el = new Way(1);
		el.addTag("z", "1");

		GType type = getFirstType(rs, el);
		assertNotNull("chain of commands", type);
		assertEquals("'destiny' should be selected", 1, type.getType());
	}

	/**
	 * A chain of rules, some of which contain tags from the element and
	 * some that contain only tags that are set in previous rules.
	 */
	@Test
	public void testOrderChain2() {
		RuleSet rs = makeRuleSet("z=1 {add fred=1;}" +
				"fred=1 {add abba=1}" +
				"abba=1 {add destiny=1}" +
				"destiny=1 [0x1]");

		Way el = new Way(1);
		el.addTag("z", "1");

		GType type = getFirstType(rs, el);
		assertNotNull("chain of commands", type);
	}

	/**
	 * Append to a variable in the correct order as in the rule set.
	 */
	@Test
	public void testAppendInOrder() {
		RuleSet rs = makeRuleSet("highway=primary {set R='${R} a'}" +
				"ref=A1 {set R='${R} b'}" +
				"z=1 {set R='${R} c'}" +
				"a=1 {set R='${R} d'}");

		Way el = new Way(1);
		el.addTag("R", "init");
		el.addTag("highway", "primary");
		el.addTag("ref", "A1");
		el.addTag("z", "1");
		el.addTag("a", "1");

		getFirstType(rs, el);
		String s = el.getTag("R");
		assertEquals("appended value", "init a b c d", s);
	}

	/**
	 * Rules should only be evaluated once for an element.  Because of the
	 * way that we handle rules that may get run after tags are set in actions
	 * it is possible that a rule would get run twice if not careful.
	 *
	 * It is not that easy to trigger, as this is the second attempt at
	 * showing it is possible...
	 */
	@Test
	public void testRuleEvaluatedOnce() {
		RuleSet rs = makeRuleSet("highway=primary " +
				"  {set highway=primary; set result='${result} 1';}" +
				"highway='primary' {set result='${result} 2'");
		Way el = new Way(1);
		el.addTag("highway", "primary");
		el.addTag("result", "0");

		getFirstType(rs, el);
		assertEquals("rules run once", "0 1 2", el.getTag("result"));
	}

	/**
	 * The example that was in the check in comment, make sure it actually
	 * does work ;)
	 */
	@Test
	public void testCheckinExample() {
		RuleSet rs = makeRuleSet("highway=motorway  {set blue=true;}\n" +
				"blue=true  [0x1 ]\n" +
				"highway=motorway [0x2]");

		Way el = new Way(1);
		el.addTag("highway", "motorway");

		GType type = getFirstType(rs, el);
		assertEquals("first match is on blue", 1, type.getType());
	}

	@Test
	public void testActionsMixedWithTypes() {
		RuleSet rs = makeRuleSet("highway=primary {set marker=1}" +
				"marker=2 [0x1]" +
				"highway=primary {set marker=2}" +
				"marker=2 [0x2]");

		Way el = new Way(1);
		el.addTag("highway", "primary");

		GType type = getFirstType(rs, el);
		assertEquals("second marker rule", 2, type.getType());
	}

	@Test
	public void testContinueDefault() {
		RuleSet rs = makeRuleSet("highway=footway {set surface=good;} [0x1 continue]" +
				"surface=good [0x20]" +
				"surface=bad [0x30]");

		Way el = new Way(1);
		el.addTag("highway", "footway");
		el.addTag("surface", "bad");

		List<GType> list = resolveList(rs, el);
		assertEquals("number of lines returned", 2, list.size());
		assertEquals("surface setting not propagated", "bad", el.getTag("surface"));
		assertEquals("result type", 0x30, list.get(1).getType());
	}

	@Test
	public void testContinuePropagate() {
		RuleSet rs = makeRuleSet("highway=footway {set surface=good;} [0x1 continue propagate]" +
				"surface=good [0x20]" +
				"surface=bad [0x30]");

		Way el = new Way(1);
		el.addTag("highway", "footway");
		el.addTag("surface", "bad");

		List<GType> list = resolveList(rs, el);
		assertEquals("number of lines returned", 2, list.size());
		assertEquals("surface setting is propagated", "good", el.getTag("surface"));
		assertEquals("result type", 0x20, list.get(1).getType());
	}

	@Test
	public void testContinueNoPropagate() {
		RuleSet rs = makeRuleSet("highway=footway {set surface=good;} [0x1 continue no_propagate]" +
				"surface=good [0x20]" +
				"surface=bad [0x30]");

		Way el = new Way(1);
		el.addTag("highway", "footway");
		el.addTag("surface", "bad");

		List<GType> list = resolveList(rs, el);
		assertEquals("number of lines returned", 2, list.size());
		assertEquals("surface setting is not propagated", "bad", el.getTag("surface"));
		assertEquals("result type", 0x30, list.get(1).getType());
	}

	@Test
	public void testContinueWithActions() {
		RuleSet rs = makeRuleSet("highway=footway {set surface=good;} [0x1 continue with_actions]" +
				"surface=good [0x20]" +
				"surface=bad [0x30]");

		Way el = new Way(1);
		el.addTag("highway", "footway");
		el.addTag("surface", "bad");

		List<GType> list = resolveList(rs, el);
		assertEquals("number of lines returned", 2, list.size());
		assertEquals("surface setting is propagated", "good", el.getTag("surface"));
		assertEquals("result type", 0x20, list.get(1).getType());
	}

	@Test
	public void testContinueChangesTag() {
		RuleSet rs = makeRuleSet("highway=crossing & crossing=zebra_crossing" +
				"    {set highway=deleted_crossing} [0x10404 resolution 24 continue propagate]" +
				"highway=crossing [0x1010f resolution 24 continue]" +
				"highway=deleted_crossing [0x6 resolution 24 continue]"
		);

		Way el = new Way(1);
		el.addTag("highway", "crossing");
		el.addTag("crossing", "zebra_crossing");

		List<GType> list = resolveList(rs, el);
		assertEquals("first element", 0x10404, list.get(0).getType());
		assertEquals("second element", 0x6, list.get(1).getType());
	}

	@Test
	public void testSetWithTagValue() {
		RuleSet rs = makeRuleSet("a=* {set b='${a}'}" +
				"b=1 [0x10404 resolution 24]" 
		);

		Way el = new Way(1);
		el.addTag("a", "1");

		List<GType> list = resolveList(rs, el);
		assertEquals("first element", 0x10404, list.get(0).getType());
	}

	@Test
	public void testSimulateIf() {
		RuleSet rs = makeRuleSet(	"boundary=administrative { set mkgmap:if:0001=1 }" + 
				"admin_level<3 & mkgmap:if:0001=1  [0x1e resolution 12]" + 
				"mkgmap:if:0001!=1 & admin_level<3 [0x1f resolution 14]" 
		);

		Way el = new Way(1);
		el.addTag("boundary", "xyz"); // anything but administrative
		el.addTag("admin_level", "2");

		List<GType> list = resolveList(rs, el);
		assertEquals("number of elements", 1, list.size());
		assertEquals("first element", 0x1f, list.get(0).getType());
	}
	
	@Test
	public void testIndexWithOddOrder() {
		RuleSet rs = makeRuleSet("a=* {set b=1}" +
				"b=1 {set c=1}" +
				"d=2 {set c=2}" + 
				"c=* {set a=2}" +
				"c=1 {set d=2}" +
				"c=2 {set d=1}" +
				"d=1 [0x10401 resolution 24]" + 
				"d=2 [0x10402 resolution 24]" 
		);
		
		Way el = new Way(1);
		el.addTag("a", "1");
		
		BitSet candidates = rs.getRules(el);
		BitSet expected = new BitSet();
		expected.flip(0,8);
		expected.clear(2);
		expected.clear(5);
		expected.clear(6);
		assertEquals("candidates", expected, candidates);
		List<GType> list = resolveList(rs, el);
		assertEquals("first element", 0x10402, list.get(0).getType());
		
	}
	
	private List<GType> resolveList(RuleSet rs, Way el) {
		final List<GType> list = new ArrayList<>();
		rs.resolveType(el, new TypeResult() {
			public void add(Element el, GType type) {
				list.add(type);
			}
		});
		return list;
	}

	private GType getFirstType(Rule rs, Element el) {
		final List<GType> types = new ArrayList<>();
		rs.resolveType(el, new TypeResult() {
			public void add(Element el, GType type) {
				types.add(type);
			}
		});
		if (types.isEmpty())
			return null;
		else
			return types.get(0);
	}
}
