package de.visone.nlpext.network;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import atomicwheel.Attribute;
import atomicwheel.AttributeManager;
import atomicwheel.Edge;
import atomicwheel.Graph;
import atomicwheel.IDAttribute;
import atomicwheel.Node;
import atomicwheel.why.TransportGraphConverter;
import de.visone.base.Network;
import de.visone.ext.NetworkUtil;
import de.visone.ext.TransportGraphConnector;
import de.visone.ext.nlp.TextNetworkUtil;
import de.visone.ext.nlp.TextPosition;
import de.visone.nlpext.gui.ProgressListener;
import de.visone.nlpext.util.Timer;
import edu.stanford.nlp.ling.HasWord;
import edu.stanford.nlp.ling.Word;
import edu.stanford.nlp.process.Morphology;

/**
 * Abstract class for creating a text network out of a natural language text.
 * Any inheriting text network class has to overwrite the
 * <code>buildNetwork(File file)</code> method, which is called for each
 * selected file by <code>NetworkCreation</code>.
 * 
 * Initialization of a text network (according to the options defined by the
 * creation panel) is done during instantiation (within
 * <code>CreationPanel</code>) of any inheriting text network class.
 * 
 * A <code>edu.stanford.nlp.process.DocumentPreprocessor</code> is used to
 * extract all sentences from each file, which are processed one after the other
 * within the <code>processSentence(...)</code> methods (implemented by
 * inheriting text network classes).
 * 
 * Furthermore, this class provides methods for adding a node by only specifying
 * the word it represents and saving the network and it handles the following
 * attributes: The text the network was created from, words the nodes are
 * representing and the positions the words occur in the text.
 * 
 * @author nick
 */
public abstract class TextNetwork {
	public static final String ATTR_NAME_TIME = "CreationTime";
	public static final String ATTR_NAME_DESCR = "NetworkDescription";
	protected static final String NUMBER_OF_OCCURENCES = "number of occurences";

	// TODO disable logging
	private static final boolean logging = false;

	/**
	 * "Morphology computes the base form of English words, by removing just
	 * inflections (not derivational morphology)."
	 */
	private static Morphology morphology = new Morphology();

	/**
	 * stop words are not allowed to become a label of text network node
	 */
	private static HashSet<String> stopWords;

	/**
	 * minimal length of a word to be used as label
	 */
	private int minWordLength;

	/** Network into which the text-network is build in. */
	private Graph myGraph;

	/**
	 * Reverse of wordNodeMap, maps the corresponding Node to each word it
	 * represents.
	 */
	private Hashtable<String, Node> word2NodeMap;

	/** Stores the words represented by the node */
	private Attribute<Node, String> wordNodeMap;

	/** Replacement for yFiles Node.index(). */
	private IDAttribute<Node> index;

	/** Stores a List of TextPosition at each Node. */
	private Attribute<Node, List<TextPosition>> myTextPositions;
	// to be seen as NodeMap<List<TextPosition>>

	/** ProgressListener that watches the progress of the network-building */
	public ProgressListener progressListener = null;

	/** Text the network is build from as List of Sentences. */
	List<List<? extends HasWord>> myDocument;

	/** Timer for measuring the creation time of the network. */
	protected de.visone.nlpext.util.Timer creationTimer;

	/** Network into which the text-network is build in. */
	protected Network myNetwork;

	protected final boolean doLayout;
	/** Text the network is build from as String. */
	protected StringBuilder myText;
	/** Indicates, if buildNetwork() has been called yet. */
	protected boolean isReadyBuild = false;
	/** String for saving a message for the user */
	public String message;

	protected int[] sentenceBeginnings;
	protected int[][] wordBeginnings;
	protected int totalWordCount;

	// ========== constructor ==========

	/**
	 * Creates a new uninitialized instance of this class. Call
	 * {@link #setNetwork()} to initialize and {@link #buildNetwork(String)} to
	 * create the network.
	 * 
	 * @param layout
	 *            <code>true</code> to do a quick layout after creating the
	 *            network
	 */
	public TextNetwork(final boolean layout) {
		doLayout = layout;
	}

	/**
	 * Makes this class ready to create a network into the given Network-object
	 * out of a text.
	 * 
	 * @author endele
	 */
	public void setNetwork() {
		myGraph = new Graph();
		myTextPositions = new Attribute<Node, List<TextPosition>>(
				LinkedList.class, TextPosition.class);
		wordNodeMap = new Attribute<Node, String>(String.class);
		word2NodeMap = new Hashtable<String, Node>();
		index = new IDAttribute<Node>();
		isReadyBuild = false; // make the network unsaveable by this class
		myText = new StringBuilder();
		totalWordCount = 0;
	}

