package cz.muni.fi.lasaris.sbms.semantics.logic;

import java.util.List;

import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.Property;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.vocabulary.RDF;
import org.apache.log4j.Logger;

import cz.muni.fi.lasaris.sbms.semantics.api.entities.Address;
import cz.muni.fi.lasaris.sbms.semantics.api.entities.DataPoint;
import cz.muni.fi.lasaris.sbms.semantics.api.entities.Influence;
import cz.muni.fi.lasaris.sbms.semantics.api.request.Insert;

public class ModelUpdater {
	
	final static Logger logger = Logger.getLogger(ModelUpdater.class);
	
	public static boolean insertDataPoint(Insert insert) {
		logger.debug("Inserting data: " + insert);
		
		if(insert == null || insert.getDataPoint() == null || insert.getDataPoint().getBmsId() == null) {
			//throw new IllegalArgumentException("bmsId must not be null");
			return failInsert("bmsId must not be null");
		}
		DataPoint d = insert.getDataPoint();
		
		try {	
			Model m = TdbConnector.getWritableModel();
			
// creates new DP and Observation or retrieves existing
			Resource dp = m.createResource(NS.sbmsd + d.getResourceId());
			Resource obs = m.createResource(NS.sbmsd + d.getResourceId() + "Obs");
			
			

//			?dataPoint rdf:type ?dataPointClass.
//		    ?observation rdf:type sbms:Observation.
			if(!dp.hasProperty(RDF.type)) {
				if(!TypesProvider.getDataPointTypes().getMembers().contains(d.getType())) {
					//throw new IllegalArgumentException("Unknown datapoint type");
					return failInsert("Unknown datapoint type [" + d.getType() + "]. Allowed types for " 
					+ TypesProvider.getDataPointTypes().toString());
				}
				logger.debug("Inserting dp type: " + d.getType());
				dp.addProperty(RDF.type, m.createResource(NS.sbms + d.getType()));
			}
			if(!obs.hasProperty(RDF.type)) {
				logger.debug("Inserting observation");
				obs.addProperty(RDF.type, m.createResource(NS.sbms + "Observation"));
			}
//			?dataPoint sbms:hasBMSId ?bmsId.
			Property p = null;
			p = m.createProperty(NS.sbms + "hasBMSId");
			if(!dp.hasProperty(p)) {
				logger.debug("Inserting bmsId: " + d.getBmsId());
				dp.addLiteral(p, d.getBmsId());
			}
//			?dataPoint sbms:expressesObservation ?observation.
			p = m.createProperty(NS.sbms + "expressesObservation");
			if(!dp.hasProperty(p)) {
				dp.addProperty(p, obs);
			}

//			?dataPoint sbms:publishedByDevice ?publisher
			
			if(d.getPublisher() != null && d.getPublisher().getBimId() != null) {
				if(!checkBIMId(d.getPublisher().getBimId())) {
					return failInsert("Publisher: unknown BIM id");
				}
				logger.debug("Inserting publisher: " + d.getPublisher().getResourceId());
				p = m.createProperty(NS.sbms + "publishedByDevice");
				if(!dp.hasProperty(p)) {
					dp.addProperty(p, m.createResource(NS.sbmsd + d.getPublisher().getResourceId()));
				}
			}
			if(d.getProperty() != null && d.getProperty().getDomain() != null 
					&& d.getProperty().getQuality() != null 
					&& d.getScope() != null && d.getScope().getBimId() != null) {
				if(!TypesProvider.getQualityTypes().getMembers().contains(d.getProperty().getQuality())) {
					//throw new IllegalArgumentException("Unknown quality type");
					return failInsert("Unknown quality type [" + d.getProperty().getQuality() + "]. Allowed types for " 
					+ TypesProvider.getQualityTypes().toString());
				}
				if(!TypesProvider.getPropDomainTypes().getMembers().contains(d.getProperty().getDomain())) {
					//throw new IllegalArgumentException("Unknown property type");
					return failInsert("Unknown property domain type [" + d.getProperty().getDomain() + "]. Allowed types for " 
					+ TypesProvider.getPropDomainTypes().toString());
				}
				
				logger.debug("Inserting observed property: " + d.getProperty());
				
//			    ?property rdf:type sbms:ObservedProperty.							
				Resource op = m.createResource(NS.sbmsd + d.getScope().getResourceId() 
						+ d.getProperty().getDomain() 
						+ d.getProperty().getQuality() + "ObsProp");
				op.addProperty(RDF.type, NS.sbms + m.createResource(NS.sbms + "ObservedProperty"));
				
				
//				?observation sbms:observedProperty ?property.				
				p = m.createProperty(NS.sbms + "observedProperty");
				obs.addProperty(p, op);
				
				
//				?property sbms:hasPhysicalQuality ?quality.
				op.addProperty(m.createProperty(NS.sbms + "hasPhysicalQuality"), 
						m.createResource(NS.ucum + d.getProperty().getQuality()));
//				?property sbms:hasPropertyDomain ?propDomain.
				op.addProperty(m.createProperty(NS.sbms + "hasPropertyDomain"), 
						m.createResource(NS.sbms + d.getProperty().getDomain()));
//				?property sbms:isPropertyOf ?scope.				
				op.addProperty(m.createProperty(NS.sbms + "isPropertyOf"),
						m.createResource(NS.sbmsd +d.getScope().getResourceId()));
			}
			
			if(d.getSource() != null && d.getSource().getBimId() !=null) {
				if(!checkBIMId(d.getSource().getBimId())) {
					return failInsert("Source: unknown BIM id");
				}
				
				logger.debug("Inserting source: " + d.getSource());
				
//				?observation sbms:observedBy ?source.
				obs.addProperty(m.createProperty(NS.sbms + "observedBy"), 
						m.createResource(NS.sbmsd + d.getSource().getResourceId()));
			}
			if(d.getScope() != null && d.getScope().getBimId() != null) {
				if(!checkBIMId(d.getScope().getBimId())) {
					return failInsert("Scope: unknown BIM id");
				}

				logger.debug("Inserting scope: " + d.getScope());
//				?observation sbms:featureOfInterest ?scope. 
				obs.addProperty(m.createProperty(NS.sbms + "featureOfInterest"), 
						m.createResource(NS.sbmsd + d.getScope().getResourceId()));
			}
			
			if(d.getSensing() != null && d.getSensing().getType() != null) {
				if(!TypesProvider.getSensingTypes("all").getMembers().contains(d.getSensing().getType())) {
					//throw new IllegalArgumentException("Unknown sensing type");
					return failInsert("Unknown sensing type [" + d.getSensing().getType() + "]. Allowed types for " 
					+ TypesProvider.getSensingTypes("all").toString());
				}
				logger.debug("Inserting sensing: " + d.getSensing());
				Resource sensing = m.createResource(NS.sbmsd + d.getResourceId() + "Sensing");
//				?sensing rdf:type ?sensingClass.
				sensing.addProperty(RDF.type, m.createResource(NS.sbms + d.getSensing().getType()));
//				?observation sbms:sensingMethodUsed ?sensing.
				obs.addProperty(m.createProperty(NS.sbms + "sensingMethodUsed"), 
					sensing);

				if(TypesProvider.getStatefulSensingTypes().getMembers().contains(d.getSensing().getType())
						&& d.getSensing().getWindow() == null ) {
					//throw new IllegalArgumentException("Time window must be set for this sensing type");
					return failInsert("Time window must be set for this sensing type");
				}
				
//				?sensing sbms:hasAggregationTimeWindow ?timeWindow.
				if(d.getSensing() != null && d.getSensing().getWindow() != null) {
					if(!TypesProvider.getTimeWindowTypes().getMembers().contains(d.getSensing().getWindow())) {
						//throw new IllegalArgumentException("Unknown time window type");
						return failInsert("Unknown time window type [" + d.getSensing().getWindow() + "]. Allowed types for " 
					+ TypesProvider.getTimeWindowTypes().toString());
					}
					if(!TypesProvider.getStatefulSensingTypes().getMembers().contains(d.getSensing().getType())) {
						//throw new IllegalArgumentException("No time window for this sensing type");
						return failInsert("No time window for this sensing type");
					}
					sensing.addProperty(m.createProperty(NS.sbms + "hasAggregationTimeWindow"), 
							m.createResource(NS.sbms + d.getSensing().getWindow()));
				}
			}
			
			insertInfluence(d, m, dp);
			
			TdbConnector.commit();
			logger.debug("Data commited to the TDB");
			return true;
		} catch (Exception e) {
			return failInsert(e);
		}
		
		
	}

