/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package eu.dnetlib.espas.spatial.impl;

import eu.dnetlib.espas.spatial.QShape;
import eu.dnetlib.espas.spatial.QueryCRS;
import eu.dnetlib.espas.spatial.shared.SpatialQueryStatus;
import eu.dnetlib.espas.spatial.TimePeriodConstraint;
import eu.dnetlib.espas.spatial.utils.QueryDBUtils;
import eu.dnetlib.espas.spatial.utils.SQDiskMonitor;
import eu.dnetlib.espas.util.GeometryMetadataHandler;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import javax.xml.bind.JAXBException;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Period;
import org.apache.log4j.Logger;
import org.joda.time.Duration;
import org.postgresql.util.PGInterval;
import org.xml.sax.SAXException;

/**
 *
 * @author gathanas
 */
public class QueryHandler implements Runnable{
   private static final Logger _logger = Logger.getLogger(QueryHandler.class);
   
   private QueryDBUtils dBUtils;
   private QShape spaceConstraint;
   private TimePeriodConstraint timeConstraint;
   private String userId;
   private QueryCRS crs;
   private String uniqQueryId;
   private boolean enableSatelliteSearch = true;
   private SQDiskMonitor diskMonitor;
   private boolean cancel = false;
   private NonPeriodicObservationFilter nonPeriodicFilter;
   private SatelliteObservationFilter satelliteFilter;
   
   QueryHandler(QShape querySpace, QueryCRS crs, TimePeriodConstraint timeConstraint, String userid, QueryDBUtils dBUtils,SQDiskMonitor diskMonitor) {
      this.dBUtils = dBUtils;
      this.spaceConstraint = querySpace;
      this.timeConstraint = timeConstraint;
      this.userId = userid;
      this.crs = crs;
      this.diskMonitor = diskMonitor;
      generateQueryId();
      dBUtils.registerSpatialQuery(uniqQueryId,querySpace,crs,timeConstraint,userid);
   }

   public void setEnableSatelliteSearch(boolean enableSatelliteSearch) {
      this.enableSatelliteSearch = enableSatelliteSearch;
   }

   
   public String getQueryId() {
      return uniqQueryId;
   }

   private void generateQueryId() {
      String representation = "";
      representation+=spaceConstraint.toString()+"#";
      representation+=timeConstraint.toString()+"#"+userId+"#";
      representation+=(new Date()).toString();
      uniqQueryId = ("sq_"+representation.hashCode()).replaceAll("-", "_");
   }

   public void cancelQuery(){
       this.cancel=true;
       if(nonPeriodicFilter!=null)
            nonPeriodicFilter.cancel();
       if(satelliteFilter!=null)
           satelliteFilter.cancel();
   }
   
   @Override
   public void run() {
       if(!cancel)
          dBUtils.updateQueryStatus(uniqQueryId,SpatialQueryStatus.QueryStatus.RUNNING,"");
    
      CountDownLatch latch = new CountDownLatch(2);
      long startTime  = System.currentTimeMillis();
      calculateTimeRelatedStaticInstObs();
      calculateNonPeriodicStaticInstObs(latch);
      calculateSatelliteObs(latch);
      try {
         latch.await();
      } catch (InterruptedException ex) {
         _logger.error( null, ex);
      }
      long endFilterTime = System.currentTimeMillis();
      executeQuery();

      long endQueryTime = System.currentTimeMillis();
      Duration calculations = Duration.millis(endFilterTime-startTime);
      Duration queryTime = Duration.millis(endQueryTime-endFilterTime);

      SpatialQueryStatus.QueryStatus queryStatus = null;
      String statusReport = null;
      
      if(!cancel){
        statusReport = "Request processing completed successfully."
                + " Total Filtering time was : Min:"+calculations.toStandardMinutes()+" Secs"+calculations.getStandardSeconds()+"  ["+(endFilterTime-startTime)+" (msecs)]."
                + " Total Query time was Min:"+queryTime.toStandardMinutes()+" Secs "+queryTime.getStandardSeconds()+"  ["+(endQueryTime-endFilterTime)+" (msecs)]";
         queryStatus = SpatialQueryStatus.QueryStatus.COMPLETED;
      }
      else{
        statusReport = "Request processing was canceled by the user.";
         queryStatus = SpatialQueryStatus.QueryStatus.CANCELED;          
      }
      
      dBUtils.updateQueryStatus(uniqQueryId, queryStatus,statusReport);
      _logger.info("\n\nTotal calculation time was :Min:"+(endQueryTime - startTime)+ ". Total Filtering time was : Min:"+calculations.toStandardMinutes()+" Secs"+calculations.getStandardSeconds()+"  ["+(endFilterTime-startTime)+" (msecs)]."
              + " Total Query time was Min:"+queryTime.toStandardMinutes()+" Secs "+queryTime.getStandardSeconds()+"  ["+(endQueryTime-endFilterTime)+" (msecs)]");
   }