	/**
	 * enables feedback of the progress and the possibility to display it
	 * 
	 * @param progressListener
	 *            the NetworkCreation object that uses this class
	 */
	public void setProgressListener(final ProgressListener progressListener) {
		this.progressListener = progressListener;
	}

	/**
	 * set method for stop word list
	 * 
	 * @param words
	 */
	public void setStopWords(final String[] words) {
		stopWords = new HashSet<String>();
		for (final String word : words) {
			// System.out.println(words[i]);
			stopWords.add(word);
		}
	}

	// ========== network-building-methods ==========

	/**
	 * Creates a network out of a text file.
	 * 
	 * @param file
	 *            the text file containing the network
	 * */
	public void buildNetwork(final File file) throws IOException {
		buildNetwork(new FileReader(file));
	}

	/**
	 * Creates a network out of a text read from the given reader.
	 * 
	 * @param input
	 *            a reader who already knows the file to process
	 * */
	public void buildNetwork(final Reader input) throws IOException {
		String text = "";
		String line;
		final BufferedReader reader = new BufferedReader(input);
		while ((line = reader.readLine()) != null) {
			text = text + line + "\n";
		}
		reader.close();
		buildNetwork(text);
	}

	/**
	 * Creates a network out of the given text.
	 * 
	 * @param text
	 *            the text to build the network from
	 * */
	public void buildNetwork(final String text) {
		// start measuring of network creation time
		creationTimer = new Timer(true);

		setNetwork();

		final List<List<? extends HasWord>> allSentences = TextPreprocessor
				.getAllSentences(text);
		sentenceBeginnings = new int[allSentences.size()];
		wordBeginnings = new int[allSentences.size()][];

		// iterate over all sentences
		for (int currentSentenceIndex = 0; currentSentenceIndex < allSentences
				.size(); currentSentenceIndex++) {
			// monitor the progress, i.e. proportion of processed sentences
			if (progressListener != null) {
				if (progressListener.isCanceled()) {
					return;
				}

				progressListener.setProgress(currentSentenceIndex,
						allSentences.size());
			}

			// process the next sentence
			final List<? extends HasWord> nextSentence = allSentences
					.get(currentSentenceIndex);
			processSentence(nextSentence, currentSentenceIndex);

			sentenceBeginnings[currentSentenceIndex] = myText.length();
			wordBeginnings[currentSentenceIndex] = new int[nextSentence.size()];
			int currentWordIndex = 0;
			for (final HasWord word : nextSentence) {
				wordBeginnings[currentSentenceIndex][currentWordIndex] = myText
						.length() - sentenceBeginnings[currentSentenceIndex];
				currentWordIndex++;
				myText.append(word);
				myText.append(' ');
				totalWordCount++;
			}
		}

		// finalize network creation
		finalizeCreation();
		addAttributesToGraph();

		createNetworkFromTransportGraph();

		TextNetworkUtil.setRealizers(myNetwork);

		addAttributesToNetwork();
		isReadyBuild = true; // allow this TextNetwork to be saved
	}

	protected void createNetworkFromTransportGraph() {
		myNetwork = TransportGraphConnector.createNetwork(
				TransportGraphConverter.createTransportGraph(myGraph),
				NetworkUtil.NAME_ATTRIBUTE, doLayout);
	}

	/**
	 * Adds DataMap created by this TextNetwork as attributes to the network.
	 * 
	 * @author endele
	 * */
	private void addAttributesToGraph() {

		// TODO
		// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
		// ToDo: zufaellige Vorkommen von XML-Tags im Text irgendwie ersetzen
		// und beim Laden in getText() resubstituieren!
		// //////////////////////////////////////////////////////////////////////////////////////////////////////////////

		// get attribute managers of the network
		final AttributeManager<Node> nodeAttrManager = myGraph
				.getNodeAttributeManager();

		// add the words represented by the nodes as node attribute
		nodeAttrManager.add(TextNetworkUtil.ATTR_NAME_WORD, wordNodeMap);

		// create a NodeMap containing the TextPosition-Lists in a saveable
		// String representation
		final Attribute<Node, String> textPosStringNodeMap = new Attribute<Node, String>(
				String.class);
		// add the number of occurrences of a single word
		final Attribute<Node, Integer> occurrenceCount = nodeAttrManager
				.createAttribute(NUMBER_OF_OCCURENCES, Integer.class);
		for (final Node node : myGraph.getNodes()) {
			final List<TextPosition> textPosList = myTextPositions.get(node);
			textPosStringNodeMap.set(node, TextPosition.toString(textPosList));
			occurrenceCount.set(node, textPosList.size());
		}
		// ...and add it as a node attribute to the network
		nodeAttrManager.add(TextNetworkUtil.ATTR_NAME_POSITIONS,
				textPosStringNodeMap);
	}