	private static void insertInfluence(Address entity, Model m, Resource res) {
		Property p;
		//			?dataPoint sbms:influences ?property.			
		if(entity.getInfluenceList() != null) {
			for(Influence inf : entity.getInfluenceList()) {
				if(inf.getProperty() != null 
						&& inf.getProperty().getDomain() != null 
						&& inf.getProperty().getQuality() != null && inf.getScope() != null &&
						inf.getScope().getBimId() != null) {
					
					logger.debug("Inserting influence: " + inf);
					
					Resource ip = m.createResource(NS.sbmsd + inf.getScope().getResourceId() 
							+ inf.getProperty().getDomain() 
							+ inf.getProperty().getQuality() + "ObsProp");
					ip.addProperty(RDF.type, m.createResource(NS.sbms + "ObservedProperty"));

					//				?datapoint sbms:influences ?property.				
					p = m.createProperty(NS.sbms + "influences");
					res.addProperty(p, ip);

					//				?property sbms:hasPhysicalQuality ?quality.
					ip.addProperty(m.createProperty(NS.sbms + "hasPhysicalQuality"), 
							m.createResource(NS.ucum + inf.getProperty().getQuality()));
					//				?property sbms:hasPropertyDomain ?propDomain.
					ip.addProperty(m.createProperty(NS.sbms + "hasPropertyDomain"), 
							m.createResource(NS.sbms + inf.getProperty().getDomain()));
					//				?property sbms:isPropertyOf ?scope.				
					ip.addProperty(m.createProperty(NS.sbms + "isPropertyOf"),
							m.createResource(NS.sbmsd +inf.getScope().getResourceId()));
				}
			}
		}
	}
	
