
import java.util.logging.Logger;

import java.security.Principal;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletOutputStream;

import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

/* for streaming the cutout-file */
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
import java.util.Properties;

// for Logging/Accounting
import org.json.simple.JSONObject;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;


import java.nio.file.StandardOpenOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;


public class ServletCutout extends javax.servlet.http.HttpServlet
{
   protected static final Logger   LOGGER   = Logger.getLogger(ServletCutout.class.getName());
   protected static final Settings settings = Settings.getInstance();

   final String RESPONSE_ENCODING = "utf-8";
   final String DEFAULT_RESPONSEFORMAT = settings.defaults.responseFormat;
   final String DEFAULT_SKY_SYSTEM     = settings.defaults.skySystem;
   final String DEFAULT_SPEC_SYSTEM    = settings.defaults.specSystem;
   final String DEFAULT_TIME_SYSTEM = "MJD_UTC"; // FIXME take from confif file

   boolean showDuration = settings.defaults.showDuration;
   long startTime_msec;

   protected Cutout cutout = new CutoutImpl(settings);

   private Subsurvey[] subsurveys = null;

   public void init() throws ServletException
   {
      super.init();

      LOGGER.info("AMQP : " + settings.amqpConn.toString());
      LOGGER.info("FITS : " + settings.fitsPaths.toString());
      String surveysAbsPathname = settings.fitsPaths.surveysMetadataAbsPathname();
      if( (surveysAbsPathname != null) && (surveysAbsPathname.length() > 1) )
         subsurveys = Subsurvey.loadSubsurveys(surveysAbsPathname);
   }


   protected void doSodaDescriptor(PrintWriter writer, String requestUrl)
   {
      String theDescriptor =
         "<VOTABLE xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://www.ivoa.net/xml/VOTable/v1.3\" version=\"1.3\">"
         + "<RESOURCE type=\"meta\" utype=\"adhoc:service\" name=\"this\">"
         + "<PARAM name=\"standardID\" datatype=\"char\" arraysize=\"*\" value=\"ivo://ivoa.net/std/SODA#sync-1.0\"/>"
         + "<PARAM name=\"accessURL\" datatype=\"char\" arraysize=\"*\" value=\"" + requestUrl  + "\"/>"
         + "<GROUP name=\"inputParams\">"
         +  "<PARAM name=\"ID\" ucd=\"meta.id;meta.dataset\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"POS\" ucd=\"pos.outline;obs\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"CIRCLE\" ucd=\"phys.angArea;obs\" unit=\"deg\" datatype=\"double\" arraysize=\"3\" xtype=\"circle\" value=\"\"/>"
         +  "<PARAM name=\"POLYGON\" unit=\"deg\" ucd=\"pos.outline;obs\" datatype=\"double\" arraysize=\"*\" xtype=\"polygon\"  value=\"\"/>"
         +  "<PARAM name=\"BAND\" ucd=\"em.wl;stat.interval\" unit=\"m\" datatype=\"double\" arraysize=\"2\" xtype=\"interval\" value=\"\"/>"
         +  "<PARAM name=\"TIME\" ucd=\"time.interval;obs.exposure\" unit=\"d\" datatype=\"double\" arraysize=\"2\" xtype=\"interval\" value=\"\"/>"
         +  "<PARAM name=\"POL\" ucd=\"meta.code;phys.polarization\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"RESPONSEFORMAT\" ucd=\"meta.code.mime\" datatype=\"char\" arraysize=\"*\" value=\"application/fits\"/>"
         + "</GROUP>"
         + "</RESOURCE>"
         + "</VOTABLE>";

      writer.println(theDescriptor);
   }


