/*******************************************************************************
 * Copyright 2012 University of Southern California
 * 
 * Licensed 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.
 * 
 * This code was developed by the Information Integration Group as part 
 * of the Karma project at the Information Sciences Institute of the 
 * University of Southern California.  For more information, publications, 
 * and related projects, please see: http://www.isi.edu/integration
 ******************************************************************************/

package edu.isi.karma.kr2rml;

import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.JSONException;
import org.openrdf.OpenRDFException;
import org.openrdf.model.BNode;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.ValueFactory;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.repository.Repository;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.turtle.TurtleWriter;
import org.openrdf.sail.memory.MemoryStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import edu.isi.karma.common.HttpMethods;
import edu.isi.karma.kr2rml.formatter.KR2RMLColumnNameFormatter;
import edu.isi.karma.kr2rml.mapping.KR2RMLMapping;
import edu.isi.karma.kr2rml.planning.TriplesMap;
import edu.isi.karma.kr2rml.template.StringTemplateTerm;
import edu.isi.karma.kr2rml.template.TemplateTerm;
import edu.isi.karma.kr2rml.template.TemplateTermSet;
import edu.isi.karma.modeling.Namespaces;
import edu.isi.karma.modeling.Prefixes;
import edu.isi.karma.modeling.Uris;
import edu.isi.karma.rep.RepFactory;
import edu.isi.karma.rep.Worksheet;
import edu.isi.karma.rep.Workspace;
import edu.isi.karma.rep.metadata.WorksheetProperties;
import edu.isi.karma.rep.metadata.WorksheetProperties.Property;

public class KR2RMLMappingWriter {
	
	private Repository myRepository;
	
	// Internal instance variables
	private RepositoryConnection con;
	private ValueFactory f;
	private Map<String, URI> repoURIs;
	private static Logger logger = LoggerFactory
			.getLogger(KR2RMLMappingWriter.class);
	
	public KR2RMLMappingWriter()
			throws RepositoryException {

		initializeTripleStore();
		initializeURIs();
	}

	private Resource addKR2RMLMappingResource(Worksheet worksheet, KR2RMLMapping mapping, String optionalMappingUri)
			throws RepositoryException {
		/** Create resource for the mapping as a blank node **/
		Resource mappingRes;
		if(optionalMappingUri != null && !optionalMappingUri.trim().isEmpty()) {
			mappingRes = f.createURI(optionalMappingUri);
		}
		else {
			mappingRes = f.createBNode();
		}
		con.add(mappingRes, RDF.TYPE, repoURIs.get(Uris.KM_R2RML_MAPPING_URI));
		Value srcNameVal = f.createLiteral(worksheet.getTitle());
		con.add(mappingRes, repoURIs.get(Uris.KM_SOURCE_NAME_URI), srcNameVal);
		
		// Add the timestamp
		con.add(mappingRes, repoURIs.get(Uris.KM_MODEL_PUBLICATION_TIME_URI), f.createLiteral(new Date().getTime()));
		String inputColumns = worksheet.getMetadataContainer().getWorksheetProperties().getPropertyValue(
				Property.inputColumns);
		String outputColumns = worksheet.getMetadataContainer().getWorksheetProperties().getPropertyValue(
				Property.outputColumns);
		String oldHistory = worksheet.getMetadataContainer().getWorksheetProperties().getPropertyValue(
				Property.oldCommandHistory);
		String graphLabel = worksheet.getMetadataContainer().getWorksheetProperties().getPropertyValue(
				Property.graphLabel);
		String baseURI = worksheet.getMetadataContainer().getWorksheetProperties().getPropertyValue(
				Property.baseURI);
		// Add the version
		con.add(mappingRes, repoURIs.get(Uris.KM_MODEL_VERSION_URI), f.createLiteral(KR2RMLVersion.getCurrent().toString()));
		if (inputColumns != null && !inputColumns.trim().isEmpty())
			con.add(mappingRes, repoURIs.get(Uris.KM_HAS_INPUTCOLUMNS), f.createLiteral(inputColumns));
		if (outputColumns != null && !outputColumns.trim().isEmpty())
			con.add(mappingRes, repoURIs.get(Uris.KM_HAS_OUTPUTCOLUMNS), f.createLiteral(outputColumns));
		if (graphLabel != null && !graphLabel.trim().isEmpty())
			con.add(mappingRes, repoURIs.get(Uris.KM_HAS_MODELLABEL), f.createLiteral(graphLabel));	
		if (baseURI != null && !baseURI.trim().isEmpty())
			con.add(mappingRes, repoURIs.get(Uris.KM_HAS_BASEURI), f.createLiteral(baseURI));
		if (oldHistory != null && !oldHistory.trim().isEmpty())
			con.add(mappingRes, repoURIs.get(Uris.KM_HAS_OLDHISTORY), f.createLiteral(oldHistory));
		addWorksheetProperties(worksheet, mappingRes);
		addCompleteWorksheetHistory(mapping, mappingRes);
		return mappingRes;
	}

