/*
 * Copyright (C) 2006 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: 01-Jan-2007
 */
package uk.me.parabola.mkgmap;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.EnhancedProperties;

/**
 * Command line arguments for Main.  Arguments consist of options and filenames.
 * You read arguments from left to right and when a filename is encountered
 * the file is processed with the options that were in force at the time.
 * 
 * Since it is likely that the number of options will become quite large, you
 * can place options in a file.  Place the options each on a separate line
 * without the initial '--'.
 *
 * @author Steve Ratcliffe
 */
public class CommandArgsReader {
	private static final Logger log = Logger.getLogger(CommandArgsReader.class);

	private final ArgumentProcessor proc;

	private boolean mapnameWasSet;

	private final ArgList arglist = new ArgList();

	private final EnhancedProperties args = new EnhancedProperties();
	private Set<String> validOptions;

	{
		// Set some default values.  It is as if these were on the command
		// line before any user supplied options.
		add(new CommandOption("mapname", "63240001"));
		add(new CommandOption("description", "OSM street map"));
		add(new CommandOption("overview-mapname", "osmmap"));
		add(new CommandOption("overview-mapnumber", "63240000"));
		add(new CommandOption("poi-address", ""));
		add(new CommandOption("merge-lines", ""));
	}

	public CommandArgsReader(ArgumentProcessor proc) {
		this.proc = proc;
	}

	/**
	 * Read and interpret the command line arguments.  Most have a double hyphen
	 * preceding them and these work just the same if they are in a config
	 * file.
	 * <p/>
	 * There are a few options that consist of a single hyphen followed by a
	 * single letter that are short cuts for a long option.
	 * <p/>
	 * The -c option is special.  It is followed by the name of a file in which
	 * there are further command line options.  Any option on the command line
	 * that comes after the -c option will override the value that is set in
	 * this file.
	 *
	 * @param args The command line arguments.
	 */
	public void readArgs(String[] args) {

		proc.startOptions();

		int i = 0;
		while (i < args.length) {
			String arg = args[i++];
			if (arg.startsWith("--")) {
				// This is a long style 'property' format option.
				addOption(arg.substring(2));

			} else if (arg.equals("-c")) {
				// Config file
				readConfigFile(args[i++]);

			} else if (arg.equals("-n")) {
				// Map name (should be an 8 digit number).
				addOption("mapname", args[i++]);

			} else if (arg.equals("-v")) {
				// make commands more verbose
				addOption("verbose");

			} else if (arg.startsWith("-")) {
				// this is an unrecognised option.
				System.err.println("unrecognised option " + arg);

			} else {
				log.debug("adding filename:", arg);
				add(new Filename(arg));
			}
		}

		// If there is more than one filename argument we inform of this fact
		// via a fake option.
		proc.processOption("number-of-files", String.valueOf(arglist.getFilenameCount()));
		
		// Now process the arguments in order.
		for (ArgType a : arglist) {
			a.processArg();
		}

		proc.endOptions(new CommandArgs(this.args));
	}


	/**
	 * Add an option based on the option and value separately.
	 * @param option The option name.
	 * @param value Its value.
	 */
	private void addOption(String option, String value) {
		CommandOption opt = new CommandOption(option, value);
		addOption(opt);
	}

	/**
	 * Add an option from a raw string.
	 * @param optval The option=value string.
	 */
	private void addOption(String optval) {
		CommandOption opt = new CommandOption(new Option(optval));
		boolean legacyOptionDetected = false; 
		// translate legacy options drive-on-left and drive-on-right
		String option = opt.getOption();
		if ("drive-on-left".equals(option)){
			opt = new CommandOption(new Option("drive-on=left"));
			legacyOptionDetected = true;
		} 
		if ("drive-on-right".equals(option)){
			opt = new CommandOption(new Option("drive-on=right"));
			legacyOptionDetected = true;
		} 
		if (legacyOptionDetected){
			System.err.println("Option " + option + " is deprecated. Will use " + opt.getOption() + "=" + opt.getValue() + " as replacement for " + optval);
		}
		addOption(opt);
	}

