/*
 * Copyright (C) 2008 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 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-Dec-2008
 */
package uk.me.parabola.mkgmap.osmstyle.actions;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import uk.me.parabola.mkgmap.reader.osm.Element;
import uk.me.parabola.mkgmap.scan.SyntaxException;

/**
 * Build a value that can have tag values substituted in it.
 *
 * @author Steve Ratcliffe
 * @author Toby Speight
 */
public class ValueBuilder {
	private static final Pattern[] FILTER_ARG_PATTERNS = {
			Pattern.compile("[ \t]*([^: \\t|]+:\"[^\"]+\")[ \t]*"),
			Pattern.compile("[ \t]*([^: \\t|]+:'[^']+')[ \t]*"),

			// This must be last
			Pattern.compile("[ \t]*([^: \\t|]+:[^|]*)"),
			Pattern.compile("[ \t]*([^: \\t|]+)"),
	};

	private static final Pattern NAME_ARG_SPLIT = Pattern.compile("([^:]+)(?::[\"']?(.*?)[\"']?)?", Pattern.DOTALL);

	private final List<ValueItem> items = new ArrayList<>();
	private final boolean completeCheck;

	public ValueBuilder(String pattern) {
		this (pattern, true);
	}
	
	public ValueBuilder(String pattern, boolean completeCheck) {
		this.completeCheck =completeCheck;
		compile(pattern);
	}

	/**
	 * Build this string if all the tags that are required are available.
	 *
	 * If a tag does not exist then the whole string is rejected.  This allows
	 * you to make conditional replacements.
	 *
	 * @param el Used as a source of tags.
	 * @param lel Used as a source of local tags.
	 * @return The built string if all required tags are available.  If any
	 * are missing then it returns null.
	 */
	public String build(Element el, Element lel) {
		if (completeCheck) {
			// Check early for no match and return early
			for (ValueItem item : items) {
				if (item.getValue(el, lel) == null)
					return null;
			}
		}

		// If we get here we can build the final string.  A common case
		// is that there is just one, so return it directly.
		if (items.size() == 1)
			return items.get(0).getValue(el, lel);

		// OK we have to construct the result.
		StringBuilder sb = new StringBuilder();
		for (ValueItem item : items)
			sb.append(item.getValue(el, lel));

		return sb.toString();
	}

	/**
	 * A tag value can contain variables that are the values of other tags.
	 * This is especially useful for 'name', as you might want to set it to
	 * some combination of other tags.
	 *
	 * If there are no replacement values, the same string as was passed
	 * in.  If all the replacement values exist, then the string with the
	 * values all replaced.  If any replacement tagname does not exist
	 * then returns null.

	 * @param in An input string that may contain tag replacement introduced
	 * by ${tagname}.
	 */
	private void compile(String in) {
		if (!in.contains("$")) {
			items.add(new ValueItem(in));
			return;
		}

		char state = '\0';
		StringBuilder text = new StringBuilder();
		StringBuilder tagname = null;
		for (char c : in.toCharArray()) {
			switch (state) {
			case '\0':
				if (c == '$') {
					state = '$';
				} else {
					text.append(c);
				}
				break;
			case '$':
				switch (c) {
				case '{':
				case '(':
					if (text.length() > 0) {
						items.add(new ValueItem(text.toString()));
						text.setLength(0);
					}
					tagname = new StringBuilder();
					state = (c == '{') ? '}' : ')';
					break;
				default:
					state = '\0';
					text.append('$');
					text.append(c);
				}
				break;
			case '}':
			case ')':
				if (c == state) {
					//noinspection ConstantConditions
					assert tagname != null;
					addTagValue(tagname.toString(), c == ')');
					state = '\0';
					tagname = null;
				} else {
					tagname.append(c);
				}
				break;
			default:
				assert false;
			}
		}

		if (text.length() > 0) 
			items.add(new ValueItem(text.toString()));
	}

	private void addTagValue(String tagname, boolean isLocal) {
		ValueItem item = new ValueItem();
		if (tagname.contains("|")) {
			String[] parts = tagname.split("[ \t]*\\|", 2);
			assert parts.length > 1;

			item.setTagname(parts[0], isLocal);

			String s = parts[1];

			int start = 0;
			int end = s.length();
			while (start < end) {
				Matcher matcher = null;
				for (Pattern p : FILTER_ARG_PATTERNS) {
					matcher = p.matcher(s);
					matcher.region(start, end);
					if (matcher.lookingAt())
						break;
				}

				if (matcher != null && matcher.lookingAt()) {
					start = matcher.end() + 1;
					addFilter(item, matcher.group(1));
				} else {
					assert false;
					start = end;
				}
			}
		} else {
			item.setTagname(tagname, isLocal);
		}
		items.add(item);
	}

	private static void addFilter(ValueItem item, String expr) {
		Matcher matcher = NAME_ARG_SPLIT.matcher(expr);

		matcher.matches();
		String cmd = matcher.group(1);
		String arg = matcher.group(2);

		switch (cmd) {
		case "def":
			item.addFilter(new DefaultFilter(arg));
			break;
		case "conv":
			item.addFilter(new ConvertFilter(arg));
			break;
		case "subst":
			item.addFilter(new SubstitutionFilter(arg));
			break;
		case "prefix":
			item.addFilter(new PrependFilter(arg));
			break;
		case "highway-symbol":
			item.addFilter(new HighwaySymbolFilter(arg));
			break;
		case "height":
			item.addFilter(new HeightFilter(arg));
			break;
		case "not-equal":
			item.addFilter(new NotEqualFilter(arg));
			break;
		case "substring":
			item.addFilter(new SubstringFilter(arg));
			break;
		case "part":
			item.addFilter(new PartFilter(arg));
			break;
		case "ascii":
			item.addFilter(new TransliterateFilter("ascii"));
			break;
		case "latin1":
			item.addFilter(new TransliterateFilter("latin1"));
			break;
		case "country-ISO":
			item.addFilter(new CountryISOFilter());
			break;
		case "not-contained":
			item.addFilter(new NotContainedFilter(arg));
			break;
		default:
			throw new SyntaxException(String.format("Unknown filter '%s'", cmd));
		}
	}

	public String toString() {
		return items.stream().map(ValueItem::toString).collect(Collectors.joining(" | "));
	}

	public Set<String> getUsedTags() {
		return items.stream().map(ValueItem::getTagname).filter(Objects::nonNull).collect(Collectors.toSet());
	}
}