	private void initializeTripleStore() throws RepositoryException {
		/** Initialize an in-memory sesame triple store **/
		myRepository = new SailRepository(new MemoryStore());
		myRepository.initialize();
		con = myRepository.getConnection();
		f = myRepository.getValueFactory();
	}
	
	protected void initializeURIs()
	{
		repoURIs = new HashMap<>();
		for (String uri : Uris.Uris) {
			repoURIs.put(uri, f.createURI(uri));
		}				
		
	}

	public boolean addR2RMLMapping(KR2RMLMapping mapping, Worksheet worksheet, Workspace workspace)
			throws RepositoryException, JSONException {
		return addR2RMLMapping(mapping, worksheet, workspace, null);
	}
	
	public boolean addR2RMLMapping(KR2RMLMapping mapping, Worksheet worksheet, Workspace workspace, String optionalMappingUri)
			throws RepositoryException, JSONException {

		try {

			Resource mappingRes = addKR2RMLMappingResource(worksheet, mapping, optionalMappingUri);
			addTripleMaps(mapping, mappingRes, worksheet, workspace);
			addPrefixes(mapping);
			
		} catch (OpenRDFException e) {
			logger.error("Error occured while generating RDF representation of R2RML data " +
					"structures.", e);
		}
		return true;
	}

	public void writeR2RMLMapping(PrintWriter writer)  {
		try {
			con.export(new TurtleWriter(writer));
		} catch (RepositoryException e) {
			logger.error("Error occured while outputing R2RML mapping");
		} catch (RDFHandlerException e) {
			logger.error("Error occured while outputing R2RML mapping");
		}
	}

	private void addPrefixes(KR2RMLMapping mapping) throws RepositoryException {
		for (Prefix p : mapping.getPrefixes())
		{
			con.setNamespace(p.getPrefix(), p.getNamespace());
		}
		con.setNamespace(Prefixes.RR, Namespaces.RR);
		con.setNamespace(Prefixes.KARMA_DEV, Namespaces.KARMA_DEV);
	}

	private void addTripleMaps(KR2RMLMapping mapping, Resource mappingRes, Worksheet worksheet, Workspace workspace)
			throws RepositoryException {
		/** Get the required data structures of R2RML **/

		List<TriplesMap> triplesMapList = mapping.getTriplesMapList();
		/** Add all the triple maps **/
		for (TriplesMap trMap:triplesMapList) {
			addTripleMap(mapping, mappingRes, worksheet, workspace,
					trMap);
		}
	}

	private void addTripleMap(KR2RMLMapping mapping, Resource mappingRes,
			Worksheet worksheet, Workspace workspace, TriplesMap trMap) throws RepositoryException {
		URI trMapUri = f.createURI(Namespaces.KARMA_DEV + trMap.getId());
		addTripleMapMetadata(mapping, mappingRes, worksheet, trMap, trMapUri);
		
		addSubjectMap(mapping, mappingRes, trMap, trMapUri, workspace);
		
		addPredicateObjectMaps(mapping, mappingRes, trMap, trMapUri, workspace, worksheet);
	}

