
import java.util.logging.Logger;
import java.util.logging.Level;

import java.security.Principal;

import java.time.Instant;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
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;

import vo.parameter.*;
import vo.error.*;

public class ServletCutout extends HttpServlet
{
   protected static final Logger      LOGGER     = Logger.getLogger(ServletCutout.class.getName());
   protected static final Settings    settings   = Settings.getInstance();
   protected static final Subsurvey[] subsurveys = Subsurvey.loadSubsurveys(settings.fitsPaths.surveysMetadataAbsPathname());

   protected boolean resolveFromId    = true;//FIXME separate setting authz is separate table settings.dbConn.isDbUriEmpty(); 
   protected boolean useEngineOverCli = settings.amqpConn.isHostnameEmpty();

   final String RESPONSE_ENCODING      = "utf-8";
   final String DEFAULT_RESPONSEFORMAT = "application/fits";

   public void init() throws ServletException
   {
      LOGGER.config("FITS : " + settings.fitsPaths.toString());
      if(subsurveys != null)
         LOGGER.config("Subsurveys loaded : " + String.valueOf(subsurveys.length));
      LOGGER.config("DEFAULT_RESPONSEFORMAT : " + DEFAULT_RESPONSEFORMAT);
      LOGGER.config("Resolver : " + (resolveFromId    ? "IVOID" : "DB"));
      LOGGER.config("Engine   : " + (useEngineOverCli ? "CLI"   : "AMQP"));
      if(!useEngineOverCli)
         LOGGER.config("AMQP : " + settings.amqpConn.toString());
   }


   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=\"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=\"PIXELS\" ucd=\"instr.pixel;meta.dataset\" datatype=\"char\" arraysize=\"*\" value=\"\"/>"
         +  "<PARAM name=\"RESPONSEFORMAT\" ucd=\"meta.code.mime\" datatype=\"char\" arraysize=\"*\" value=\"application/fits\"/>"

         +  "<PARAM name=\"POSSYS\" ucd=\"pos.frame\" datatype=\"char\" arraysize=\"*\" value=\"\">"
         +   "<DESCRIPTION>Coordinate system for POS values</DESCRIPTION>"
         +   "<VALUES>"
         +     "<OPTION>ICRS</OPTION>"
         +     "<OPTION>GALACTIC</OPTION>"
         +   "</VALUES>"
         +  "</PARAM>"

         +  "<PARAM name=\"BANDSYS\" ucd=\"spect;pos.frame\" datatype=\"char\" arraysize=\"*\" value=\"\">"
         +   "<DESCRIPTION>Coordinate system for BAND vlaues.</DESCRIPTION>"
         +   "<VALUES>"
         +     "<OPTION>WAVE_Barycentric</OPTION>"
         +     "<OPTION>VELO_LSRK</OPTION>"
         +   "</VALUES>"
         +  "</PARAM>"

         + "</GROUP>"
         + "</RESOURCE>"
         + "</VOTABLE>";

