package cz.muni.fi.lasaris.sbms.model;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.jena.graph.NodeFactory;
import org.apache.jena.graph.Triple;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.expr.E_LogicalAnd;
import org.apache.jena.sparql.expr.E_NotEquals;
import org.apache.jena.sparql.expr.E_NotExists;
import org.apache.jena.sparql.expr.ExprVar;
import org.apache.jena.sparql.expr.nodevalue.NodeValueNode;
import org.apache.jena.sparql.syntax.ElementFilter;
import org.apache.jena.sparql.syntax.ElementGroup;
import org.apache.jena.sparql.syntax.ElementOptional;
import org.apache.jena.sparql.syntax.ElementTriplesBlock;
import org.apache.log4j.Logger;

import cz.muni.fi.lasaris.sbms.api.entities.DataPoint;
import cz.muni.fi.lasaris.sbms.api.entities.ObservedProperty;
import cz.muni.fi.lasaris.sbms.api.entities.Scope;
import cz.muni.fi.lasaris.sbms.api.entities.Sensing;
import cz.muni.fi.lasaris.sbms.api.entities.Source;
import cz.muni.fi.lasaris.sbms.api.request.DataPointsRequest;
import cz.muni.fi.lasaris.sbms.api.response.DataPointsResponse;

public class QueryParser {
	
	final static Logger logger = Logger.getLogger(QueryParser.class);
	
	public QueryParser() {
	}
	
	public static DataPointsResponse getDataPoints(DataPointsRequest dpr) {
		DataPointsResponse d = new DataPointsResponse();

		logger.debug("Query starting...");
		long start = new Date().getTime();

		// https://jena.apache.org/documentation/query/manipulating_sparql_using_arq.html

		ElementGroup body = getQueryBody(dpr);

		Query q = QueryFactory.make();
		q.setQueryPattern(body);                               // Set the body of the query to our group
		q.setQuerySelectType();                                // Make it a select query

		if(dpr.getGrouping() != null && !dpr.getGrouping().equals(Fields.DEFAULT_GROUPING)) {
			q.addResultVar("group");
		}
		
		for(String field : dpr.getResponseFields()) {
			q.addResultVar(gsv(field));
		}
		
		if(!q.getResultVars().contains(gsv("bmsId"))) {
			q.addResultVar(gsv("bmsId"));
		}
		
		
		
		logger.debug(q);
		List<QuerySolution>  s = TdbConnector.executeSPARQLResults(q);
		for (QuerySolution qs : s)
		{
			findGroup(d.getDataPoints(), createDataPoint(qs, dpr.getResponseFields(), dpr.getGrouping()));
		}
		logger.debug("Query finished.");
		logger.debug(String.format("Time : %d", new Date().getTime() - start));

		return d;
	}
	
	private static String gsv(String f) {
		return DataPointsRequest.getSPARQLvar(f);
	}
	
	private static void findGroup(Map<String, List<DataPoint>> dataPoints, DataPoint dp) {
		if(!dataPoints.containsKey(dp.getGroup())) {
			dataPoints.put(dp.getGroup(), new LinkedList<DataPoint>());
		}
		dataPoints.get(dp.getGroup()).add(dp);
	}

	private static ElementGroup getQueryBody(DataPointsRequest dpr) {
		ElementGroup body = new ElementGroup();
		DataPoint dp = dpr.getDataPoint();
		List<String> fields = dpr.getResponseFields();

		body.addTriplePattern(Triple.create(Var.alloc("datapoint"), 
				NodeFactory.createURI(NS.sbms + "hasBMSId"),
				Var.alloc(gsv("bmsId"))));

		if(fields.contains("type")) {
			setTypeClause(body, "datapoint", gsv("type"), NS.sbms + "DataPoint", NS.sbms + "DataPoint");
		}

		if(dp.getType() != null) {
			body.addTriplePattern(Triple.create(Var.alloc("datapoint"), 
					NodeFactory.createURI(NS.rdf + "type"),
					NodeFactory.createURI(NS.sbms + dp.getType())));
		}

		body.addTriplePattern(Triple.create(Var.alloc("datapoint"), 
				NodeFactory.createURI(NS.sbms + "expressesObservation"),
				Var.alloc("obs")));

		if(dp.getBmsId() != null) {
			body.addTriplePattern(Triple.create(Var.alloc("datapoint"), 
					NodeFactory.createURI(NS.sbms + "hasBMSId"), 
					NodeFactory.createLiteral(dp.getBmsId()))
					);
		}

		if(fields.contains("source.bimId") 
				|| fields.contains("source.type") 
				|| fields.contains("source.location")
				|| dpr.getGrouping(0).equals("source")
				|| dp.getSource() != null) {
			body.addTriplePattern(Triple.create(Var.alloc("obs"), 
					NodeFactory.createURI(NS.sbms + "observedBy"),
					Var.alloc("source")));

			if(dp.getSource() != null && dp.getSource().getBimId() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("source"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						NodeFactory.createLiteral(dp.getSource().getBimId())));
			}

			if(fields.contains("source.bimId")) {
				body.addTriplePattern(Triple.create(Var.alloc("source"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						Var.alloc(gsv("source.bimId"))));
			}


			if(dp.getSource() != null && dp.getSource().getType() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("source"), 
						NodeFactory.createURI(NS.rdf + "type"),
						NodeFactory.createURI(NS.sbim + dp.getSource().getType())));
			}