   protected void doCutoutStream(String id, Pos pos, Band band, Time time, Pol pol,
         OutputStream respOutputStream) throws IOException, InterruptedException
   {
      LOGGER.info("trace" + pos);

      Resolver rsl = new ResolverFromId();
      rsl.resolve(id);

      if(pos  != null) pos.setSystem(Pos.System.valueOf(DEFAULT_SKY_SYSTEM));
      if(band != null) band.setSystem(Band.System.valueOf(DEFAULT_SPEC_SYSTEM));
      if(time != null) time.setSystem(Time.System.valueOf(DEFAULT_TIME_SYSTEM));

      cutout.doStream(rsl.relPathname(), rsl.hdunum(), pos, band, time, pol, respOutputStream);
   }


   protected void doCutoutFileStream(String id, Pos pos, Band band, Time time, Pol pol, OutputStream respOutputStream)
      throws IOException
   {
      LOGGER.info("trace" + pos);

      Resolver rsl = new ResolverFromId();
      rsl.resolve(id);



      if(pos  != null) pos.setSystem(Pos.System.valueOf(DEFAULT_SKY_SYSTEM));
      if(band != null) band.setSystem(Band.System.valueOf(DEFAULT_SPEC_SYSTEM));
      if(time != null) time.setSystem(Time.System.valueOf(DEFAULT_TIME_SYSTEM));

      CutResult cutResult = cutout.doFile(rsl.relPathname(), rsl.hdunum(), pos, band, time, pol, false, null);

      Path path = Paths.get(cutResult.filename);
      InputStream inStream = Files.newInputStream(path, StandardOpenOption.DELETE_ON_CLOSE);
      inStream.transferTo(respOutputStream);
      inStream.close();
   }


   protected DataLink doCutoutFile(String id, Pos pos, Band band, Time time, Pol pol,
         boolean countNullValues, String respFormat)
   {
      LOGGER.info("trace");

      String relPathname;
      int hdunum;

      String dbUri = settings.dbConn.uri();

      if((dbUri == null) || dbUri.trim().isEmpty())
      {
         Resolver rsl = new ResolverFromId();
         rsl.resolve(id);
         relPathname = rsl.relPathname();
         hdunum      = rsl.hdunum();
      }
      else
      {
         ResolverByObsCore rsl = new ResolverByObsCore(settings.dbConn, subsurveys);
         rsl.resolve(id);
         relPathname = rsl.relPathname();
         hdunum      = rsl.hdunum();
         String subsurveyId = rsl.obsCollection(); //this implementation assumes ObsCore::obs_collection holds ssID
         FitsCard[] extraCards = null;
         if(subsurveyId != null)
         {
            extraCards = Subsurvey.subsurveysFindCards(subsurveys, subsurveyId);
         }
         else
         {
            LOGGER.info("Resolver with Obscore returns subsurveyId null: no extraCards loaded.");
         }
         // FIXME use of extraCards not implemented
      }

      final String DEFAULT_TIME_SYSTEM = "MJD_UTC"; // FIXME take from confif file

      if(pos  != null) pos.setSystem(Pos.System.valueOf(DEFAULT_SKY_SYSTEM));
      if(band != null) band.setSystem(Band.System.valueOf(DEFAULT_SPEC_SYSTEM));
      if(time != null) time.setSystem(Time.System.valueOf(DEFAULT_TIME_SYSTEM));

      CutResult cutResult = cutout.doFile(relPathname, hdunum, pos, band, time, pol, false, null);

      DataLink dlk = new DataLink();

      dlk.id            = id;
      dlk.accessUrl     = dlk.convertLocalPathnameToRemoteUrl(cutResult.filename,
            settings.fitsPaths.cutouts(), settings.fitsPaths.cutoutsUrl());
      dlk.serviceDef    = null;
      dlk.errorMessage  = null;
      dlk.description   = "A cutout from " + id;// + " by parameters "
                                                // + pos.toString() + " " + band.toString() + " " + time.toString() + " " + pol.toString();
      dlk.semantics     = "http://www.ivoa.net/rdf/datalink/core#proc#cutout";
      dlk.contentType   = "application/fits";
      dlk.contentLength = cutResult.filesize;

      // VLKB-extension to DataLink:
      Coord coord = new Coord(DEFAULT_SKY_SYSTEM, pos, DEFAULT_SPEC_SYSTEM, band, time, pol);
      LOGGER.info(coord.toString());

      dlk.inputs         = new Inputs(id, coord, countNullValues);
      dlk.versionString  = Version.asString;
      dlk.cut            = null;
      dlk.absCutPathname = cutResult.filename;
      dlk.datacubeCount  = 1;
      dlk.nullVals       = ((cutResult.nullValueCount.percent < 0) || (cutResult.nullValueCount.totalCount < 1)) ?
         null : cutResult.nullValueCount;
      dlk.mcutResultArr  = null;

      return dlk;
   }