	/**
	 * Actually add the option.  Some of these are special in that they are
	 * filename arguments or instructions to read options from another file.
	 *
	 * @param opt The decoded option.
	 */
	private void addOption(CommandOption opt) {
		String option = opt.getOption();
		String value = opt.getValue();

		if (validOptions != null && !validOptions.contains(option) && !opt.isExperimental()) {
			throw new ExitException(String.format("Invalid option: '%s'", option));
		}

		log.debug("adding option", option, value);

		// Note if an explicit mapname is set
		if (option.equals("mapname"))
			mapnameWasSet = true;

		switch (option) {
		case "input-file":
			if (value != null){
				log.debug("adding filename", value);
				add(new Filename(value));
			}
			break;
		case "read-config":
			readConfigFile(value);
			break;
		case "latin1":
			add(new CommandOption("code-page", "1252"));
			break;
		case "unicode":
			add(new CommandOption("code-page", "65001"));
			break;
		default:
			add(opt);
			break;
		}
	}

	private void add(CommandOption option) {
		arglist.add(option);
	}

	private void add(Filename filename) {
		arglist.add(filename);
	}

	/**
	 * Read a config file that contains more options.  When the number of
	 * options becomes large it is more convenient to place them in a file.
	 *
	 * @param filename The filename to obtain options from.
	 */
	private void readConfigFile(String filename) {
		Options opts = new Options(new OptionProcessor() {
			public void processOption(Option opt) {
				log.debug("incoming opt", opt.getOption(), opt.getValue());
				addOption(new CommandOption(opt));
			}
		});
		try {
			opts.readOptionFile(filename);
		} catch (IOException e) {
			throw new ExitException("Failed to read option file", e);
		}
	}

	public void setValidOptions(Set<String> validOptions) {
		this.validOptions = validOptions;
	}

	/**
	 * Interface that represents an argument type.  It provides a method for
	 * the argument to be processed in order.  Options can be interspersed with
	 * filenames.  The options take effect where they appear.
	 */
	interface ArgType {
		public abstract void processArg();
	}

	/**
	 * A filename.
	 */
	class Filename implements ArgType {
		private final String name;
		private boolean useFilenameAsMapname = true;

		private Filename(String name) {
			this.name = name;
			if (mapnameWasSet)
				useFilenameAsMapname = false;
		}

		public void processArg() {
			// If there was no explicit mapname specified and the input filename
			// looks like it contains an 8digit number then we use that.
			String mapname;
			if (useFilenameAsMapname) {
				mapname = extractMapName(name);
				if (mapname != null)
					args.setProperty("mapname", mapname);
			}

			// Now process the file
			proc.processFilename(new CommandArgs(args), name);

			// Increase the name number.  If the next arg sets it then that
			// will override this new name.
			mapname = args.getProperty("mapname");
			try {
				Formatter fmt = new Formatter();
				try {
					int n = Integer.parseInt(mapname);
					fmt.format("%08d", ++n);
				} catch (NumberFormatException e) {
					fmt.format("%8.8s", mapname);
				}
				args.setProperty("mapname", fmt.toString());
				fmt.close();
			} catch (NumberFormatException e) {
				// If the name is not a number then we just leave it alone...
			}
		}

		private String extractMapName(String path) {

			File file = new File(path);
			String fname = file.getName();
			Pattern pat = Pattern.compile("([0-9]{8})");
			Matcher matcher = pat.matcher(fname);
			boolean found = matcher.find();
			if (found)
				return matcher.group(1);

			return null;
		}
	}

	/**
	 * An option argument.  A key value pair.
	 */
	class CommandOption implements ArgType {
		private final Option option;

		private CommandOption(Option option) {
			this.option = option;
		}

		private CommandOption(String key, String val) {
			this.option = new Option(key, val);
		}

		public void processArg() {
			if (option.isReset()) {
				args.remove(option.getOption());
				proc.removeOption(option.getOption());
			} else {
				args.setProperty(option.getOption(), option.getValue());
				proc.processOption(option.getOption(), option.getValue());
			}
		}

		public String getOption() {
			return option.getOption();
		}

		public String getValue() {
			return option.getValue();
		}

		public boolean isExperimental() {
			return option.isExperimental();
		}
	}

	/**
	 * The arguments are held in this list.
	 */
	class ArgList implements Iterable<ArgType> {
		private final List<ArgType> alist;

		private int filenameCount;

		ArgList() {
			alist = new ArrayList<ArgType>();
		}

		protected void add(CommandOption option) {
			alist.add(option);
		}

		public void add(Filename name) {
			filenameCount++;
			alist.add(name);
		}

		public Iterator<ArgType> iterator() {
			return alist.iterator();
		}

		public int getFilenameCount() {
			return filenameCount;
		}
	}
}
