package com.github.markusbernhardt.xmldoclet;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.markusbernhardt.xmldoclet.xjc.Root;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.RootDoc;

/**
 * Doclet class.
 * 
 * @author markus
 */
public class XmlDoclet {

	private final static Logger log = LoggerFactory.getLogger(Parser.class);

	/**
	 * The parsed object model. Used in unit tests.
	 */
	public static Root root;

	/**
	 * The Options instance to parse command line strings.
	 */
	public final static Options options;

	static {
		options = new Options();

		OptionBuilder.withArgName("directory");
		OptionBuilder.isRequired(false);
		OptionBuilder.hasArg();
		OptionBuilder.withDescription("Destination directory for output file.\nDefault: .");
		options.addOption(OptionBuilder.create("d"));

		OptionBuilder.withArgName("encoding");
		OptionBuilder.isRequired(false);
		OptionBuilder.hasArg();
		OptionBuilder.withDescription("Encoding of the output file.\nDefault: UTF8");
		options.addOption(OptionBuilder.create("docencoding"));

		OptionBuilder.withArgName("dryrun");
		OptionBuilder.isRequired(false);
		OptionBuilder.hasArgs(0);
		OptionBuilder.withDescription("Parse javadoc, but don't write output file.\nDefault: false");
		options.addOption(OptionBuilder.create("dryrun"));

		OptionBuilder.withArgName("filename");
		OptionBuilder.isRequired(false);
		OptionBuilder.hasArg();
		OptionBuilder.withDescription("Name of the output file.\nDefault: javadoc.xml");
		options.addOption(OptionBuilder.create("filename"));
	}

	/**
	 * Check for doclet-added options. Returns the number of arguments you must
	 * specify on the command line for the given option. For example, "-d docs"
	 * would return 2.
	 * <P>
	 * This method is required if the doclet contains any options. If this
	 * method is missing, Javadoc will print an invalid flag error for every
	 * option.
	 * 
	 * @see com.sun.javadoc.Doclet#optionLength(String)
	 * 
	 * @param optionName
	 *            The name of the option.
	 * @return number of arguments on the command line for an option including
	 *         the option name itself. Zero return means option not known.
	 *         Negative value means error occurred.
	 */
	public static int optionLength(String optionName) {
		Option option = options.getOption(optionName);
		if (option == null) {
			return 0;
		}
		return option.getArgs() + 1;
	}

	/**
	 * Check that options have the correct arguments.
	 * <P>
	 * This method is not required, but is recommended, as every option will be
	 * considered valid if this method is not present. It will default
	 * gracefully (to true) if absent.
	 * <P>
	 * Printing option related error messages (using the provided
	 * DocErrorReporter) is the responsibility of this method.
	 * 
	 * @see com.sun.javadoc.Doclet#validOptions(String[][], DocErrorReporter)
	 * 
	 * @param optionsArrayArray
	 *            The two dimensional array of options.
	 * @param reporter
	 *            The error reporter.
	 * 
	 * @return <code>true</code> if the options are valid.
	 */
	public static boolean validOptions(String optionsArrayArray[][], DocErrorReporter reporter) {
		return null != parseCommandLine(optionsArrayArray);
	}

	/**
	 * Processes the JavaDoc documentation.
	 * <p>
	 * This method is required for all doclets.
	 * 
	 * @see com.sun.javadoc.Doclet#start(RootDoc)
	 * 
	 * @param rootDoc
	 *            The root of the documentation tree.
	 * 
	 * @return <code>true</code> if processing was successful.
	 */
	public static boolean start(RootDoc rootDoc) {
		CommandLine commandLine = parseCommandLine(rootDoc.options());
		Parser parser = new Parser();
		root = parser.parseRootDoc(rootDoc);
		save(commandLine, root);
		return true;
	}

	/**
	 * Save XML object model to a file via JAXB.
	 * 
	 * @param commandLine
	 *            the parsed command line arguments
	 * @param root
	 *            the document root
	 */
	public static void save(CommandLine commandLine, Root root) {
		if (commandLine.hasOption("dryrun")) {
			return;
		}

		FileOutputStream fileOutputStream = null;
		BufferedOutputStream bufferedOutputStream = null;
		try {
			JAXBContext contextObj = JAXBContext.newInstance(Root.class);

			Marshaller marshaller = contextObj.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			if (commandLine.hasOption("docencoding")) {
				marshaller.setProperty(Marshaller.JAXB_ENCODING, commandLine.getOptionValue("docencoding"));
			}

			String filename = "javadoc.xml";
			if (commandLine.hasOption("filename")) {
				filename = commandLine.getOptionValue("filename");
			}
			if (commandLine.hasOption("d")) {
				filename = commandLine.getOptionValue("d") + File.separator + filename;
			}

			fileOutputStream = new FileOutputStream(filename);
			bufferedOutputStream = new BufferedOutputStream(fileOutputStream, 1024 * 1024);

			marshaller.marshal(root, bufferedOutputStream);
			bufferedOutputStream.flush();
			fileOutputStream.flush();

		} catch (JAXBException e) {
			log.error(e.getMessage(), e);
		} catch (FileNotFoundException e) {
			log.error(e.getMessage(), e);
		} catch (IOException e) {
			log.error(e.getMessage(), e);
		} finally {
			try {
				if (bufferedOutputStream != null) {
					bufferedOutputStream.close();
				}
				if (fileOutputStream != null) {
					fileOutputStream.close();
				}
			} catch (IOException e) {
				log.error(e.getMessage(), e);
			}
		}
	}

	/**
	 * Return the version of the Java Programming Language supported by this
	 * doclet.
	 * <p>
	 * This method is required by any doclet supporting a language version newer
	 * than 1.1.
	 * <p>
	 * This Doclet supports Java 5.
	 * 
	 * @see com.sun.javadoc.Doclet#languageVersion()
	 * 
	 * @return LanguageVersion#JAVA_1_5
	 */
	public static LanguageVersion languageVersion() {
		return LanguageVersion.JAVA_1_5;
	}

	/**
	 * Parse the given options.
	 * 
	 * @param optionsArrayArray
	 *            The two dimensional array of options.
	 * @return the parsed command line arguments.
	 */
	public static CommandLine parseCommandLine(String[][] optionsArrayArray) {
		try {
			List<String> argumentList = new ArrayList<String>();
			for (String[] optionsArray : optionsArrayArray) {
				argumentList.addAll(Arrays.asList(optionsArray));
			}

			CommandLineParser commandLineParser = new BasicParser() {
				@Override
				protected void processOption(final String arg, @SuppressWarnings("rawtypes") final ListIterator iter)
						throws ParseException {
					boolean hasOption = getOptions().hasOption(arg);
					if (hasOption) {
						super.processOption(arg, iter);
					}
				}
			};
			CommandLine commandLine = commandLineParser.parse(options, argumentList.toArray(new String[] {}));
			return commandLine;
		} catch (ParseException e) {
			LoggingOutputStream loggingOutputStream = new LoggingOutputStream(log, LoggingLevelEnum.INFO);
			PrintWriter printWriter = new PrintWriter(loggingOutputStream);

			HelpFormatter helpFormatter = new HelpFormatter();
			helpFormatter.printHelp(printWriter, 74, "javadoc -doclet " + XmlDoclet.class.getName() + " [options]",
					null, options, 1, 3, null, false);
			return null;
		}
	}
}