   private void calculateTimeRelatedStaticInstObs() {
      List<Object[]> filteredObservations = new LinkedList<Object[]>();
//            views.observation.id, views.observation.startdate, views.observation.enddate, views.observation.temporalresolution, views.location.location,views.location.srsname
      List<Object[]> periodicStaticInstrumentObs =this.dBUtils.getPeriodicStaticInstrumentObs(this.timeConstraint.getFromDate(),this.timeConstraint.getToDate());
      for(Object[] row:periodicStaticInstrumentObs){
         List<Date> includedTimestamps = findIncludedTimestamps((Date)row[1],(Date)row[2],row[3]);
         includedTimestamps = filterTimestampsByTimeConstraint(includedTimestamps);
         for(Date filteredDate: includedTimestamps)
             if(cancel){
                 return;
             }
         else   {
            String locationXML=null;
            try {
                locationXML = GeometryMetadataHandler.transformGeoLocation(filteredDate,(String)row[4],crs.espasValue());
            } catch (IOException ex) {
               _logger.error(null, ex);
            } catch (JAXBException ex) {
               _logger.error(null, ex);
            } catch (ParseException ex) {
               _logger.error(null, ex);
            } catch (SAXException ex) {
               _logger.error(null, ex);
            }
            
            String obId = (String) row[0];
            filteredObservations.add(new Object[]{uniqQueryId, obId,filteredDate,filteredDate,locationXML});
         }
      }
      if(!cancel)
        this.dBUtils.registerFilteredObservations(uniqQueryId,SQueryObservationType.StaticPeriodic.name(),filteredObservations);
      _logger.info("\n\n Evaluated observations size is :"+periodicStaticInstrumentObs.size());
   }

   private List<Date> findIncludedTimestamps(Date startDate, Date endDate, Object periodDesc) {
      List<Date> results= new LinkedList<Date>();
//      int years, int months, int weeks, int days, int hours, int minutes, int seconds, int millis
      Period period = new Period(((PGInterval)periodDesc).getYears(), ((PGInterval)periodDesc).getMonths(), 0, ((PGInterval)periodDesc).getDays(), ((PGInterval)periodDesc).getHours(),((PGInterval)periodDesc).getMinutes(),(int)((PGInterval)periodDesc).getSeconds(),0);
      DateTime current = new DateTime(startDate.getTime(),DateTimeZone.UTC);
      DateTime endDateTime = new DateTime(endDate.getTime(),DateTimeZone.UTC);
      
      while(current.isBefore(endDateTime)||current.isEqual(endDateTime)){
         results.add(current.toGregorianCalendar().getTime());
         current = current.plus(period);
         }
      
      return results;
   }

   private List<Date> filterTimestampsByTimeConstraint(List<Date> includedTimestamps) {
      List<Date> results = new LinkedList<Date>();
      try {
      for(Date[] timePeriod : this.timeConstraint.getTimeFilter()){
            Date fDate=timePeriod[0], tDate=timePeriod[1];
            if(fDate!=null && tDate!=null)
               for(Date timestamp:includedTimestamps){
               _logger.debug("Evaluating "+timestamp+" against ["+fDate+","+tDate+"]");

                  if(timestamp.getTime()>=fDate.getTime() && timestamp.getTime()<=tDate.getTime())
                     results.add(timestamp);
               }
         }
      } catch (ParseException ex) {
         _logger.error("Failed to filter calculated timestamps by the given constraint", ex);
      }
      _logger.info("Matching timestamps to datetime constraint :"+results.size());
      return results;
   }

   private void executeQuery() {
       try {
           if(!cancel){
           spaceConstraint.transformToCRS(crs);
           _logger.debug("\n Transformed Query constraint is :"+spaceConstraint.getQueryString());
           this.dBUtils.performLocationQuery(uniqQueryId, spaceConstraint);
           }
       } catch (SQLException ex) {
           _logger.error("failed to perform location query for spatial query id :"+uniqQueryId, ex);
       }
   }

   private void calculateNonPeriodicStaticInstObs(CountDownLatch latch) {
      TimePeriodCalculator timePeriodCalculator = new TimePeriodCalculator();
      nonPeriodicFilter = new NonPeriodicObservationFilter(timePeriodCalculator, dBUtils, spaceConstraint, timeConstraint, userId, crs, uniqQueryId);
      Thread worker = new Thread(new QueryWorker(nonPeriodicFilter, latch));
      worker.start();
   }

   private void calculateSatelliteObs(CountDownLatch latch){
      TimePeriodCalculator timePeriodCalculator = new TimePeriodCalculator();
      satelliteFilter = new SatelliteObservationFilter(dBUtils, timeConstraint, userId, crs, uniqQueryId, timePeriodCalculator, spaceConstraint);
      try {
         satelliteFilter.setDiskMonitor(this.diskMonitor);
         Thread worker = new Thread(new QueryWorker(satelliteFilter, latch));
         if(enableSatelliteSearch)
            worker.start();
         else
            latch.countDown();
      } catch (FileNotFoundException ex) {
         _logger.error("Failed while trying to load satellite list from :"+diskMonitor.getSatelliteListFilePath()+". Satellite locations will not be used in location search.", ex);
         latch.countDown();
      }
 }
   
}


class QueryWorker implements Runnable{
   
   private ObservationFilter observationFilter;
   private CountDownLatch latch;

   public QueryWorker(ObservationFilter observationFilter, CountDownLatch latch) {
      this.observationFilter = observationFilter;
      this.latch = latch;
   }
   
   @Override
   public void run() {
      try{
      observationFilter.execute();
      }
      finally{
         latch.countDown();
      }
   }
}
