/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License") +  you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openmeetings.cli;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;

public class OmHelpFormatter extends HelpFormatter {
	private static final String GENERAL_OPTION_GROUP = "";
	private int maxPrefixLength = 0;

	@SuppressWarnings("unchecked")
	private static List<OmOption> getReqOptions(Options opts) {
		//suppose we have only 1 group (for now)
		OptionGroup g = ((List<OptionGroup>)opts.getRequiredOptions()).get(0);
		List<OmOption> result = new ArrayList<>();
		for (Option o : g.getOptions()) {
			result.add((OmOption)o);
		}
		Collections.sort(result, (o1, o2) -> o1.getOrder() - o2.getOrder());
		return result;
	}

	private Map<String, List<OmOption>> getOptions(Options opts, int leftPad) {
		final String longOptSeparator = " ";
		final String lpad = createPadding(leftPad);
		final String lpadParam = createPadding(leftPad + 2);
		List<OmOption> reqOptions = getReqOptions(opts);
		Map<String, List<OmOption>> map = new LinkedHashMap<>(reqOptions.size());
		map.put(GENERAL_OPTION_GROUP, new ArrayList<>());
		for (OmOption o : reqOptions) {
			map.put(o.getOpt(), new ArrayList<>());
		}
		for (Option option : opts.getOptions()) {
			OmOption o = (OmOption)option;
			boolean skipOption = map.containsKey(o.getOpt());
			boolean mainOption = skipOption || o.getGroup() == null;

			// first create list containing only <lpad>-a,--aaa where
			// -a is opt and --aaa is long opt; in parallel look for
			// the longest opt string this list will be then used to
			// sort options ascending
			StringBuilder optBuf = new StringBuilder();
			if (o.getOpt() == null) {
				optBuf.append(mainOption ? lpad : lpadParam).append("   ").append(getLongOptPrefix())
						.append(o.getLongOpt());
			} else {
				optBuf.append(mainOption ? lpad : lpadParam).append(getOptPrefix())
						.append(o.getOpt());

				if (o.hasLongOpt()) {
					optBuf.append(',').append(getLongOptPrefix())
							.append(o.getLongOpt());
				}
			}

			if (o.hasArg()) {
				String argName = o.getArgName();
				if (argName != null && argName.length() == 0) {
					// if the option has a blank argname
					optBuf.append(' ');
				} else {
					optBuf.append(o.hasLongOpt() ? longOptSeparator : " ");
					optBuf.append("<")
							.append(argName != null ? o.getArgName()
									: getArgName()).append(">");
				}
			}

			o.setHelpPrefix(optBuf);
			maxPrefixLength = Math.max(optBuf.length(), maxPrefixLength);

			if (skipOption) {
				continue;
			}
			String grp = o.getGroup();
			grp = grp == null ? GENERAL_OPTION_GROUP : grp;
			String[] grps = grp.split(",");
			for(String g : grps) {
				map.get(g).add(o);
			}
		}
		for (Map.Entry<String, List<OmOption>> me : map.entrySet()) {
			final String key = me.getKey();
			List<OmOption> options = me.getValue();
			Collections.sort(options, (o1, o2) -> {
					boolean o1opt = !o1.isOptional(key);
					boolean o2opt = !o2.isOptional(key);
					return (o1opt && o2opt || !o1opt && !o2opt) ? (o1.getOpt() == null ? 1 : -1) : (o1opt ? -1 : 1);
				});
			if (opts.hasOption(key)) {
				options.add(0, (OmOption)opts.getOption(key));
			}
		}
		return map;
	}

	private static StringBuilder getReqOptionsString(Options opts) {
		String delim = "";
		StringBuilder result = new StringBuilder();
		for (Option o : getReqOptions(opts)) {
			result.append(delim).append("-").append(o.getOpt());
			delim = "|";
		}
		return result;
	}

	@Override
	protected StringBuffer renderOptions(StringBuffer sb, int width, Options options, int leftPad, int descPad) {
		final String dpad = createPadding(descPad);
		final String optional = "(optional) ";

		Map<String, List<OmOption>> optList = getOptions(options, leftPad);

		char[] delimiter = new char[width - 2];
		Arrays.fill(delimiter, '-');

		for (Entry<String, List<OmOption>> me : optList.entrySet()) {
			if (GENERAL_OPTION_GROUP.equals(me.getKey())) {
				sb.append("General options:").append(getNewLine());
			}
			for (OmOption option : me.getValue()) {
				StringBuilder optBuf = new StringBuilder(option.getHelpPrefix());

				if (optBuf.length() < maxPrefixLength) {
					optBuf.append(createPadding(maxPrefixLength - optBuf.length()));
				}

				optBuf.append(dpad);

				int nextLineTabStop = maxPrefixLength + descPad;

				if (option.isOptional(me.getKey())) {
					optBuf.append(optional);
				}
				if (option.getDescription() != null) {
					optBuf.append(option.getDescription());
				}

				renderWrappedText(sb, width, nextLineTabStop, optBuf.toString());

				sb.append(getNewLine());
			}
			sb.append(delimiter).append(getNewLine());
		}
		return sb;
	}

	@Override
	public void printHelp(PrintWriter pw, int width, String cmdLineSyntax,
			String header, Options options, int leftPad, int descPad,
			String footer, boolean autoUsage) {
		if ((cmdLineSyntax == null) || (cmdLineSyntax.length() == 0)) {
			throw new IllegalArgumentException("cmdLineSyntax not provided");
		}

		printUsage(pw, width, cmdLineSyntax, options);

		if ((header != null) && (header.trim().length() > 0)) {
			printWrapped(pw, width, header);
		}

		printOptions(pw, width, options, leftPad, descPad);

		if ((footer != null) && (footer.trim().length() > 0)) {
			printWrapped(pw, width, footer);
		}
	}

	@Override
	public void printUsage(PrintWriter pw, int width, String app, Options opts) {
		pw.println(String.format("usage: %1$s [%2$s] [options]", app, getReqOptionsString(opts)));
	}
}