	/**
	 * Adds DataMap created by this TextNetwork as attributes to the network.
	 * 
	 * @author endele
	 * */
	private void addAttributesToNetwork() {
		// TODO
		// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
		// ToDo: zufaellige Vorkommen von XML-Tags im Text irgendwie ersetzen
		// und beim Laden in getText() resubstituieren!
		// //////////////////////////////////////////////////////////////////////////////////////////////////////////////

		// text positions of sentences and words
		NetworkUtil.createNetworkAttribute(myNetwork,
				TextNetworkUtil.ATTR_NAME_SENTENCE_BEGINNINGS,
				TextNetworkUtil.formatSentenceBeginnings(sentenceBeginnings),
				"offset at which each sentence starts");
		NetworkUtil.createNetworkAttribute(myNetwork,
				TextNetworkUtil.ATTR_NAME_WORD_BEGINNINGS,
				TextNetworkUtil.formatWordBeginnings(wordBeginnings),
				"offset at which each word starts");

		// add the text this network was created from as global attribute
		NetworkUtil.createNetworkAttribute(myNetwork,
				TextNetworkUtil.ATTR_NAME_TEXT, myText.toString(),
				"The text this network has been created from");

		// add the time the network has been created in as global attribute
		final long millis = creationTimer.getElapsedTime();
		NetworkUtil.createNetworkAttribute(myNetwork, ATTR_NAME_TIME, millis
				+ " ms = " + millisToString(millis),
				"The time this network needed to be created");

		// add a description of this network as global attribute
		NetworkUtil.createNetworkAttribute(myNetwork, ATTR_NAME_DESCR,
				getDescription(),
				"Description of the type of this text network");

		// add the words represented by the nodes as node attribute
		NetworkUtil.setNodeAttributeDescription(myNetwork,
				TextNetworkUtil.ATTR_NAME_WORD,
				"Word a specific node is representing");
		// display this attribute at the nodes
		NetworkUtil.setNodeLabel(myNetwork, TextNetworkUtil.ATTR_NAME_WORD);

		// add the number of occurrences of a single word
		NetworkUtil.setNodeAttributeDescription(myNetwork,
				NUMBER_OF_OCCURENCES,
				"the number of occurences of this word in the text");

		// ...and add it as a node attribute to the network
		NetworkUtil.setNodeAttributeDescription(myNetwork,
				TextNetworkUtil.ATTR_NAME_POSITIONS,
				"position(s) in the text where a specific word occures");
		// register NodeMap textPositions as DataProvider to the Graph2D so that
		// it can be retrieved by getTextPositions()
		// but will not be saved as an attribute (as TextPosition-objects are
		// not a permitted AttributeType for a Network)
		// omit that. getTextPositions() can recompute it from
		// ATTR_NAME_POSITIONS
	}

	// ========== abstract methods ==========

	/** Called for each sentence of a given text when buildNetwork is called. */
	protected abstract void processSentence(List<? extends HasWord> sentence,
			int sentenceIndex);

	/** Called when the last sentence has been processed. */
	protected abstract void finalizeCreation();

	// ========== node/edge-creation-methods ==========

	/** Checks if a word is a stop-word. */
	public static boolean isStopword(final String word) {
		return stopWords.contains(word.toLowerCase());
	}

	/** Stems a given word. */
	public String stem(final String word) {
		return word.contains("_") ? word.toLowerCase() : morphology.stem(
				new Word(word)).word();
	}

