package com.monead.semantic.education;

/**
 * DiffInferencing - Show inferencing at different levels of semantic reasoning
 * 
 * This program uses Jena and Pellet to show the progression of inferencing
 * that occurs for the provided ontology.
 * 
 *    Copyright (C) 2010 David S. Read
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    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 Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *    
 *    For information on Jena: http://jena.sourceforge.net/
 *    For information on Pellet: http://clarkparsia.com/pellet
 *    
 * This program extends the concepts introduced in: 
 *   Hebeler, John. "Modeling Knowledge in the Real World." 
 *   		Semantic Web Programming. Indianapolis, IN: Wiley, 
 *   		2009. 166-67. Print.
 */

import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.mindswap.pellet.jena.PelletReasonerFactory;

import com.hp.hpl.jena.ontology.Individual;
import com.hp.hpl.jena.ontology.OntModel;
import com.hp.hpl.jena.ontology.OntModelSpec;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.reasoner.Reasoner;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;

public class DiffInferencing implements Runnable {
	/**
	 * The version identifier
	 */
	public final static String VERSION = "1.0";
	
	/**
	 * The set of formats that can be loaded. These are defined by Jena
	 */
	private final static String[] FORMATS = { "N3", "N-Triples", "RDF/XML",
			"Turtle" };

	/**
	 * The set of reasoning levels that will be compared.
	 */
	protected final static String[] REASONING_LEVELS = { "none", "rdfs", "owl" };
	
	/**
	 * A default file to process - in case one is not supplied
	 * on the command line
	 */
	private final static String DEFAULT_INPUT_FILE = "DSRDiffInferencingTestOntology.turtle";

	/**
	 * Constant used if a value cannot be found in an array
	 */
	private final static int UNKNOWN = -1;

	/**
	 * The name (and path if necessary) to the ontology being loaded
	 */
	private String inputFileName;

	/**
	 * The name (and path if necessary) to the output file for the diff report
	 */
	private String outputFileName;

	/**
	 * The loaded ontology
	 */
	private OntModel ontModel;

	/**
	 * Map of the text values in the model First map is keyed on reasoner Inner
	 * map is keyed on the subject Innermost List contains a collection of
	 * 2-element String array String arrays contain the predicate [0] and the
	 * object [1]
	 */
	private Map<String, Map<String, List<String[]>>> triples;

	/**
	 * Constructor - sets up the input and output file paths and the triples map
	 * 
	 * @param inputFileName The name (and optional path) to an ontology
	 * @param outputFileName The name (and optional path) for the output
	 */
	public DiffInferencing(String inputFileName, String outputFileName) {
		setInputFileName(inputFileName);
		setOutputFileName(outputFileName);
		triples = new HashMap<String, Map<String, List<String[]>>>();
	}

	/**
	 * Perform the steps to load, compare and report on the ontology
	 */
	public void run() {
		for (String reasoner : REASONING_LEVELS) {
			System.out.println("Load model with reasoner: " + reasoner);
			loadModel(reasoner);
			storeModel(reasoner);
		}

		reportTriples("All Assertions at this Level", false);

		diffMaps();
		reportTriples("Assertions Unique at this Level", true);
	}

	/**
	 * Place the triples in the model into the map for the supplied reasoner
	 * 
	 * @param reasoner The reasoning level being used
	 */
	private void storeModel(String reasoner) {
		triples.put(reasoner, getTriples());
	}

	/**
	 * Perform the comparison between the sets of triples for each reasoning
	 * level. The algorithm walks through the reasoners backward since triples
	 * are removed if they are found in an earlier reasoner's triples.
	 */
	private void diffMaps() {
		for (int reasonerIndex = REASONING_LEVELS.length - 1; 
				reasonerIndex > 0; 
				--reasonerIndex) {
			diffMaps(REASONING_LEVELS[reasonerIndex],
					REASONING_LEVELS[reasonerIndex - 1]);
		}
	}

	/**
	 * Update the superset by removing anything that is also in the subset e.g.
	 * at the end of processing, the superset should be disjoint from the subset
	 * 
	 * @param superset Reasoner level being checked for duplicates entries
	 * @param subset Reasoner level whose values should not be in the superset
	 */
	private void diffMaps(String superset, String subset) {
		Map<String, List<String[]>> supersetMap;
		Map<String, List<String[]>> subsetMap;
		List<String[]> supersetSubjectAssertions;
		List<String[]> subsetSubjectAssertions;
		String[] supersetAssertion;
		List<String> subjectsToRemoveFromSuperset;

		supersetMap = triples.get(superset);
		subsetMap = triples.get(subset);
		subjectsToRemoveFromSuperset = new ArrayList<String>();

		for (String subject : supersetMap.keySet()) {
			supersetSubjectAssertions = supersetMap.get(subject);
			subsetSubjectAssertions = subsetMap.get(subject);

			if (subsetSubjectAssertions != null) {
				for (String[] subsetAssertion : subsetSubjectAssertions) {
					for (int supersetAssertionIndex = 0; 
							supersetAssertionIndex < 
							supersetSubjectAssertions.size(); 
							++supersetAssertionIndex) {
						supersetAssertion = supersetSubjectAssertions
								.get(supersetAssertionIndex);
						if (supersetAssertion[0].equals(subsetAssertion[0])
								&& supersetAssertion[1]
										.equals(subsetAssertion[1])) {
							supersetSubjectAssertions
									.remove(supersetAssertionIndex);
							break;
						}
					}
				}
				if (supersetSubjectAssertions.size() > 0) {
					supersetMap.put(subject, supersetSubjectAssertions);
				} else {
					/**
					 * Removal here can lead to ConcurrentModificationException
					 */
					subjectsToRemoveFromSuperset.add(subject);
				}
			}
		}

		// remove any subjects that have no associated assertions
		for (String subject : subjectsToRemoveFromSuperset) {
			supersetMap.remove(subject);
		}
	}