	public static boolean insertTrend(Insert insert) {
		if(insert.getTrend() == null || insert.getTrend().getBmsId() == null) {
			return failInsert("no TL to insert");
		}
		
		try {	
			Model m = TdbConnector.getWritableModel();
			
			Resource trend = m.createResource(NS.sbmsd + insert.getTrend().getResourceId());
			if(!trend.hasProperty(RDF.type)) {
				trend.addProperty(RDF.type, m.createResource(NS.sbms + "Trend"));
			}
			Property p = null;
			p = m.createProperty(NS.sbms + "hasBMSId");
			if(!trend.hasProperty(p)) {
				trend.addLiteral(p, insert.getTrend().getBmsId());
			}
			if(insert.getTrend().getDataPoint() != null && insert.getTrend().getDataPoint().getResourceId() != null) {
				Resource dp = m.createResource(NS.sbmsd + insert.getTrend().getDataPoint().getResourceId());
				p = m.createProperty(NS.sbms + "trends");
				if(!trend.hasProperty(p)) {
					trend.addProperty(p, dp);
				}
			}
			TdbConnector.commit();
			logger.debug("Data commited to the TDB");
			return true;
		} catch (Exception e) {
			return failInsert(e);
		}
	}
	
	public static boolean insertAddress(Insert insert) {
		if(insert.getAddress() == null || insert.getAddress().getBmsId() == null) {
			return failInsert("no address to insert");
		}
		Address a = insert.getAddress();
		try {	
			Model m = TdbConnector.getWritableModel();
			
			Resource address = m.createResource(NS.sbmsd + a.getResourceId());
			if(!address.hasProperty(RDF.type)) {
				if(!TypesProvider.getAddressTypes("all").getMembers().contains(a.getType())) {
					//throw new IllegalArgumentException("Unknown datapoint type");
					return failInsert("Unknown address type");
				}
				
				address.addProperty(RDF.type, m.createResource(NS.sbms + a.getType()));
			}
			Property p = null;
			p = m.createProperty(NS.sbms + "hasBMSId");
			if(!address.hasProperty(p)) {
				address.addLiteral(p, a.getBmsId());
			}
			insertInfluence(a, m, address);
			TdbConnector.commit();
			logger.debug("Data commited to the TDB");
			return true;
		} catch (Exception e) {
			return failInsert(e);
		}
	}
	