      writer.println(theDescriptor);
   }


   protected int doCutoutStream(String id,
       Pos pos, Band band, Time time, Pol pol, Pixeli[] pixeli,
       String pixels, OutputStream respOutputStream) throws IOException, InterruptedException
   {
      LOGGER.fine("trace " + pos);

      final Resolver resolver = (resolveFromId ?
            new ResolverFromId(subsurveys) :
            new ResolverByObsCore(settings.dbConn, subsurveys));

      final Soda soda = new SodaImpl(settings.fitsPaths);

      // if only ID given return header
      boolean headerReq = (id!=null)
                       && (pos==null) && (band==null) && (time==null) && (pol==null)
                       && (pixeli==null)
                       && (pixels==null);

      resolver.resolve(id);

      if(headerReq)
         return soda.doStreamHeader(resolver.relPathname(), resolver.hdunum(),
                                          respOutputStream);
      else if(pixels != null)
         return soda.doStream(resolver.relPathname(), resolver.hdunum(),
                                          pixels, respOutputStream);
      else
         return soda.doStream(resolver.relPathname(), resolver.hdunum(),
														pos, band, time, pol, pixeli, respOutputStream);
   }


   protected CutResult doCutoutFile(String id, Pos pos, Band band, Time time, Pol pol, String pixels,
         boolean countNullValues)
         throws IOException, InterruptedException
      {
         LOGGER.fine("trace");

         FitsCard[] extraCards = null;

         final Resolver resolver = (resolveFromId ?
               new ResolverFromId(subsurveys)
               : new ResolverByObsCore(settings.dbConn, subsurveys));
         final Vlkb vlkb = (useEngineOverCli ?
               new VlkbCli(settings, subsurveys)
               : new VlkbAmqp(settings, subsurveys));

         resolver.resolve(id);

         String subsurveyId = resolver.obsCollection();
         if(subsurveyId != null)
         {
            extraCards = Subsurvey.subsurveysFindCards(subsurveys, subsurveyId);
         }
         else
         {
            LOGGER.fine("Resolver returns subsurveyId null: no extraCards loaded.");
         }

         String cutAbsPathname = settings.fitsPaths.cutouts() + "/"
                          + generateSubimgPathname(resolver.relPathname(), resolver.hdunum());

			if(pixels != null)
            vlkb.doFile(resolver.relPathname(), resolver.hdunum(), pos,band,time,pol, cutAbsPathname);
         else
            vlkb.doFile(resolver.relPathname(), resolver.hdunum(), pixels, cutAbsPathname);


         // VLKB specific: null-value-count and extra-cards

         CutResult cutResult = new CutResult();

         cutResult.fileName = cutAbsPathname;
         cutResult.fileSize = Files.size(Paths.get(cutAbsPathname));
         if(countNullValues)
         {
            cutResult.nullValueCount = vlkb.doCountNullValues(cutAbsPathname, 1);
         }
         if(extraCards == null || (extraCards.length < 1))
         {
            LOGGER.finer("Adding extraCards to cut-file implemented only in VlkbAmql");
         }
         cutResult.pixels = null;

         return cutResult;
      }



   /* HTTP/J2EE -> SODA */


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

   protected void doGet(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         final boolean NO_QUERY_STRING = (request.getQueryString() == null);

         if(NO_QUERY_STRING)
         {
            writeSodaDescriptor(request, response);
            LOGGER.fine("normal exit with SODA service descriptor");
            return;
         }
         else
         {
            LOGGER.info(URLDecoder.decode(request.getQueryString(), "UTF-8"));
            execRequest(request, response);
            LOGGER.fine("normal exit");
         }
      }

   protected void doPost(HttpServletRequest request, HttpServletResponse response)
         throws ServletException, IOException, UnsupportedEncodingException
      {
         final boolean NO_QUERY_STRING = (request.getQueryString() == null);

         if(NO_QUERY_STRING)
         {
            writeSodaDescriptor(request, response);
            LOGGER.fine("normal exit with SODA service descriptor");
            return;
         }
         else
         {
            LOGGER.info(URLDecoder.decode(request.getQueryString(), "UTF-8"));
            execRequest(request, response);
            LOGGER.fine("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();
      }



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

      {
         long    startTime_msec = System.currentTimeMillis();
         long    startTime_nsec = System.nanoTime();

         ServletOutputStream  respOutputStream = response.getOutputStream();

         try
         {
            Map<String, String[]> params = request.getParameterMap();

            String id   = SingleStringParam.parseSingleStringParam(params, "ID");
            Pos    pos  = Pos.parsePos(params);
            Band   band = Band.parseBand(params);
            Time   time = Time.parseTime(params);
            Pol    pol  = Pol.parsePol(params);
            Pixeli[] pixeli = parseMultiplePixeliParam(params, "PIXEL_");
            String pixels = SingleStringParam.parseSingleStringParam(params, "PIXELS");

            String respFormat = sodaReq_getResponseFormat(request, DEFAULT_RESPONSEFORMAT);

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

            if(respFormat.startsWith("application/fits"))
            {
               response.setContentType(respFormat);
               int rc = doCutoutStream(id,pos,band,time,pol,pixeli, pixels, respOutputStream);
               if(rc == 1) response.setStatus(HttpServletResponse.SC_NO_CONTENT);
            }
            else if(respFormat.startsWith("application/x-vlkb+xml"))
            {
               boolean showDuration    = true;
               boolean countNullValues = vlkbReq_getNullValues(request);
               response.setContentType(respFormat);

               CutResult cutResult = doCutoutFile(id, pos, band, time, pol, pixels, countNullValues);// FIXME wh? , respFormat);


               /* FIXME errors from engine not checked - cut-file might not have been created */

               PrintWriter writer =new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

               String accessUrl = convertLocalPathnameToRemoteUrl(cutResult.fileName,
                     settings.fitsPaths.cutouts(),
                     settings.fitsPaths.cutoutsUrl());

               XmlSerializer.serializeToLegacyCutResult(writer, RESPONSE_ENCODING,
                     cutResult, accessUrl,
                     id, pos, band, time, pol, pixels, countNullValues,
                     showDuration, startTime_msec);

               writer.close(); /* must close to force flush to complete the xml */
            }
            else
            {
               throw new IllegalArgumentException("Unsupported RESPONSEFORMAT value : " + respFormat);
            }

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

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

            Lib.doMultiValuedParamNotSupported(ex.getMessage(), writer);

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

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

            Lib.doUsageError(ex.getMessage(), writer);

            writer.close();
         }
         catch(Exception ex)
         {
            LOGGER.severe("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));

            Lib.doError(ex.toString(), writer);

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

         LOGGER.fine("RUNTIME[sec]: "+String.valueOf( (System.nanoTime() - startTime_nsec) / 1000000000.0 ));
      }

   private String convertLocalPathnameToRemoteUrl(String localPathname,
         String FITScutpath, String FITSRemoteUrlCutouts)
   {
      LOGGER.fine("trace " + localPathname);
      String fileName = localPathname.replaceAll(FITScutpath + "/", "");
      LOGGER.finest("local filename: " + fileName);
      String remotefname = FITSRemoteUrlCutouts + "/" + fileName;
      LOGGER.finest("remote url    : " + remotefname);
      return remotefname;
   }


   private  String generateSubimgPathname(String relPathname, int hdunum)
   {
      String cutfitsname = "vlkb-cutout";

      Instant instant = Instant.now() ;
      String timestamp = instant.toString().replace(":","-").replace(".","_");

      String tempPathname1 = relPathname.replaceAll("/","-");
      String tempPathname2 = tempPathname1.replaceAll(" ","_");

      if(hdunum == 1)
      {
         return cutfitsname + "_" + timestamp + "_" + tempPathname2;
      }
      else
      {
         String extnum = "EXT" + String.valueOf(hdunum-1);
         return cutfitsname + "_" + timestamp + "_" + extnum + "_" + tempPathname2;
      }
   }




   /* 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"));
   }

   private Pixeli[] parseMultiplePixeliParam(Map<String, String[]> params, String keyRoot)
   {
      LOGGER.fine("trace");

      final int maxAxes = 5;
      Pixeli[] pixiArr = new Pixeli[maxAxes];
      boolean atLeastOneFound = false;

		for(int i=0; i <maxAxes; i++)
      {
          Pixeli pixi = new Pixeli(0,0,' ');

          String[] valArr = params.get(keyRoot + String.valueOf(i+1));
          if((valArr != null) && valArr.length != 0)
          {
             if(valArr.length > 1)
                  throw new IllegalArgumentException(
                         "MultiValuedParamNotSupported: "
                         + valArr[0] + " was found " + valArr.length + " times");
             else
             {
                // PIXEL_i=m n
                String value = valArr[0];
                String[] parts = value.split(" ");
                if(parts.length != 2)
                {
                   throw new IllegalArgumentException(
                      "PIXLE_i is interval with two space separated values but "
                      + parts.length + " values found: " + value);
                }
                else
                {
                   try
						 {
							 int pix1 = Integer.parseInt(parts[0]);
							 int pix2 = Integer.parseInt(parts[1]);
                      if( pix1 < 0 || pix2 < 0)
                      {
                          throw new IllegalArgumentException(
									 "PIXLE_i interval must be positive integers but: "
                              + String.valueOf(pix1) + " " + String.valueOf(pix2));
                      }
							 pixi = new Pixeli(pix1,pix2,'x');
							 atLeastOneFound = true;
						 }
						 catch(NumberFormatException e)
						 {
							 throw new IllegalArgumentException(
									 "PIXLE_i is interval of integers but : "
									 + e.getMessage());
						 }
					 }
				 }
			 }
			 pixiArr[i] = pixi;
          LOGGER.fine("pixiArr["+ i +"]: " + pixiArr[i].pix1 +" "+ pixiArr[i].pix2
                                      +" " + pixiArr[i].type);
		}
		// dont return empty (0,0,' ') array
		return atLeastOneFound ? pixiArr : null;
	}
}



/* 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
 */