	/**
	 * Write the resulting triples out to a file, separated by the reasoning
	 * level where that triple was first found
	 * 
	 * @param title The text to use as the title of the triples being output
	 * @param append Whether to append to an existing file (overwrite if false)
	 */
	private void reportTriples(String title, boolean append) {
		String outputFileName;
		PrintWriter out;
		Map<String, List<String[]>> reasonerTriples;
		List<String[]> subjectAssertions;

		outputFileName = getOutputFileName();
		out = null;

		try {
			out = new PrintWriter(new FileWriter(outputFileName, append));
		} catch (Throwable throwable) {
			System.err.println("Failed to open output file: " + outputFileName);
			throwable.printStackTrace();
			System.exit(5);
		}

		for (String reasoner : REASONING_LEVELS) {
			out.println(title + " (Reasoning Level: " + reasoner + ")\n");
			reasonerTriples = triples.get(reasoner);
			for (String subject : reasonerTriples.keySet()) {
				out.println("  Individual: " + subject);
				subjectAssertions = reasonerTriples.get(subject);
				for (String assertion[] : subjectAssertions) {
					out.println("    " + assertion[0] + ": " + assertion[1]);
				}
				out.println();
			}
			out.println();
		}

		System.out.println("Wrote triples to " + outputFileName);

		try {
			out.close();
		} catch (Throwable throwable) {
			System.err.println("Error closing output file");
			throwable.printStackTrace();
			System.exit(6);
		}

	}

	/**
	 * Get the set of defined ontology file formats that the program can load as
	 * a CSV list String
	 * 
	 * @return The known ontology file formats as a CSV list
	 */
	public final static String getFormatsAsCSV() {
		return getArrayAsCSV(FORMATS);
	}

	/**
	 * Get the set of reasoning levels that the program will use as a CSV list
	 * String
	 * 
	 * @return The known reasoning levels as a CSV list
	 */
	public final static String getReasoningLevelsAsCSV() {
		return getArrayAsCSV(REASONING_LEVELS);
	}

	/**
	 * Create a CSV list from a String array
	 * 
	 * @param array An array
	 * @return The array values in a CSV list
	 */
	public final static String getArrayAsCSV(String[] array) {
		StringBuffer csv;

		csv = new StringBuffer();

		for (String value : array) {
			if (csv.length() > 0) {
				csv.append(", ");
			}
			csv.append(value);
		}

		return csv.toString();

	}

	/**
	 * Set the input file name, where the ontology is located
	 * 
	 * @param inputFileName The name of the file containing the ontology
	 */
	public void setInputFileName(String inputFileName) {
		this.inputFileName = inputFileName;
	}

	/**
	 * Get the input file name for the location of the ontology
	 * 
	 * @return The input file name where the ontology is located
	 */
	public String getInputFileName() {
		return inputFileName;
	}

	/**
	 * Set the output file name, where the report should be written
	 * 
	 * @param outputFileName The output file name
	 */
	public void setOutputFileName(String outputFileName) {
		this.outputFileName = outputFileName;
	}

	/**
	 * Get the output file name for the location of the generated report
	 * 
	 * @return The output file name
	 */
	public String getOutputFileName() {
		return outputFileName;
	}

	/**
	 * Convert the ontology into a set of Strings representing the triples
	 * 
	 * @return A Map containing Lists that relate subjects to objects and
	 *         predicates
	 */
	protected Map<String, List<String[]>> getTriples() {
		Map<String, List<String[]>> triples;
		List<String[]> oneSubject;
		String subject;
		String[] predicateObject;
		ExtendedIterator<Individual> iIndividuals;
		StmtIterator iProperties;

		triples = new HashMap<String, List<String[]>>();

		iIndividuals = ontModel.listIndividuals();

		while (iIndividuals.hasNext()) {
			Individual individual = iIndividuals.next();
			subject = individual.getLocalName();
			oneSubject = new ArrayList<String[]>();
			iProperties = individual.listProperties();
			while (iProperties.hasNext()) {
				Statement statement = (Statement) iProperties.next();
				predicateObject = new String[2];
				predicateObject[0] = statement.getPredicate().getLocalName();
				predicateObject[1] = statement.getObject().toString();
				oneSubject.add(predicateObject);
			}
			iProperties.close();

			triples.put(subject, oneSubject);
		}

		iIndividuals.close();

		return triples;
	}