	public static boolean insertAlgorithm(Insert insert) {
		if(insert.getAlgorithm() == null || insert.getAlgorithm().getBmsId() == null) {
			return failInsert("no algorithm to insert");
		}
		
		try {	
			Model m = TdbConnector.getWritableModel();
			
			Resource alg = m.createResource(NS.sbmsd + insert.getAlgorithm().getResourceId());
			Property p = null;
			
			if(!alg.hasProperty(RDF.type)) {
				alg.addProperty(RDF.type, m.createResource(NS.sbms + "Algorithm"));
			}
			p = m.createProperty(NS.sbms + "hasBMSId");
			if(!alg.hasProperty(p)) {
				alg.addLiteral(p, insert.getAlgorithm().getBmsId());
			}
			if(insert.getAlgorithm().getUsedAddressList() != null) {
				for(Address a : insert.getAlgorithm().getUsedAddressList()) {
					Resource address = m.createResource(NS.sbmsd + a.getResourceId());
					p = m.createProperty(NS.sbms + "uses");
					alg.addProperty(p, address);
					if(a.getType() != null && !address.hasProperty(RDF.type)) {
						address.addProperty(RDF.type, m.createResource(NS.sbms + a.getType()));
					}
				}
			}
			TdbConnector.commit();
			logger.debug("Data commited to the TDB");
			return true;
		} catch (Exception e) {
			return failInsert(e);
		}
	}
	
	private static boolean failInsert(Object msg) {
		TdbConnector.discardChanges();
		logger.info(msg);
		return false;
	}
	
