/*
 * Copyright (C) 2017.
 *
 * 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.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import uk.me.parabola.imgfmt.app.mdr.Mdr7;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.scan.TokType;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokenScanner;
import uk.me.parabola.util.EnhancedProperties;

/**
 * Code to add special Garmin separators 0x1b, 0x1e and 0x1f. 
 * The separator 0x1e tells Garmin that the part of the name before that separator
 * should not be displayed when zooming out enough. It is displayed like a blank. 
 * The separator 0x1f tells Garmin that the part of the name after that separator 
 * should not be displayed when zooming out enough. It is displayed like a blank. 
 * The separator 0x1b works like 0x1e, but is not displayed at all.
 * The separator 0x1c works like 0x1f, but is not displayed at all.
 * See also class {@link Mdr7}. 
 * 
 * @author Gerd Petermann
 *
 */
public class PrefixSuffixFilter {
	private static final Logger log = Logger.getLogger(PrefixSuffixFilter.class);

	private static final int MODE_PREFIX = 0;
	private static final int MODE_SUFFIX = 1;
	
	private boolean enabled;
	private final Set<String> languages = new LinkedHashSet<>();
	private final Map<String, List<String>> langPrefixMap = new HashMap<>();
	private final Map<String, List<String>> langSuffixMap = new HashMap<>();
	private final Map<String, List<String>> countryLanguageMap = new HashMap<>();
	private final Map<String, List<String>> countryPrefixMap = new HashMap<>();
	private final Map<String, List<String>> countrySuffixMap = new HashMap<>();

	private EnhancedProperties options = new EnhancedProperties();

	public PrefixSuffixFilter(EnhancedProperties props) {
		String cfgFile = props.getProperty("road-name-config",null);
		enabled = readConfig(cfgFile);
	}