			if(fields.contains("source.type")) {
				setTypeClause(body, "source", gsv("source.type"), NS.sbim + "Device", NS.sbms + "Source");
			}

			if((dp.getSource() != null && dp.getSource().getLocation() != null)) {	
				body.addTriplePattern(Triple.create(Var.alloc("source"), 
						//NodeFactory.createURI(NS.sbim + "hasInstallationInRoom"),
						NodeFactory.createURI(NS.dul + "isPartOf"),
						Var.alloc("sourcelocationR")));

				body.addTriplePattern(Triple.create(Var.alloc("sourcelocationR"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						NodeFactory.createLiteral(dp.getSource().getLocation())));
			}

			if(fields.contains("source.location")) {
				body.addTriplePattern(Triple.create(Var.alloc("source"), 
						NodeFactory.createURI(NS.sbim + "hasInstallationInRoom"),
						//NodeFactory.createURI(NS.dul + "isPartOf"),
						Var.alloc("sourcelocationF")));

				body.addTriplePattern(Triple.create(Var.alloc("sourcelocationF"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						Var.alloc(gsv("source.location"))));
			}

		}

		// SCOPE

		if(fields.contains("scope.bimId") 
				|| fields.contains("scope.type") 
				|| fields.contains("scope.location")
				|| dpr.getGrouping(0).equals("scope")
				|| dp.getScope() != null) {

			body.addTriplePattern(Triple.create(Var.alloc("obs"), 
					NodeFactory.createURI(NS.sbms + "featureOfInterest"),
					Var.alloc("scope")));

			if(dp.getScope() != null && dp.getScope().getBimId() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("scope"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						NodeFactory.createLiteral(dp.getScope().getBimId())));
			}

			if(fields.contains("scope.bimId")) {
				body.addTriplePattern(Triple.create(Var.alloc("scope"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						Var.alloc(gsv("scope.bimId"))));
			}


			if(dp.getScope() != null && dp.getScope().getType() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("scope"), 
						NodeFactory.createURI(NS.rdf + "type"),
						NodeFactory.createURI(NS.sbim + dp.getScope().getType())));
			}

			if(fields.contains("scope.type")) {
				setTypeClause(body, "scope", gsv("scope.type"), NS.dul + "PhysicalObject", NS.sbms + "Scope");
			}

			// scopeLoc

			if((dp.getScope() != null && dp.getScope().getLocation() != null)) {
				body.addTriplePattern(Triple.create(Var.alloc("scope"),
						//NodeFactory.createURI(NS.sbim + "hasInstallationLocation"),
						NodeFactory.createURI(NS.dul + "isPartOf"),
						Var.alloc("scopeLocR")
						));
				body.addTriplePattern(Triple.create(Var.alloc("scopeLocR"),
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						NodeFactory.createLiteral(dp.getScope().getLocation())
						));
			}