	public static boolean removeDataPointAssertions(String bmsId) {
		logger.debug("Removing data: " + bmsId);
		
		if(bmsId == null) {
			//throw new IllegalArgumentException("bmsId must not be null");
			return failInsert("bmsId must not be null");
		}
		Address a = new Address(bmsId);
		/*
		DataPointsRequest r = new DataPointsRequest();
		r.setDataPoint(d);
		r.getResponseFields().addAll(DataPointsRequest.FIELDS);
		DataPointsResponse s = QueryParser.getDataPoints(r);
		
		List<DataPoint> l = s.getDataPoints().get(DataPointsRequest.DEFAULT_GROUPING);
		if(l.size() != 0) {
			d = l.get(0);
		}
		*/
		try {	
			Model m = TdbConnector.getWritableModel();
			// creates new DP and Observation or retrieves existing
			Resource dp = m.createResource(NS.sbmsd + a.getResourceId());
			Resource obs = m.createResource(NS.sbmsd + a.getResourceId() + "Obs");
			

//			?dataPoint rdf:type ?dataPointClass.
//			?observation rdf:type sbms:Observation.
			dp.removeAll(RDF.type);
			m.remove(obs, RDF.type, m.createResource(NS.sbms + "Observation"));

//						?dataPoint sbms:hasBMSId ?bmsId.
			Property p = null;
			dp.removeAll(m.createProperty(NS.sbms + "hasBMSId"));
			
			dp.removeAll(m.createProperty(NS.sbms + "publishedByDevice"));
			//						?dataPoint sbms:expressesObservation ?observation.
			dp.removeAll(m.createProperty(NS.sbms + "expressesObservation"));
			
			obs.removeAll(m.createProperty(NS.sbms + "observedProperty"));
			
				/* We don't want to remove property now - it might be used by different observation or influence
				//						    ?property rdf:type sbms:ObservedProperty.							
				Resource op = m.createResource(NS.sbmsd + d.getScope().getBimId() 
						+ d.getProperty().getDomain() 
						+ d.getProperty().getQuality() + "ObsProp");
				
				m.remove(op, RDF.type, m.createResource(NS.sbms + "ObservedProperty"));
				
				//							?observation sbms:observedProperty ?property.				
				


				//							?property sbms:hasPhysicalQuality ?quality.
				m.remove(op, m.createProperty(NS.sbms + "hasPhysicalQuality"), 
						m.createResource(NS.ucum + d.getProperty().getQuality()));
				//							?property sbms:hasPropertyDomain ?propDomain.
				m.remove(op, m.createProperty(NS.sbms + "hasPropertyDomain"), 
						m.createResource(NS.sbms + d.getProperty().getDomain()));
				//							?property sbms:isPropertyOf ?scope.				
				m.remove(op, m.createProperty(NS.sbms + "isPropertyOf"),
						m.createResource(NS.sbmsd +d.getScope().getBimId()));
			
				*/
			
				//							?observation sbms:observedBy ?source.
				
				obs.removeAll(m.createProperty(NS.sbms + "observedBy"));
		
				//							?observation sbms:featureOfInterest ?scope. 
				obs.removeAll(m.createProperty(NS.sbms + "featureOfInterest"));
			

				Resource sensing = m.createResource(NS.sbmsd + a.getResourceId() + "Sensing");
				//							?sensing rdf:type ?sensingClass.
				sensing.removeAll(RDF.type);
				//							?observation sbms:sensingMethodUsed ?sensing.
				obs.removeAll(m.createProperty(NS.sbms + "sensingMethodUsed"));

				//							?sensing sbms:hasAggregationTimeWindow ?timeWindow.
				sensing.removeAll(m.createProperty(NS.sbms + "hasAggregationTimeWindow"));
			

			//						?dataPoint sbms:influences ?property.			
/*
				Resource ip = m.createResource(NS.sbmsd + d.getInfluence().getScope().getBimId() 
						+ d.getInfluence().getProperty().getDomain() 
						+ d.getInfluence().getProperty().getQuality() + "ObsProp");
				
				m.remove(ip, RDF.type, NS.sbms + m.createResource(NS.sbms + "ObservedProperty"));
*/

				//							?datapoint sbms:influences ?property.				
				p = m.createProperty(NS.sbms + "influences");
				dp.removeAll(p);
/*
				//							?property sbms:hasPhysicalQuality ?quality.
				ip.addProperty(m.createProperty(NS.sbms + "hasPhysicalQuality"), 
						m.createResource(NS.ucum + d.getInfluence().getProperty().getQuality()));
				//							?property sbms:hasPropertyDomain ?propDomain.
				ip.addProperty(m.createProperty(NS.sbms + "hasPropertyDomain"), 
						m.createResource(NS.sbms + d.getInfluence().getProperty().getDomain()));
				//							?property sbms:isPropertyOf ?scope.				
				ip.addProperty(m.createProperty(NS.sbms + "isPropertyOf"),
						m.createResource(NS.sbmsd +d.getInfluence().getScope().getBimId()));
*/
				
				p = m.createProperty(NS.sbms + "uses");
				dp.removeAll(p);
				
			TdbConnector.commit();
			
			//cleanup();
			
			logger.debug("Changes commited to the TDB");
			return true;
		} catch (Exception e) {
			return failInsert(e);
		}
	}
	
	public static boolean removeRelationAssertions(String bmsId) {
		return removeAssertions(bmsId, NS.sbms + "influences") && removeAssertions(bmsId, NS.sbms + "uses");
	}
	
	public static boolean removeTrendAssertions(String bmsId) {
		return removeAssertions(bmsId, NS.sbms + "trends");
	}
	
	public static boolean removeAssertions(String bmsId, String propName) {
		logger.debug("Removing data: " + bmsId);
		
		if(bmsId == null) {
			//throw new IllegalArgumentException("bmsId must not be null");
			return failInsert("bmsId must not be null");
		}
		Address a = new Address(bmsId);
		try {	
			Model m = TdbConnector.getWritableModel();
			Resource dp = m.createResource(NS.sbmsd + a.getResourceId());
			
			//dp.removeAll(RDF.type);
			dp.removeAll(m.createProperty(propName));
			
			TdbConnector.commit();
			
			//cleanup();
			
			logger.debug("Changes commited to the TDB");
			return true;
		} catch (Exception e) {
			return failInsert(e);
		}
	}
	