	/**
	 * Creates a node identified with the specified label if not yet existing,
	 * otherwise the node which is already identified by the label is returned.
	 * At this point, stemming, handling of synonyms etc. will be handled.
	 * 
	 * @return the (probably created) node identified by the label
	 */
	protected Node addNode(final String word, final TextPosition textPos) {

		if (word == null)
			throw new IllegalArgumentException(
					"Word of TextNetwork Node must not be null!");

		if (textPos == null)
			throw new IllegalArgumentException(
					"Text Position of TextNetwork Node must not be null!");

		// TODO handling of stemming, synonyms etc...
		final String label = stem(word);
		if (logging)
			System.out.print(word + "\t@\t" + textPos.toString() + "\t-->\t"
					+ label);

		// storage of all occurrences of this label
		List<TextPosition> occurrences;

		// check if node for this label exists
		Node node = word2NodeMap.get(label);
		if (node == null) {

			// create new node

			if (logging)
				System.out.print("\tNEW!");

			// create new node
			node = new Node();
			myGraph.add(node);
			index.add(node);

			// map: new label --> new node
			word2NodeMap.put(label, node);

			// map: new node --> new label
			wordNodeMap.set(node, label);

			// create text position list for the new node
			occurrences = new LinkedList<TextPosition>();
			myTextPositions.set(node, occurrences);

			// TODO ???
			// node2LabelMap.put(node,label);
			// myNetwork.getGraph2D().setLabelText(node,label);

		} else {
			// a node for current label already exists
			occurrences = myTextPositions.get(node);
		}

		// associate new TextPosition with node
		occurrences.add(textPos);

		if (logging) {

			for (int i = 0; i < occurrences.size(); i++)
				System.out.print("\t" + occurrences.get(i).toString());

			System.out.println("");
		}

		return node;
	}

	/**
	 * Creates an edge between <code>n1</code> and <code>n2</code>. Loops are
	 * avoided, i.e. null will be returned and no edge be created if
	 * <code>n1==n2</code>.
	 * 
	 * @return the created edge, or null if
	 *         <code>n1==null || n2==null || n1==n2</code>
	 */
	protected Edge addEdge(Node n1, Node n2) {

		if (n1 == null || n2 == null) {
			return null;
		} else if (n1 == n2) { // avoid loops
			return null;
		} else {

			if (logging)
				System.out.println("EDGE:\t\t" + wordNodeMap.get(n1)
						+ "\t---\t" + wordNodeMap.get(n2));

			if (index.get(n1) > index.get(n2)) {
				// swap nodes, so that i<j is true for all edges (i,j). That is,
				// no node-pair i,j has two edges (i,j), (j,i).
				final Node tmpNode = n1;
				n1 = n2;
				n2 = tmpNode;
			}

			// TODO multiplicity directly calculating
			/*
			 * Edge edge = n1.getEdge(n2); int multiplicity = 1; if (edge==null)
			 * { edge = myNetwork.createEdge(n1, n2, false); //create undirected
			 * edge } else { multiplicity =
			 * edgeMultiplicities.get(edge).intValue() + 1; }
			 * edgeMultiplicities.put(edge, new Integer(multiplicity));
			 */

			// create directed edge
			final Edge edge = new Edge(n1, n2, true);
			myGraph.add(edge);
			return edge;
		}
	}

	/**
	 * Connects all given nodes to one another.
	 * 
	 * @param nodes
	 *            list of nodes, should be backed by a random access data store
	 * 
	 * @author endele
	 */
	protected void buildClique(final List<Node> nodes) {

		for (int i = 0; i < nodes.size(); i++) {
			for (int j = i + 1; j < nodes.size(); j++) {
				addEdge(nodes.get(i), nodes.get(j));
			}
		}
	}

	// ========== get-methods ==========

	public int getMinWordLength() {
		return minWordLength;
	}

	protected void setMinWordLength(final int length) {
		minWordLength = length;
	}

	/**
	 * Returns the text of a TextNetwork if one was registered to it by this
	 * class.
	 * 
	 * @author endele
	 */
	public String getText() {
		return myText.toString();
	}

	public boolean isReadyBuild() {
		return isReadyBuild;
	}

	/** Returns a description of this TextNetwork (e.g. type, used tools). */
	protected abstract String getDescription();

	/** Returns the network held by this instance. */
	public Network getNetwork() {
		return myNetwork;
	}

	public static String millisToString(final long millis) {
		final String hours = "" + millis / 3600000;
		String mins = "" + millis / 60000 % 60;
		String secs = "" + millis / 1000 % 60;
		String mils = "" + millis % 1000;
		while (mins.length() < 2)
			mins = "0" + mins;
		while (secs.length() < 2)
			secs = "0" + secs;
		while (mils.length() < 3)
			mils = "0" + mils;
		return hours + ":" + mins + ":" + secs + "." + mils + " s";
	}
}