			if(fields.contains("scope.location")) {
				setOptionalClause(body, new String[] {"scope", "scopeLocF"}, 
						new String[] {NS.sbim + "hasInstallationLocation", NS.sbim + "hasBIMId"}, 
						new String[] {"scopeLocF", gsv("scope.location")});
			}

		}

		if(fields.contains("sensing.type") || fields.contains("sensing.window") || dp.getSensing() != null) {
			body.addTriplePattern(Triple.create(Var.alloc("obs"), 
					NodeFactory.createURI(NS.sbms + "sensingMethodUsed"),
					Var.alloc("sensing")));

			if(dp.getSensing() != null && dp.getSensing().getType() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("sensing"), 
						NodeFactory.createURI(NS.rdf + "type"),
						NodeFactory.createURI(NS.sbms + dp.getSensing().getType())));
			}

			if(dp.getSensing() != null && dp.getSensing().getWindow() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("sensing"), 
						NodeFactory.createURI(NS.sbms + "hasAggregationTimeWindow"),
						NodeFactory.createURI(NS.sbms + dp.getSensing().getWindow())));
			}

			if(fields.contains("sensing.type")) {
				setTypeClause(body, "sensing", gsv("sensing.type"), NS.sbms + "Sensing", NS.sbms + "Sensing");
			}

			if(fields.contains("sensing.window")) {
				setOptionalClause(body, "sensing", NS.sbms + "hasAggregationTimeWindow", gsv("sensing.window"));
			}

		}

		if(fields.contains("property.quality") || fields.contains("property.domain") || dp.getProperty() != null) {
			body.addTriplePattern(Triple.create(Var.alloc("obs"), 
					NodeFactory.createURI(NS.sbms + "observedProperty"),
					Var.alloc("property")));

			if(dp.getProperty() != null && dp.getProperty().getQuality() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("property"), 
						NodeFactory.createURI(NS.sbms + "hasPhysicalQuality"),
						NodeFactory.createURI(NS.ucum + dp.getProperty().getQuality())));
			}

			if(dp.getProperty() != null && dp.getProperty().getDomain() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("property"), 
						NodeFactory.createURI(NS.sbms + "hasPropertyDomain"),
						NodeFactory.createURI(NS.sbms + dp.getProperty().getDomain())));
			}

			if(fields.contains("property.quality")) {
				body.addTriplePattern(Triple.create(Var.alloc("property"), 
						NodeFactory.createURI(NS.sbms + "hasPhysicalQuality"),
						Var.alloc(gsv("property.quality"))));
			}

			if(fields.contains("property.domain")) {
				body.addTriplePattern(Triple.create(Var.alloc("property"), 
						NodeFactory.createURI(NS.sbms + "hasPropertyDomain"),
						Var.alloc(gsv("property.domain"))));
			}

			// INFLUENCING
			if(dp.getInfluence() != null) {
				body.addTriplePattern(Triple.create(Var.alloc("dp"), 
						NodeFactory.createURI(NS.sbms + "influences"),
						Var.alloc(gsv("influenced.property"))));

				if(dp.getInfluence().getScope() != null) {
					body.addTriplePattern(Triple.create(Var.alloc("inflScope"), 
							NodeFactory.createURI(NS.sbms + "hasProperty"),
							Var.alloc(gsv("influenced.property"))));

					body.addTriplePattern(Triple.create(Var.alloc("influenced.scope"), 
							NodeFactory.createURI(NS.sbim + "hasBIMId"),
							NodeFactory.createLiteral(dp.getInfluence().getScope().getBimId())));
				}

				if(dp.getInfluence().getProperty() != null) {
					if(dp.getInfluence().getProperty().getDomain() != null) {
						body.addTriplePattern(Triple.create(Var.alloc("influenced.property"), 
								NodeFactory.createURI(NS.sbms + "hasPropertyDomain"),
								NodeFactory.createURI(NS.sbms + dp.getInfluence().getProperty().getDomain())));
					}

					if(dp.getInfluence().getProperty().getQuality() != null) {
						body.addTriplePattern(Triple.create(Var.alloc("influenced.property"), 
								NodeFactory.createURI(NS.sbms + "hasPhysicalQuality"),
								NodeFactory.createURI(NS.ucum + dp.getInfluence().getProperty().getQuality())));
					}
				}
			}
		}

		// GROUPING
		if(dpr.getGrouping() != null && Fields.GROUPINGS.contains(dpr.getGrouping())) {
			switch(dpr.getGrouping()) {
			case "datapoint.type":
				setTypeClause(body, "datapoint", "group", NS.sbms + "DataPoint", NS.sbms + "DataPoint");
				break;
			case "source.type":
				setTypeClause(body, "source", "group", NS.sbim + "Device", NS.sbms + "Source");
				break;
			case "scope.type":
				setTypeClause(body, "scope", "group", NS.dul + "PhysicalObject", NS.sbms + "Scope");
				break;
			case "scope.room":
			case "scope.floor":
			case "scope.building":
			case "scope.site":
			case "source.room":
			case "source.floor":
			case "source.building":
			case "source.site":
				// source/scope isPartOf group
				/*
				String[] parts = dpr.getGrouping().split("\\.");
				parts[1] = Character.toUpperCase(parts[1].charAt(0)) + parts[1].substring(1);
				*/
				body.addTriplePattern(Triple.create(Var.alloc(dpr.getGrouping(0)), 
						NodeFactory.createURI(NS.dul + "isPartOf"),
						Var.alloc("groupR")));
				// group rdf:type grouping
				body.addTriplePattern(Triple.create(Var.alloc("groupR"), 
						NodeFactory.createURI(NS.rdf + "type"),
						NodeFactory.createURI(NS.sbim + dpr.getGrouping(1))));

				body.addTriplePattern(Triple.create(Var.alloc("groupR"), 
						NodeFactory.createURI(NS.sbim + "hasBIMId"),
						Var.alloc("group")));
				break;
			case Fields.DEFAULT_GROUPING:
			default:
				break;
			}

		}

		return body;
	}

	private static DataPoint createDataPoint(QuerySolution soln, List<String> fields, String grouping) {
		
		logger.debug(soln.toString());
		DataPoint dp = new DataPoint(soln.getLiteral(gsv("bmsId")).getString(), null);
		
		if(fields.contains("type")) {
			dp.setType(getIndividual(gsv("type"), soln));
		}
		
		if(grouping != null) {
			switch(grouping) {
			case "datapoint.type":
			case "source.type":
			case "scope.type":
				dp.setGroup(getIndividual("group", soln));
				break;
			case "scope.room":
			case "scope.floor":
			case "scope.building":
			case "scope.site":
			case "source.room":
			case "source.floor":
			case "source.building":
			case "source.site":
				dp.setGroup(getLiteral("group", soln));
				break;
			case Fields.DEFAULT_GROUPING:
			default:
				dp.setGroup(Fields.DEFAULT_GROUPING);
				break;
			}
		} else {
			dp.setGroup(Fields.DEFAULT_GROUPING);
		}
		
		// SOURCE
		
		if(fields.contains("source.bimId")) {
			dp.setSource(new Source());
			dp.getSource().setBimId(getLiteral(gsv("source.bimId"), soln));
		}
		
		if(fields.contains("source.type")) {
			if(dp.getSource() == null) {
				dp.setSource(new Source());
			}
			dp.getSource().setType(getIndividual(gsv("source.type"), soln));
		}
		
		if(fields.contains("source.location")) {
			if(dp.getSource() == null) {
				dp.setSource(new Source());
			}
			dp.getSource().setLocation(getLiteral(gsv("source.location"), soln));
		}
		
		// SCOPE
		
		if(fields.contains("scope.bimId")) {
			dp.setScope(new Scope());
			dp.getScope().setBimId(getLiteral(gsv("scope.bimId"), soln));
		}
		
		if(fields.contains("scope.type")) {
			if(dp.getScope() == null) {
				dp.setScope(new Scope());
			}
			dp.getScope().setType(getIndividual(gsv("scope.type"), soln));
		}
		
		if(fields.contains("scope.location") && soln.getResource(gsv("scope.location")) != null) {
			if(dp.getScope() == null) {
				dp.setScope(new Scope());
			}
			dp.getScope().setLocation(getIndividual(gsv("scope.location"), soln));
		}
		
		// SENSING
		
		if(fields.contains("sensing.type") && soln.getResource(gsv("sensing.type")) != null) {
			dp.setSensing(new Sensing());
			dp.getSensing().setType(getIndividual(gsv("sensing.type"), soln));
		}
		
		if(fields.contains("sensing.window") && soln.getResource(gsv("sensing.window")) != null) {
			if(dp.getSensing() == null) {
				dp.setSensing(new Sensing());
			}
			dp.getSensing().setWindow(getIndividual(gsv("sensing.window"), soln));
		}
		
		// PROPERTY
		
		if(fields.contains("property.quality")) {
			dp.setProperty(new ObservedProperty());
			dp.getProperty().setQuality(getIndividual(gsv("property.quality"), soln));
		}
		
		if(fields.contains("property.domain")) {
			if(dp.getProperty() == null) {
				dp.setProperty(new ObservedProperty());
			}
			dp.getProperty().setDomain(getIndividual(gsv("property.domain"), soln));
		}
		
		return dp;
		
	}
	
	private static void setTypeClause(ElementGroup body, String fieldName, String fieldTypeName, 
			String rootClass, String ignoredClass) {
		
		/* THE WHOLE QUERY:
		?dataPoint rdf:type ?dataPointClass.
		?dataPointClass rdfs:subClassOf sbms:DataPoint.
				  FILTER (?dataPointClass != sbms:DataPoint
					&& not exists { ?subtype rdfs:subClassOf ?dataPointClass.
					 FILTER (?subtype != ?dataPointClass && ?subtype != sbms:DataPoint && ?subtype != owl:Nothing) }
					).
		*/
		
		// ?dataPoint rdf:type ?dataPointClass.
		body.addTriplePattern(Triple.create(Var.alloc(fieldName), 
				NodeFactory.createURI(NS.rdf + "type"),
				Var.alloc(fieldTypeName)));
		
		// ?dataPointClass rdfs:subClassOf sbms:DataPoint.
		Triple tp = Triple.create(Var.alloc(fieldTypeName), 
				NodeFactory.createURI(NS.rdfs + "subClassOf"),
				NodeFactory.createURI(rootClass));

		body.addTriplePattern(tp);


		
		// ?subtype rdfs:subClassOf ?dataPointClass.
		// FILTER (?subtype != ?dataPointClass && ?subtype != sbms:DataPoint && ?subtype != owl:Nothing)
		ElementGroup b = new ElementGroup();
		b.addTriplePattern(Triple.create(Var.alloc("subtype"), 
				NodeFactory.createURI(NS.rdfs + "subClassOf"),
				Var.alloc(fieldTypeName)));
		
		// FILTER (?subtype != ?dataPointClass && ?subtype != sbms:DataPoint && ?subtype != owl:Nothing)
		b.addElement(new ElementFilter(
				new E_LogicalAnd(
						new E_LogicalAnd(
								// ?subtype != ?dataPointClass
								new E_NotEquals(new ExprVar("subtype"), new ExprVar(fieldTypeName)),
								// ?subtype != sbms:DataPoint
								new E_NotEquals(new ExprVar("subtype"), new NodeValueNode(NodeFactory.createURI(ignoredClass)))	
								),
						// ?subtype != owl:Nothing
						new E_NotEquals(new ExprVar("subtype"), new NodeValueNode(NodeFactory.createURI(NS.owl+"Nothing")))
						)
				)
				);
		
		//FILTER (?dataPointClass != sbms:DataPoint
		//&& not exists { ?subtype rdfs:subClassOf ?dataPointClass.
	    //			 FILTER (?subtype != ?dataPointClass && ?subtype != sbms:DataPoint && ?subtype != owl:Nothing) }
		//		).
		
		body.addElementFilter(new ElementFilter(
				new E_LogicalAnd(
						// ?dataPointClass != sbms:DataPoint
						new E_NotEquals(new ExprVar(fieldTypeName), new NodeValueNode(NodeFactory.createURI(ignoredClass))),
						// not exists { ?subtype rdfs:subClassOf ?dataPointClass.
						// FILTER (?subtype != ?dataPointClass && ?subtype != sbms:DataPoint && ?subtype != owl:Nothing) }
						new E_NotExists(b))));
	}
	
	private static void setOptionalClause(ElementGroup body, String subject, String prop, String object) {
		
		setOptionalClause(body, new String[] {subject}, new String[] {prop}, new String[] {object});
	}
	
	private static void setOptionalClause(ElementGroup body, String[] subjects, String[] props, String[] objects) {
		if(!(subjects.length == props.length && props.length == objects.length)) {
			throw new IllegalArgumentException("different lengths of input arrays");
		}
		ElementTriplesBlock tel = new ElementTriplesBlock();
		for(int i = 0; i < subjects.length; i++) {
			tel.addTriple(Triple.create(Var.alloc(subjects[i]), 
				NodeFactory.createURI(props[i]),
				Var.alloc(objects[i])));
		}
		ElementOptional oel = new ElementOptional(tel);
		body.addElement(oel);
	}
	
	private static String getIndividual(String name, QuerySolution soln) {
		return (soln.getResource(name) != null) ? soln.getResource(name).getLocalName() : null; 
	}
	
	private static String getLiteral(String name, QuerySolution soln) {
		return (soln.getLiteral(name) != null) ? soln.getLiteral(name).getString() : null; 
	}
	
	/*
	queryString = Files.toString(new File("C:\\Users\\Adam\\Documents\\muni\\PhD\\BMS BIM Ontology\\v2\\queries\\findDataPointsByScope.txt"), 
					StandardCharsets.UTF_8);
	*/
	
}