	/**
	 * Read the configuration file for this filter.
	 * @param cfgFile path to file
	 * @return true if filter can be used, else false.
	 */
	private boolean readConfig(String cfgFile) {
		if (cfgFile == null) 
			return false;
		try (InputStreamReader reader = new InputStreamReader(new FileInputStream(cfgFile), "utf-8")) {
			readOptionFile(reader, cfgFile);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage());
			log.error(this.getClass().getSimpleName() + " disabled, failed to read config file " + cfgFile);
			return false;
		}
	}
	
	/**
	 * 
	 * @param r
	 * @param filename
	 */
	private void readOptionFile(Reader r, String filename) {
		BufferedReader br = new BufferedReader(r);
		TokenScanner ts = new TokenScanner(filename, br);
		ts.setExtraWordChars(":");

		while (!ts.isEndOfFile()) {
			Token tok = ts.nextToken();
			if (tok.isValue("#")) {
				ts.skipLine();
				continue;
			}

			String key = tok.getValue();

			ts.skipSpace();
			tok = ts.peekToken();
			
			if (tok.getType() == TokType.SYMBOL) {

				String punc = ts.nextValue();
				String val;
				if (punc.equals(":") || punc.equals("=")) {
					val = ts.readLine();
				} else {
					ts.skipLine();
					continue;
				}
				processOption(key, val);
			} else if (key != null){
				throw new IllegalArgumentException("don't understand line with " + key );
			} else {
				ts.skipLine();
			}
		}
		/**
		 * process lines starting with prefix1 or prefix2. 
		 */
		for (String lang : languages) {
			String prefix1 = options.getProperty("prefix1:" + lang, null);
			if (prefix1 == null)
				continue;
			String prefix2 = options.getProperty("prefix2:" + lang, null);
			List<String> p1 = prefix1 != null ? Arrays.asList(prefix1.split(",")) : Collections.emptyList();
			List<String> p2 = prefix2 != null ? Arrays.asList(prefix2.split(",")) : Collections.emptyList();
			langPrefixMap.put(lang, genPrefix(p1, p2));
		}
	}

	private void processOption(String key, String val) {
		String[] keysParts = key.split(":");
		String[] valParts = val.split(",");
		if (keysParts.length < 2 || val.isEmpty() || valParts.length < 1) {
			throw new IllegalArgumentException("don't understand " + key + " = " + val);
		}
		switch (keysParts[0].trim()) {
		case "prefix1":
		case "prefix2":
			options.put(key, val); // store for later processing
			break;
		case "suffix":
			List<String> suffixes = new ArrayList<>();
			for (String s : valParts) {
				suffixes.add(stripBlanksAndQuotes(s));
			}
			sortByLength(suffixes);
			langSuffixMap.put(keysParts[1].trim(), suffixes);
			break;
		case "lang":
			String iso = keysParts[1].trim();
			List<String> langs = new ArrayList<>();
			for (String lang : valParts) {
				langs.add(lang.trim());
			}
			countryLanguageMap .put(iso, langs);
			languages.addAll(langs);
		default:
			break;
		}
	}
	

	/** Create all combinations of items in prefix1 with items in prefix2 and finally prefix1 with an extra blank.  
	 * @param prefix1 list of prefix words
	 * @param prefix2 list of prepositions
	 * @return all combinations
	 */
	private List<String> genPrefix (List<String> prefix1, List<String> prefix2) {
		List<String> prefixes = new ArrayList<>();
		for (String p1 : prefix1) {
			p1 = stripBlanksAndQuotes(p1);
			for (String p2 : prefix2) {
				p2 = stripBlanksAndQuotes(p2);
				prefixes.add(p1 + " " + p2);
			}
			prefixes.add(p1 + " ");
		}
		return prefixes;
	}

	/**
	 * First remove leading and trailing blanks, next check for paired quotes
	 * @param s the string 
	 * @return the modified string
	 */
	private String stripBlanksAndQuotes(String s) {
		s = s.trim();
		if (s.startsWith("'") && s.endsWith("'") || s.startsWith("\"") && s.endsWith("\"")) {
			return s.substring(1, s.length()-1);
		}
		return s;
	}
	
	
	/**
	 * Modify all labels of a road. Each label is checked against country specific lists of 
	 * well known prefixes (e.g. "Rue de la ", "Avenue des "  ) and suffixes (e.g. " Road").
	 * If a well known prefix is found the label is modified. If the prefix ends with a blank,
	 * that blank is replaced by 0x1e, else 0x1b is added after the prefix.
	 * If a well known suffix is found the label is modified. If the suffix starts with a blank,
	 * that blank is replaced by 0x1f, else 0x1c is added before the suffix.
	 * @param road
	 */
	public void filter(MapRoad road) {
		if (!enabled)
			return;
		String country = road.getCountry();
		if (country == null)
			return;
		
		List<String> prefixesCountry = getSearchStrings(country, MODE_PREFIX);
		List<String> suffixesCountry = getSearchStrings(country, MODE_SUFFIX);
		
		// perform brute force search, seems to be fast enough
		String[] labels = road.getLabels();
		for (int i = 0; i < labels.length; i++) {
			String label = labels[i];
			if (label == null || label.length() == 0)
				continue;
			boolean modified = false;
			for (String prefix : prefixesCountry) {
				if (label.charAt(0) < 7)
					break; // label starts with shield code
				if (label.length() < prefix.length())
					continue;
				if (prefix.equalsIgnoreCase(label.substring(0, prefix.length()))) {
					if (prefix.endsWith(" ")) {
						label = prefix.substring(0, prefix.length() - 1) + (char) 0x1e
								+ label.substring(prefix.length());
					} else {
						label = prefix + (char) 0x1b + label.substring(prefix.length());
					}
					modified = true;
					break;
				}
			}
			for (String suffix : suffixesCountry) {
				int len = label.length();
				if (len < suffix.length())
					continue;
				int pos = len - suffix.length();
				if (suffix.equalsIgnoreCase(label.substring(pos, len))) {
					if (suffix.startsWith(" "))
						label = label.substring(0, pos) + (char) 0x1f + suffix.substring(1);
					else 
						label = label.substring(0, pos) + (char) 0x1c + suffix;
					modified = true;
					break;
				}
			}
			if (modified) {
				labels[i] = label;
				log.debug("modified",label,country,road.getRoadDef());
			}
		}
	}
	
	/**
	 * Build list of prefixes or suffixes for a given country.
	 * @param country String with 3 letter ISO code
	 * @param mode : signals prefix or suffix
	 * @return List with prefixes or suffixes
	 */
	private List<String> getSearchStrings(String country, int mode) {
		Map<String, List<String>>  cache = (mode == MODE_PREFIX) ? countryPrefixMap : countrySuffixMap;
		List<String> res = cache.get(country); 
		if (res == null) {
			// compile the list 
			List<String> languages = countryLanguageMap.get(country);
			if (languages == null)
				res = Collections.emptyList();
			else  {
				List<List<String>> all = new ArrayList<>();
				for (String lang : languages) {
					List<String> prefixes = mode == MODE_PREFIX ? langPrefixMap.get(lang) : langSuffixMap.get(lang);
					if(prefixes != null)
						all.add(prefixes);
				}
				if(all.isEmpty())
					res = Collections.emptyList();
				else if (all.size() == 1) {
					res = all.get(0);
				}
				else {
					Set<String> allPrefixesSet = new HashSet<>();
					for (List<String> prefOneLang : all)
						allPrefixesSet.addAll(prefOneLang);
					res = new ArrayList<>(allPrefixesSet);
					sortByLength(res);
					
				}
			}
			// cache the result
			cache.put(country, res);
		}
		return res;
	}

	/**
	 * Sort by string length so that longest string comes first.
	 * @param strings
	 */
	private void sortByLength(List<String> strings) {
		strings.sort(new Comparator<String>() {
			@Override
			public int compare(String o1, String o2) {
				return Integer.compare(o2.length(), o1.length());
			}
		});
	}
}