	/**
	 * Create a model with a reasoner set based on the chosen reasoning level.
	 * 
	 * @param reasoningLevel The reasoning level for this model
	 * 
	 * @return The created ontology model
	 */
	private OntModel createModel(String reasoningLevel) {
		OntModel model;
		int reasoningLevelIndex;

		model = null;

		reasoningLevelIndex = getReasoningLevelIndex(reasoningLevel);

		if (reasoningLevelIndex == 0) { // None
			model = ModelFactory.createOntologyModel(OntModelSpec.OWL_DL_MEM);
		} else if (reasoningLevelIndex == 1) { // RDFS
			model = ModelFactory
					.createOntologyModel(OntModelSpec.OWL_DL_MEM_RDFS_INF);
		} else if (reasoningLevelIndex == 2) { // OWL
			Reasoner reasoner = PelletReasonerFactory.theInstance().create();
			Model infModel = ModelFactory.createInfModel(reasoner, ModelFactory
					.createDefaultModel());
			model = ModelFactory.createOntologyModel(OntModelSpec.OWL_DL_MEM,
					infModel);
		}

		return model;
	}

	/**
	 * Obtain an ontology model set to the chosen reasoning level. Load the
	 * ontology into the model
	 * 
	 * @param reasoningLevel The selected reasoning level
	 */
	private void loadModel(String reasoningLevel) {
		FileInputStream inputStream = null;
		String modelFormat;

		try {
		} catch (Throwable throwable) {
			System.err.println("Failed to open input file: " + inputFileName);
			throwable.printStackTrace();
			System.exit(3);
		}

		modelFormat = null;

		for (String format : FORMATS) {
			try {
				inputStream = new FileInputStream(inputFileName);
				ontModel = createModel(reasoningLevel);
				ontModel.read(inputStream, null, format.toUpperCase());
				modelFormat = format;
				break;
			} catch (Throwable throwable) {
				System.err.println("Error reading file: "
						+ throwable.getClass().getName() + ": as format: "
						+ format + ": " + throwable.getMessage());
			} finally {
				try {
					inputStream.close();
				} catch (Throwable throwable) {
					System.err.println("Error closing input file");
					throwable.printStackTrace();
					System.exit(4);
				}
			}
		}

		if (modelFormat == null) {
			throw new IllegalStateException(
					"The format of the input file cannot be determined.\nTried: "
							+ getFormatsAsCSV());
		} else {
			System.out.println("Loaded model " + inputFileName
					+ " using format: " + modelFormat);
		}
	}

	/**
	 * Get the index position of the supplied reasoning level label
	 * 
	 * @param reasonerName A reasoning level label
	 * 
	 * @return The index position of the reasoning level. Will be equal to the
	 *         constant UNKNOWN if the value cannot be found in the collection
	 *         of known reasoning levels
	 */
	public final static int getReasoningLevelIndex(String reasonerName) {
		return getIndexValue(REASONING_LEVELS, reasonerName);
	}

	/**
	 * Find a String value within and array of Strings. Return the index
	 * position where the value was found.
	 * 
	 * @param array An array of string to search
	 * @param name The value to find in the array
	 * 
	 * @return The position where the value was found in the array. Will be
	 *         equal to the constant UNKNOWN if the value cannot be found in the
	 *         collection of known reasoning levels
	 */
	public final static int getIndexValue(String[] array, String name) {
		Integer indexValue;

		indexValue = null;

		for (int index = 0; index < array.length && indexValue == null; ++index) {
			if (array[index].toUpperCase().equals(name.toUpperCase())) {
				indexValue = index;
			}
		}

		return indexValue == null ? UNKNOWN : indexValue;
	}

	/**
	 * The execution point for the program. Verifies the input arguments have
	 * been supplied, creates an instance of the DiffInferencing class and
	 * creates a thread to run the instance. The program requires an input file
	 * name to be supplied on the command line. An optional output file name may
	 * also be supplied.
	 * 
	 * @param args The array of input arguments
	 */
	public static void main(String[] args) {
		Runnable runnable;
		int argNum;
		String inputFileName = null;
		String outputFileName = null;

		if (args.length > 2) {
			System.err
					.println("usage: DiffInferencing [<input file> [<output file>]]");
			System.exit(1);
		}

		argNum = 0;

		if (args.length > 0) {
			inputFileName = args[argNum++].trim();
		}

		if (inputFileName == null || inputFileName.length() == 0) {
			inputFileName = DEFAULT_INPUT_FILE;
			System.out
					.println("Using default input file: " + inputFileName);

		}
		if (args.length == 2) {
			outputFileName = args[argNum++].trim();
		}

		if (outputFileName == null || outputFileName.length() == 0) {
			outputFileName = inputFileName + ".compare.out";
			System.out.println("Defaulting output file name to: "
					+ outputFileName);
		}

		runnable = new DiffInferencing(inputFileName, outputFileName);

		new Thread(runnable).start();
	}
}
