import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.util.HashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.log4j.Logger;
import org.w3c.dom.Element;

import de.visone.base.Network;
import de.visone.ext.script.InvalidScriptException;
import de.visone.ext.script.Script;
import de.visone.ext.script.ScriptAlgo;
import de.visone.gui.tabs.AbstractAlgorithmCard;
import de.visone.gui.tabs.AbstractTabCard;
import de.visone.nlpext.NLPExtension;
import de.visone.nlpext.gui.CRACard;
import de.visone.nlpext.gui.NLPCard;
import de.visone.nlpext.gui.NLPPanel;
import de.visone.nlpext.gui.WNACard;
import de.visone.nlpext.network.TextNetwork;
import de.visone.nlpext.util.HeadlessMediator;

/**
 * Simple tool to batch-create text networks.
 * 
 * @author matthias
 */
public class Batch {
	private static final String ERROR_SCRIPTFORMAT = "script must contain exactly one step, either CRA or WNA";
	private static Logger logger = Logger.getLogger(Batch.class);

	private final HeadlessMediator mediator;
	private final NLPPanel algo;

	public Batch(final String algorithm) throws FileNotFoundException,
			InvalidScriptException {
		mediator = new HeadlessMediator();

		if (algorithm.equalsIgnoreCase("wna"))
			algo = new WNACard(mediator);
		else if (algorithm.equalsIgnoreCase("cra"))
			algo = new CRACard(mediator);
		else if (algorithm.endsWith(".xml"))
			algo = loadScript(algorithm);
		else
			throw new IllegalArgumentException(algorithm
					+ " is neither an algorithm name nor an XML script");

		algo.getParameterPanel();
	}

	private NLPPanel loadScript(final String algorithm)
			throws InvalidScriptException, FileNotFoundException {
		// only support the "text" tab, but also do so if the extension isn't
		// loaded
		final NLPCard textTab = new NLPCard(mediator);
		final HashMap<String, AbstractTabCard> root = new HashMap<String, AbstractTabCard>();
		root.put(textTab.getCardName(), textTab);
		final ByteArrayInputStream stream = patchVersion(new FileInputStream(
				algorithm));
		final Script script = new Script(root, stream);

		// check that there is exactly one NLP algorithm in the script
		if (script.size() != 1)
			throw new InvalidScriptException(ERROR_SCRIPTFORMAT);
		final ScriptAlgo firstScript = script.get(0);
		final AbstractAlgorithmCard card = firstScript.getCard();
		if (!(card instanceof NLPPanel))
			throw new InvalidScriptException(ERROR_SCRIPTFORMAT);

		// load settings from xml. annoyingly this includes the source text
		// file, which is not used anywhere because the filename is passed
		// by the batch processing code, so it can be anything, but
		// unfortunately it still has to exist else loading of the XML
		// fails.
		firstScript.loadOptions();
		return (NLPPanel) card;
	}

	/**
	 * Changes the version tag in scripts so they match the visone version. This
	 * is much less silly than re-releasing the 30MB package for every version
	 * because of a 2-character version change.
	 * <p>
	 * Should be safe to do here because it only uses the "text" tab anyway,
	 * which, when it changes, changes completely independently of any visone
	 * versions.
	 */
	private ByteArrayInputStream patchVersion(final InputStream script) {
		try {
			// read XML
			final DocumentBuilder docBuilder = DocumentBuilderFactory
					.newInstance().newDocumentBuilder();
			final Element root = docBuilder.parse(script).getDocumentElement();
			// change version
			root.setAttribute(ScriptAlgo.VERSION,
					NLPExtension.getVisoneVersion());
			// write xml
			final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
			final Transformer trafo = TransformerFactory.newInstance()
					.newTransformer();
			trafo.transform(new DOMSource(root), new StreamResult(buffer));
			return new ByteArrayInputStream(buffer.toByteArray());
		} catch (final Exception e) {
			throw new RuntimeException("failed to patch version number", e);
		}
	}

	public void processFile(final File textFile, final File outputFile) {
		try {
			// create text network. this overrides the name of the source file
			logger.info("processing " + textFile.getName());
			final TextNetwork textNet = algo.getTextNetwork();
			textNet.buildNetwork(textFile);
			final Network network = textNet.getNetwork();

			// write to graphml and close the newly create network
			mediator.getIOManager().write(outputFile,
					mediator.getBundle(network));
			logger.info("wrote " + outputFile.getName());
			mediator.disposeNetwork(network);
		} catch (final Throwable e) {
			// ON ERROR RESUME NEXT
			// next file actually, so this isn't quite as bad
			logger.error(textFile.getName() + " failed with " + e.toString(), e);
		}
	}

	private static void usage() {
		System.err.println("usage: java -jar visone-nlp.jar"
				+ " Batch algorithm input [output]");
		System.err.println("creates text networks for all input files,"
				+ " sliently overwriting existing files.");
		System.err.println();
		System.err.println("input and output can be files or directories.\n"
				+ "if input is a directory, all .txt files in"
				+ " that directory are processed.\n"
				+ "if output is a directory, files are created in it"
				+ " with .txt replaced by .graphml.\n"
				+ "if output is a omitted,"
				+ " files are placed in the input directory instead.\n"
				+ "combining an input directory with an output file"
				+ " isn't allowed for obvious reasons.");
		System.err.println();
		System.err.println("valid algoithms are 'cra' and 'wna',"
				+ " or the name of a .xml visone script file\n"
				+ "containing a single recorded operation"
				+ " in the 'text' tab.");
		System.err.println();
		System.err.println("Stanford CoreNLP may require an additional"
				+ " -Xmx2g to avoid running out of memory.");
		System.exit(1);
	}

	public static void main(final String... args)
			throws InvalidScriptException, FileNotFoundException {
		if (args.length != 2 && args.length != 3)
			usage();
		final String algorithm = args[0];
		final File input = new File(args[1]);
		final File output;
		if (args.length != 2)
			output = new File(args[2]);
		else if (input.isDirectory())
			output = input;
		else
			output = input.getParentFile();

		if (input.isDirectory() && output.isDirectory()) {
			final Batch b = new Batch(algorithm);
			for (final File textFile : input.listFiles(new TextFileFilter()))
				b.process(textFile, output);
		} else if (input.isFile()) {
			final Batch b = new Batch(algorithm);
			b.process(input, output);
		} else
			usage();
	}

	private void process(final File input, final File output) {
		final File outputFile;
		if (output.isDirectory()) {
			final String basename = input.getName().replaceFirst(".txt$", "");
			outputFile = new File(output, basename + ".graphml");
		} else
			outputFile = output;
		processFile(input, outputFile);
	}

	private static class TextFileFilter implements FilenameFilter {
		@Override
		public boolean accept(final File dir, final String name) {
			return name.endsWith(".txt");
		}
	}
}