   protected void doMultiValuedParamNotSupported(String message, PrintWriter printWriter)
   {
      printWriter.println("MultiValuedParamNotSupported : " + message);
   }

   protected void doUsageError(String message, PrintWriter printWriter)
   {
      printWriter.println("UsageError : " + message);
   }

   protected void doError(String message, PrintWriter printWriter)
   {
      printWriter.println("Error : " + message);
   }



   /* HTTP/J2EE -> SODA */


   /* DALI allows GET and POST for sync services */

   protected void doGet(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         startTime_msec = System.currentTimeMillis();

         final boolean NO_QUERY_STRING = (request.getQueryString() == null);

         if(NO_QUERY_STRING)
         {
            writeSodaDescriptor(request, response);
            LOGGER.info("normal exit with SODA service descriptor");
            return;
         }
         else
         {
            convertHttpToSoda(request, response);
            LOGGER.info("normal exit");
         }
      }

   protected void doPost(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         startTime_msec = System.currentTimeMillis();

         final boolean NO_QUERY_STRING = (request.getQueryString() == null);

         if(NO_QUERY_STRING)
         {
            writeSodaDescriptor(request, response);
            LOGGER.info("normal exit with SODA service descriptor");
            return;
         }
         else
         {
            convertHttpToSoda(request, response);
            LOGGER.info("normal exit");
         }
      }



   protected void writeSodaDescriptor(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         PrintWriter writer = new PrintWriter(new OutputStreamWriter(response.getOutputStream(), RESPONSE_ENCODING));
         response.setContentType("text/xml");
         doSodaDescriptor(writer, request.getRequestURL().toString());
         writer.close();
      }


   private Map<SodaParam, String[]> collectSodaParams(HttpServletRequest req)
   {
      Map<SodaParam, String[]> params = new HashMap<SodaParam, String[]>();
      for(SodaParam paramToken : SodaParam.values())
      {
         String[] paramValue = req.getParameterValues(paramToken.toString());
         params.put(paramToken, paramValue);
      }
      return params;
   }



   protected void convertHttpToSoda(HttpServletRequest request, HttpServletResponse response) 
         throws ServletException, IOException, UnsupportedEncodingException