	private void addTripleMapMetadata(KR2RMLMapping mapping, Resource mappingRes, Worksheet worksheet,
			TriplesMap trMap, URI trMapUri) throws RepositoryException {
		
		// Add the triples map type statement
		con.add(trMapUri, RDF.TYPE, repoURIs.get(Uris.RR_TRIPLESMAP_CLASS_URI));
		// Associate it with the source mapping URI
		con.add(mappingRes, repoURIs.get(Uris.KM_HAS_TRIPLES_MAP_URI), trMapUri);
		con.add(trMapUri, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
		
		addLogicalSource(mapping, worksheet, mappingRes, trMapUri);
	}

	private void addLogicalSource(KR2RMLMapping mapping, Worksheet worksheet, Resource mappingRes, URI trMapUri)
			throws RepositoryException {
		// Add the Logical table information
		BNode logTableBNode = f.createBNode();
		
		con.add(logTableBNode, repoURIs.get(Uris.RR_TABLENAME_URI), f.createLiteral(worksheet.getTitle()));
		con.add(logTableBNode, RDF.TYPE, repoURIs.get(Uris.RR_LOGICAL_TABLE_CLASS_URI));
		con.add(logTableBNode, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
		con.add(mappingRes, repoURIs.get(Uris.KM_HAS_LOGICAL_TABLE_URI), logTableBNode);
		con.add(trMapUri, repoURIs.get(Uris.RR_LOGICAL_TABLE_URI), logTableBNode);

	}
	
	private void addSubjectMap(KR2RMLMapping mapping,
			Resource mappingRes, TriplesMap trMap,
			URI trMapUri, Workspace workspace) throws RepositoryException {
		
		KR2RMLColumnNameFormatter columnNameFormatter = mapping.getColumnNameFormatter();
		RepFactory factory = workspace.getFactory();
		// Add the subject map statements
		SubjectMap sjMap = trMap.getSubject();
		BNode sjBlankNode = f.createBNode();
		con.add(trMapUri, repoURIs.get(Uris.RR_SUBJECTMAP_URI), sjBlankNode);
		con.add(mappingRes, repoURIs.get(Uris.KM_HAS_SUBJECT_MAP_URI), sjBlankNode);
		con.add(sjBlankNode, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
		// Add the subject map type statement
		con.add(sjBlankNode, RDF.TYPE, repoURIs.get(Uris.RR_SUBJECTMAP_CLASS_URI));
		
		// Add the node id for the subject
		Value nodeIdVal = f.createLiteral(sjMap.getId());
		con.add(sjBlankNode, repoURIs.get(Uris.KM_NODE_ID_URI), nodeIdVal);
		
		// Add the type for subject maps
		List<TemplateTermSet> rdfsTypes = sjMap.getRdfsType();
		for (TemplateTermSet typeTermSet:rdfsTypes) {
			if (typeTermSet.isSingleUriString()) {
				URI sjTypeUri = f.createURI(typeTermSet.getR2rmlTemplateString(factory, columnNameFormatter));
				con.add(sjBlankNode, repoURIs.get(Uris.RR_CLASS_URI), sjTypeUri);
			} else {
				if (typeTermSet.isSingleColumnTerm()) {
					BNode typeBlankNode = f.createBNode();
					String colRepr  = typeTermSet.getR2rmlTemplateString(factory, columnNameFormatter);
					con.add(typeBlankNode, repoURIs.get(Uris.RR_TEMPLATE_URI), f.createLiteral(colRepr));
					con.add(sjBlankNode, repoURIs.get(Uris.RR_CLASS_URI), typeBlankNode);
				}
			}
		}
		
		// Check if the subject map is a blank node
		if (sjMap.isBlankNode()) {
			con.add(sjBlankNode, repoURIs.get(Uris.RR_TERM_TYPE_URI), repoURIs.get(Uris.RR_BLANK_NODE_URI));
			
			// Add the prefix name for the blank node
			String prefix = mapping.getAuxInfo().getBlankNodesUriPrefixMap().get(sjMap.getId());
			Value prefixVal = f.createLiteral(prefix);
			con.add(sjBlankNode, repoURIs.get(Uris.KM_BLANK_NODE_PREFIX_URI), prefixVal);
		}
		else
		{
			// Print out the template for anything that isn't a blank node
			Value templVal = f.createLiteral(sjMap.getTemplate()
					.getR2rmlTemplateString(factory, columnNameFormatter));
			con.add(sjBlankNode, repoURIs.get(Uris.RR_TEMPLATE_URI), templVal);
		}
		
		// Mark as Steiner tree root node if required
		if (sjMap.isSteinerTreeRootNode()) {
			con.add(sjBlankNode, RDF.TYPE, repoURIs.get(Uris.KM_STEINER_TREE_ROOT_NODE));
		}
	}


	private void addPredicateObjectMaps(
			KR2RMLMapping mapping, Resource mappingRes, TriplesMap trMap,
			URI trMapUri, Workspace workspace, Worksheet worksheet) throws RepositoryException {

		// Add the predicate object maps
		for (PredicateObjectMap pom:trMap.getPredicateObjectMaps()) {
			addPredicateObjectMap(mapping, mappingRes, trMapUri, workspace, pom, worksheet);
		}
	}

	private void addPredicateObjectMap(KR2RMLMapping mapping, Resource mappingRes, URI trMapUri,
			Workspace workspace, PredicateObjectMap pom, Worksheet worksheet)
			throws RepositoryException {
		KR2RMLColumnNameFormatter columnNameFormatter = mapping.getColumnNameFormatter();
		RepFactory factory = workspace.getFactory();
		URI pomUri = f.createURI(Namespaces.KARMA_DEV + pom.getId());
		//BNode pomBlankNode = f.createBNode();
		
		boolean usablePredicate = addPredicate(pom, columnNameFormatter, factory, pomUri);
		if(!usablePredicate)
		{
			return;
		}
		
		addObject(mappingRes, pom, columnNameFormatter, factory, pomUri, worksheet);
		con.add(trMapUri, repoURIs.get(Uris.RR_PRED_OBJ_MAP_URI), pomUri);
		// Add the predicate object map type statement
		con.add(pomUri, RDF.TYPE, repoURIs.get(Uris.RR_PREDICATEOBJECTMAP_CLASS_URI));
		con.add(pomUri, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
		con.add(mappingRes, repoURIs.get(Uris.KM_HAS_PREDICATE_OBJECT_MAP_URI), pomUri);
		
	}
	
	private boolean addPredicate(PredicateObjectMap pom,
			KR2RMLColumnNameFormatter columnNameFormatter, RepFactory factory,
			URI pomUri) throws RepositoryException {
		// Add the predicate
		TemplateTermSet predTermSet = pom.getPredicate().getTemplate();
		if (predTermSet.isSingleUriString()) {
			URI predValUri = f.createURI(predTermSet
					.getR2rmlTemplateString(factory, columnNameFormatter));
			
			// Skip the class instance special meta property
			if (predValUri.stringValue().equals(Uris.CLASS_INSTANCE_LINK_URI))
				return false;
			
			con.add(pomUri, repoURIs.get(Uris.RR_PREDICATE_URI), predValUri);
		} else {
			Value predValLiteratl = f.createLiteral(predTermSet.
					getR2rmlTemplateString(factory, columnNameFormatter));
			con.add(pomUri, repoURIs.get(Uris.RR_PREDICATE_URI), predValLiteratl);
		}
		return true;
	}

	private void addObject(Resource mappingRes, PredicateObjectMap pom,
			KR2RMLColumnNameFormatter columnNameFormatter, RepFactory factory,
			URI pomUri, Worksheet worksheet) throws RepositoryException {
		// Add the object: Could be RefObjectMap or simple object with column values
		if (pom.getObject().hasRefObjectMap()) {
			RefObjectMap rfMap = pom.getObject().getRefObjectMap();
			URI rfUri = f.createURI(Namespaces.KARMA_DEV + rfMap.getId());
			con.add(rfUri, RDF.TYPE, repoURIs.get(Uris.RR_REF_OBJECT_MAP_CLASS_URI));
			con.add(rfUri, RDF.TYPE, repoURIs.get(Uris.RR_OBJECTMAP_CLASS_URI));
			
			TriplesMap prMap = rfMap.getParentTriplesMap();
			URI prMapUri = f.createURI(Namespaces.KARMA_DEV + prMap.getId());
			con.add(rfUri, repoURIs.get(Uris.RR_PARENT_TRIPLE_MAP_URI), prMapUri);
			con.add(rfUri, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
			con.add(mappingRes, repoURIs.get(Uris.KM_HAS_OBJECT_MAP_URI), rfUri);
			// Add the RefObjectMap as the object map of current POMap
			con.add(pomUri, repoURIs.get(Uris.RR_OBJECTMAP_URI), rfUri);
		} else {
			TemplateTermSet objTermSet = pom.getObject().getTemplate();
			TemplateTermSet rdfLiteralTypeTermSet = pom.getObject().getRdfLiteralType();
			TemplateTermSet languageTermSet = pom.getObject().getLanguage();
			
			if (objTermSet.isSingleColumnTerm()) {
				BNode cnBnode = f.createBNode();
				Value cnVal = f.createLiteral(objTermSet.
						getColumnNameR2RMLRepresentation(factory, columnNameFormatter));
				
				con.add(cnBnode, repoURIs.get(Uris.RR_COLUMN_URI), cnVal);
				con.add(cnBnode, RDF.TYPE, repoURIs.get(Uris.RR_OBJECTMAP_CLASS_URI));
				if (rdfLiteralTypeTermSet != null && rdfLiteralTypeTermSet.isSingleUriString()) {
					String rdfLiteralTypeString = rdfLiteralTypeTermSet.
							getR2rmlTemplateString(factory);
					if(!rdfLiteralTypeString.isEmpty())
					{
						Value cnRdfLiteralType = f.createLiteral(rdfLiteralTypeString);
						con.add(cnBnode, repoURIs.get(Uris.RR_DATATYPE_URI), cnRdfLiteralType);
					}

				}
				if(languageTermSet != null) {
					String languageString = languageTermSet.getR2rmlTemplateString(factory);
					if(!languageString.isEmpty()) {
						Value cnLanguage = f.createLiteral(languageString);
						con.add(cnBnode, repoURIs.get(Uris.RR_LANGUAGE_URI), cnLanguage);
					}
				}
				con.add(cnBnode, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
				con.add(mappingRes, repoURIs.get(Uris.KM_HAS_OBJECT_MAP_URI), cnBnode);
				
				// Add the link b/w blank node and object map
				con.add(pomUri, repoURIs.get(Uris.RR_OBJECTMAP_URI), cnBnode);
			}
			else if(!objTermSet.isEmpty())
			{
				BNode cnBnode = f.createBNode();
				// Print out the template for anything that isn't a blank node
				boolean isUri = false;
				
				for (TemplateTerm term:objTermSet.getAllTerms()) {
					if (term instanceof StringTemplateTerm && ((StringTemplateTerm)term).hasFullUri()) {
						isUri = true;
					}
				}
						
				String value = objTermSet
						.getR2rmlTemplateString(factory, columnNameFormatter);
				Value templVal;
				if(isUri) {
					try {
						templVal = f.createURI(value);
					}catch(IllegalArgumentException e) {
						String baseURI = worksheet.getMetadataContainer().getWorksheetProperties().getPropertyValue(
								Property.baseURI);
						templVal = f.createURI(baseURI + value);
					}
				} else {
					templVal = f.createLiteral(value);
				}
				con.add(cnBnode, repoURIs.get(Uris.RR_CONSTANT), templVal);
				if (rdfLiteralTypeTermSet != null && rdfLiteralTypeTermSet.isSingleUriString()) {
					String rdfLiteralTypeString = rdfLiteralTypeTermSet.
							getR2rmlTemplateString(factory);
					if(!rdfLiteralTypeString.isEmpty())
					{
						Value cnRdfLiteralType = f.createLiteral(rdfLiteralTypeString);
						con.add(cnBnode, repoURIs.get(Uris.RR_DATATYPE_URI), cnRdfLiteralType);
					}

				}
				if(languageTermSet != null) {
					String languageString = languageTermSet.getR2rmlTemplateString(factory);
					if(!languageString.isEmpty()) {
						Value cnLanguage = f.createLiteral(languageString);
						con.add(cnBnode, repoURIs.get(Uris.RR_LANGUAGE_URI), cnLanguage);
					}
				}
				//con.add(cnBnode, repoURIs.get(Uris.RR_TERM_TYPE_URI), repoURIs.get(Uris.RR_LITERAL_URI));
				con.add(cnBnode, RDF.TYPE, repoURIs.get(Uris.RR_OBJECTMAP_CLASS_URI));
				con.add(cnBnode, repoURIs.get(Uris.KM_IS_PART_OF_MAPPING_URI), mappingRes);
				con.add(mappingRes, repoURIs.get(Uris.KM_HAS_OBJECT_MAP_URI), cnBnode);
				
				//Add the link b/w blank node and object map
				con.add(pomUri, repoURIs.get(Uris.RR_OBJECTMAP_URI), cnBnode);
				
			}
		}
	}


	public void close() throws RepositoryException {
		con.close();
		myRepository.shutDown();
	}

	public void addCompleteWorksheetHistory(KR2RMLMapping mapping, Resource mappingRes) 
			throws RepositoryException {
		URI hasWorksheetHistoryUri = f.createURI(Uris.KM_HAS_WORKSHEET_HISTORY_URI);
		
			Value historyLiteral = f.createLiteral(mapping.getWorksheetHistory().toString(4));
			con.add(mappingRes, hasWorksheetHistoryUri, historyLiteral);
		
	}

	public void addWorksheetProperties(Worksheet worksheet, Resource mappingRes) throws RepositoryException {
		WorksheetProperties props = worksheet.getMetadataContainer().getWorksheetProperties();
		if (props == null) {
			return;
		}
		
		// Service options (if present)
		if (props.hasServiceProperties()) {
			if (props.getPropertyValue(Property.serviceUrl) == null) {
				return;
			}
			
			// Request method triple
			URI reqMethodUri = f.createURI(Uris.KM_SERVICE_REQ_METHOD_URI);
			Value method = f.createLiteral(props.getPropertyValue(Property.serviceRequestMethod));
			con.add(mappingRes, reqMethodUri, method);
			
			// Service Url triple
			URI serUrlUri = f.createURI(Uris.KM_SERVICE_URL_URI);
			Value servUrl = f.createLiteral(props.getPropertyValue(Property.serviceUrl));
			con.add(mappingRes, serUrlUri, servUrl);
			
			if (props.getPropertyValue(Property.serviceRequestMethod).equals(HttpMethods.POST.name())) {
				// POST method related option triple
				URI postMethodUri = f.createURI(Uris.KM_SERVICE_POST_METHOD_TYPE_URI);
				Value methodUrl = f.createLiteral(props.getPropertyValue(Property.serviceDataPostMethod));
				con.add(mappingRes, postMethodUri, methodUrl);
			}
		}
	}
}