	public static void cleanup() {
		logger.debug("Starting cleanup - reading unused individuals...");
		List<RDFNode> o = GetUnusedObservations();
		List<RDFNode> p = GetUnusedProperties();
		List<RDFNode> s = GetUnusedSensings();
		List<RDFNode> t = GetUnusedTrends();
		List<RDFNode> a = GetUnusedAlgorithms();
		Model m = TdbConnector.getWritableModel();
		logger.debug("Writable model opened for cleanup.");
		for(RDFNode n : o) {
			n.asResource().removeAll(m.createProperty(NS.sbms + "observedBy"));
			n.asResource().removeAll(m.createProperty(NS.sbms + "featureOfInterest"));
			n.asResource().removeAll(m.createProperty(NS.sbms + "sensingMethodUsed"));
			n.asResource().removeAll(m.createProperty(NS.sbms + "observedProperty"));
		}
		
		for(RDFNode n : p) {
			n.asResource().removeAll(RDF.type);
			n.asResource().removeAll(m.createProperty(NS.sbms + "hasPhysicalQuality"));
			n.asResource().removeAll(m.createProperty(NS.sbms + "hasPropertyDomain"));
			n.asResource().removeAll(m.createProperty(NS.sbms + "isPropertyOf"));
		}
		
		for(RDFNode n : s) {
			n.asResource().removeAll(RDF.type);
			n.asResource().removeAll(m.createProperty(NS.sbms + "hasAggregationTimeWindow"));
		}
		
		for(RDFNode n : t) {
			n.asResource().removeAll(RDF.type);
		}
		
		for(RDFNode n : a) {
			n.asResource().removeAll(RDF.type);
		}
		
		TdbConnector.commit();
		logger.debug("Cleanup complete.");
	}
	
	private static List<RDFNode> GetUnusedAlgorithms() {
		String query = String.format("SELECT ?member WHERE {?member <%stype> <%sAlgorithm>."
				+" FILTER (not exists { ?member <%suses> ?x. }).}", NS.rdf, NS.sbms, NS.sbms);
		return TdbConnector.executeSPARQLList(query);
	}

	private static List<RDFNode> GetUnusedTrends() {
		String query = String.format("SELECT ?member WHERE {?member <%stype> <%sTrend>."
				+" FILTER (not exists { ?member <%strends> ?x. }).}", NS.rdf, NS.sbms, NS.sbms);
		return TdbConnector.executeSPARQLList(query);
	}

	private static List<RDFNode> GetUnusedSensings() {
		/*String query = String.format("SELECT ?member WHERE {?member <%stype> ?type. ?type <%ssubClassOf> <%sSensing>."
				+" FILTER (not exists { ?x <%ssensingMethodUsed> ?member. }).}", NS.rdf, NS.rdfs, NS.sbms, NS.sbms);*/
		String query = String.format("SELECT ?member WHERE {?member <%stype> <%sSensing>."
				+" FILTER (not exists { ?x <%ssensingMethodUsed> ?member. }).}", NS.rdf, NS.sbms, NS.sbms);
		return TdbConnector.executeSPARQLList(query);
		
	}

	private static List<RDFNode> GetUnusedProperties() {
		String query = String.format("SELECT ?member WHERE { ?member <%stype> <%sObservedProperty>."
				+ " FILTER (not exists { ?x <%sobservedProperty> ?member. }).}", NS.rdf, NS.sbms, NS.sbms);
		return TdbConnector.executeSPARQLList(query);
			
	}

	private static List<RDFNode> GetUnusedObservations() {
		String query = String.format("SELECT ?member WHERE { ?member <%stype> <%sObservation>."
				+ " FILTER (not exists { ?x <%sexpressesObservation> ?member. }).}", NS.rdf, NS.sbms, NS.sbms);
		return TdbConnector.executeSPARQLList(query);
		
	}

	private static boolean checkBIMId(String bimId) {
		logger.debug("Checking bimId: " + bimId);
		if( TdbConnector.executeSPARQLList(
				String.format("SELECT * { ?object <%shasBIMId> \"%s\"}", NS.sbim, bimId), true).size() > 0) {
			logger.debug("Check succeeded");
			return true;
		} else {
			logger.debug("Check failed");
			return false;
		}
	}
}