      {
         ServletOutputStream  respOutputStream = response.getOutputStream();

         try
         {
            Map<SodaParam, String[]> params = collectSodaParams(request);
            SodaParser parser = new SodaParser(params);

            String id   = null;
            Pos    pos  = null;
            Band   band = null;
            Time   time = null;
            Pol    pol  = null;

            if(parser.sodaReq_hasSodaId())
            {
               id   = parser.sodaReq_getId();
               pos  = parser.sodaReq_getPosCirclePolygon();
               band = parser.sodaReq_getBand();
               time = parser.sodaReq_getTime();
               pol  = parser.sodaReq_getPol();
            }
            else
            {
               id   = parser.vlkbReq_getPubdid();
               pos  = parser.vlkbReq_getCircleRect();
               band = parser.vlkbReq_getVelocity();
            }

            String respFormat = sodaReq_getResponseFormat(request, DEFAULT_RESPONSEFORMAT);

            LOGGER.info("responseFormat: " + respFormat);

            if(respFormat.equals("application/fits;createfile=yes"))
            {
               response.setContentType(respFormat);
               doCutoutFileStream(id, pos, band, time, pol, respOutputStream);
            }
            else if(respFormat.equals("application/fits"))
            {
               response.setContentType(respFormat);
               doCutoutStream(id, pos, band, time, pol, respOutputStream);
            }
            else if(respFormat.equals("application/x-vlkb+xml"))
            {
               boolean  countNullValues = vlkbReq_getNullValues(request);
               response.setContentType(respFormat);

               DataLink respDataLink = doCutoutFile(id, pos, band, time, pol, countNullValues, respFormat);

               /* FIXME errors from engine not checked - cut-file might not have been created */
               LOGGER.info("DataLink - id:" + respDataLink.id + " url: " + respDataLink.accessUrl );

               final String respEncoding = "utf-8";
               PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));
               XmlSerializer.serializeToLegacyCutResults(writer, respEncoding, respDataLink, showDuration, startTime_msec);
               writer.close(); /* NOTE must close to force flush to complete the xml */
            }
            else
            {
               throw new IllegalArgumentException("Unsupported RESPONSEFORMAT value : " + respFormat);
            }

         }
         catch(MultiValuedParamNotSupported ex)
         {
            LOGGER.info("MultiValuedParamNotSupported: " + ex.getMessage());

            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("text/plain");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

            doMultiValuedParamNotSupported(ex.getMessage(), writer);

            writer.close();
         }
         catch(IllegalArgumentException ex)
         {
            LOGGER.info("IllegalArgumentException: " + ex.getMessage());

            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("text/plain");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

            doUsageError(ex.getMessage(), writer);

            writer.close();
         }
         catch(Exception ex)
         {
            LOGGER.info("Exception: " + ex.getMessage());
            ex.printStackTrace();

            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.setContentType("text/plain");
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

            doError(ex.toString(), writer);

            writer.close();
         }
         finally
         {
            respOutputStream.close();
         }

      }






   /* SODA */


   /* return null if value not present or the value if present exactly once
    * else throw MultiplicityNotSupoorted SODA_error
    */ 
   private String soda_getSingleValue(HttpServletRequest req, String name)
   {
      String[] valArr = req.getParameterValues(name);

      if(valArr == null)
         return null;
      else
         if(valArr.length == 0)
            return null;
         else if(valArr.length == 1)
            return valArr[0];
         else
            throw new IllegalArgumentException(
                  "MultiValuedParamNotSupported: " + name + " was found " + valArr.length + " times");
   }


   private String sodaReq_getResponseFormat(HttpServletRequest req, String defaultResponseFormat)
   {
      String respFormat = soda_getSingleValue(req, "RESPONSEFORMAT");
      return ((respFormat == null) ? defaultResponseFormat : respFormat);
   }


   private boolean vlkbReq_getNullValues(HttpServletRequest request)
   {
      return (null != soda_getSingleValue(request, "nullvals"));
   }

}



/* from SODA (upon error):
   Error codes are specified in DALI. Error documents should be text
   using the text/plain content-type and the text must begin with one of the
   following strings:

   Error CodeDescription
   ---------------------------------------
Error: General error (not covered below)

AuthenticationError: Not authenticated
AuthorizationError: Not authorized to access the resource

ServiceUnavailable: Transient error (could succeed with retry)
UsageError: Permanent error (retry pointless)

MultiValuedParamNotSupported: request included multiple values for a parameter
but the service only supports a single value
*/


/* from DALI (upon successful request):
   The service should set HTTP headers (Fielding and Gettys et al.,
   1999) that are useful to the correct values where possible. Recommended
   headers to set when possible:
   Content-Type
   Content-Encoding
   Content-Length  -- not in SPDA-stream impossible to know
   Last-Modified   -- not in SODA-stream impossible to know
   */
