
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

import java.time.Instant;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.file.StandardOpenOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import java.time.*;// Timestamp in cut-filename
import java.io.ByteArrayOutputStream; // for SODA direct streaming doSubimgStream

class DatasetsImpl implements Datasets
{
   static final Logger LOGGER = Logger.getLogger(DatasetsImpl.class.getName());

   private Settings    settings   = null;
   private Subsurvey[] subsurveys = null;


   public DatasetsImpl()
   {
      LOGGER.info("trace DatasetsImpl()");
      this.settings = Settings.getInstance();
   }


   public DatasetsImpl(Settings settings)
   {
      LOGGER.info("trace DatasetsImpl(settings)");
      this.settings = settings;
   }


   public DatasetsImpl(Settings settings, Subsurvey[] subsurveys)
   {
      LOGGER.info("trace DatasetsImpl(settings, subsurveys)");
      this.settings = settings;
      this.subsurveys = subsurveys;
   }




   public CutResult doMerge(String[] idArr, Coord coord, boolean countNullValues)
         throws FileNotFoundException, IOException
      {
         LOGGER.info("trace");

         return merge(idArr, coord, countNullValues);
      }



   public MCutResult doMCutout(String jdlJson)
         throws IOException
      {
         LOGGER.info("trace");

         String updatedJsonString = JdlMCutout.resolveAndUpdateJsonRequest(jdlJson, settings, subsurveys);

         String outJson;

         if(settings.amqpConn.isHostnameEmpty())
         {
            LOGGER.info("doMCutout with CLI");
            outJson = doCli( JdlMCutout.mcutoutToJson(updatedJsonString) );
         }
         else
         {
            LOGGER.info("doMCutout over AMQP");
            outJson = doRpc( JdlMCutout.mcutoutToJson(updatedJsonString) );
         }

         return JdlMCutout.responseFromMCutoutJson(outJson);
      }



   /* ================= ALL ================================== */



   private String doRpc(String InStr)
   {
      final String userName = "guest";
      final String password = "guest";
      // FIXME move these to Settings

      RpcOverAmqp rpc = new RpcOverAmqp(
            userName, password,
            settings.amqpConn.hostName(),
            settings.amqpConn.portNumber(),
            settings.amqpConn.routingKey());

      rpc.initConnectionAndReplyQueue();

      String OutStr = null;

      try
      {
         LOGGER.info("Sent request : " + InStr);
         OutStr = rpc.callAndWaitReply(InStr);
         LOGGER.info("Got response : " + OutStr);
      }
      catch  (Exception e)
      {
         e.printStackTrace();
      }
      finally
      {
         try
         {
            rpc.close();
         }
         catch (Exception ignore)
         {
            LOGGER.info("ignoring exception on rpc.close():" + ignore.getMessage());
         }
      }

      return OutStr;
   }



   private String doCli(String inStr)
   {

      LOGGER.info("doMCutout in CLI/ExecCmd not implemented");

      /*
      List<CutResult> cutResList = new ArrayList<CutResult>();

      Coord[] coords = Jdl.parseReq(InStr);

      for(Coord coord: coords)
      {
         CutResult cutResult = doFile(coord.relPathname, coord.hdunum,
               coord.pos,  coord.band, coord.time,  coord.pol,  coord.pixels,
               coord.countNullValues,  coord.extraCards);

         cutResList.add(cutResult);
      }

      String outStr = Jdl.encodeToJson(cutResList.toArray());
      */

      return null;//outStr;
   }



   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;
      }
   }


   /* ================= MERGE =============================== */

   private CutResult cutout(
         String publisherDid, Coord coord,
         boolean countNullValues)
   {
      // ResolverByObsCore rsl = new ResolverByObsCore(settings.dbConn, subsurveys);
      Resolver rsl = new ResolverFromId();//settings.dbConn, subsurveys);
      rsl.resolve(publisherDid);

      FitsCard[] extraCards = null;
      //Subsurvey.subsurveysFindCards(subsurveys, rsl.obsCollection());//rsl.subsurveyId);
      String absSubimgPathname = settings.fitsPaths.cutouts() + "/"
         + generateSubimgPathname(rsl.relPathname(), rsl.hdunum());
      LOGGER.info("absSubimgPathname: " + absSubimgPathname);

      LOGGER.info("Using AMQP");

      JsonEncoder jReq = new JsonEncoder();
      jReq.add(rsl.relPathname(), rsl.hdunum());
      jReq.add(coord.pos);
      jReq.add(coord.band);
      jReq.add(coord.time);
      jReq.add(coord.pol);

      // jReq.add(pixels), FIXME implement to supoort PIXLES in vlkb-legacy by AMQP

      jReq.add(countNullValues);
      jReq.add(extraCards);

      String inJson = jReq.toString();

      return JsonDecoder.responseFromCutoutJson( doRpc(inJson) );
   }


   protected CutResult merge(String[] pubdids, Coord coord, Boolean countNullValues)
   {
      LOGGER.info("trace");

      ArrayList<CutResult> allresults = new ArrayList<CutResult>();

      // 1. Decode pubdid's from inputs.pubdid and cutout on each

      CutResult[] allCutResults = do_cutouts(
            pubdids, coord,
            countNullValues);

      allresults.addAll(Arrays.asList(allCutResults));


      String[] allCutPathnames = selectCutPathnames(allCutResults);

      if( allCutPathnames.length <= 0 ){
         LOGGER.warning("No cutout created.");
         return null;
      }
      if( allCutPathnames.length != pubdids.length ) {
         LOGGER.warning("Number of cubes found and number of cutouts created do not match.");
      }

      try
      {
         // 2. regridding (closest neighbour interpolation) for files to be merged

         Regrid grid = new Regrid();
         int dim = grid.dimensions(allCutPathnames);
         if( dim > 2 ) {
            Boolean changed = grid.regrid_vel2(allCutPathnames);
            if(changed){
               //allresults.add("MSG Keywords CDELT3, CRVAL3 were adjusted for merge regridding.");
               LOGGER.info("MSG Keywords CDELT3, CRVAL3 were adjusted for merge regridding.");
            }
         }

         // 3. Merge cut-files

         //String[] strar_results = mergefiles_parallel(id, logFileName,  // logfileName
         //String[] strar_results = mergefiles_split_execution(id, logFileName,  // logfileName
         CutResult strar_results = mergefiles(
               String.valueOf(dim),  // prefix: "2D" or "3D"
               allCutPathnames);     // files to merge

         allresults.addAll(Arrays.asList(strar_results));

      }
      catch(Exception e)
      {
         LOGGER.log(Level.SEVERE, "merge:",e);
         //allresults.add(
         //      "MSG System error. Report time, your IP-number, and the exact request-URL string to service administrator.");
      }

      CutResult[] dlkArr = allresults.toArray(new CutResult[allresults.size()]);
      return dlkArr[0]; // FIXME should return only datalink for the merged file not all cutout files?
   }


   protected CutResult[] do_cutouts(
         String[] publisherDids, Coord coord,
         Boolean countNullValues)
   {
      ArrayList<CutResult> allresults = new ArrayList<CutResult>();
      if(publisherDids.length <= 0)
         return null; // no cube found

      for(String publisherDid : publisherDids)
      {
         CutResult cutout_results_table = cutout(
               publisherDid, coord,
               countNullValues);

         allresults.addAll(Arrays.asList(cutout_results_table));
      }

      return allresults.toArray(new CutResult[allresults.size()]);
   }


   protected CutResult mergefiles(
         String prefix,          // IN prefix added after filename-start-word
         String[] filestomerge)  // IN abs path with filenames to be merged
   {
      LOGGER.info("trace");

      String InJson = JsonEncoderMerge.mergefilesToJson( prefix, filestomerge);
      String OutJson = doRpc(InJson);
      return JsonDecoder.responseFromCutoutJson( OutJson );
   }



   // BEGIN parallel

   protected String[] mergefiles_parallel(
         String jobId,        // IN any identifier to be guaranteed distinct
         String logfilename,  // IN logfilename without path
         String prefix,          // IN prefix added after filename-start-word
         String[] filestomerge)  // IN abs path with filenames to be merged
   {
      LOGGER.info("mergefiles_parallel()");

      String[] responseCH = mergefiles_common_header(jobId, logfilename, prefix, filestomerge);
      for(String sentence : responseCH) DatasetsImpl.LOGGER.info("responseCmnHdr: " + sentence);
      // check if response errored -> abort with 500: Internal Server Error & log details

      int threadsCount = filestomerge.length;
      Thread threadArr[] = new Thread[threadsCount];
      Reproject reprojectArr[] = new Reproject[threadsCount];
      int i;
      for(i=0; i<threadsCount; i++)
         //for(String file : filestomerge)
      {
         String file = filestomerge[i];
         reprojectArr[i] = new Reproject(this, jobId, prefix, file);
         threadArr[i] = new Thread(reprojectArr[i], "reproject: " + String.valueOf(i)); 

         threadArr[i].start();
      }

      // wait until all threads finished

      for(i=0; i<threadsCount; i++)
         //for(Thread thread : threadArr)
      {
         try
         {
            threadArr[i].join();
         }
         catch (InterruptedException e)
         {
            e.printStackTrace();
         }


         for(String sentence : reprojectArr[i].response) DatasetsImpl.LOGGER.info("response[" + String.valueOf(i) + "]: " + sentence);
         if(!isResponseOk(reprojectArr[i].response))
         {
            ;// FIXME response incorrect -> abort merge-job, free resources
             // if incorrect paarams -> respond HTTP.WRONG REQUEST
             // if other error       -> respond HTTP.INTRNAL ERRR & log
         }
      }

      String[] response = mergefiles_add_reprojected(jobId, prefix);
      // check if response errored -> abort with 500: Internal Server Error & log details

      return response;
   }

   private boolean isResponseOk(String[] response)
   {
      // FIXME implement!
      return true;
   }





   protected String[] mergefiles_split_execution(
         String jobId,        // IN any identifier to be guaranteed distinct
         String logfilename,  // IN logfilename without path
         String prefix,          // IN prefix added after filename-start-word
         String[] filestomerge)  // IN abs path with filenames to be merged
   {
      LOGGER.info("mergefiles_split_execution()");

      String[] responseCH = mergefiles_common_header(jobId, logfilename, prefix, filestomerge);
      // check if response errored -> abort with 500: Internal Server Error & log details

      for(String file : filestomerge)// FIXME parallelize on threads & join
      {
         String[] response = mergefiles_reproject(jobId, prefix, file);
         // check if response errored -> abort with: 500: Internal Server Error & log details
      }

      String[] response = mergefiles_add_reprojected(jobId, prefix);
      // check if response errored -> abort with 500: Internal Server Error & log details

      return response;
   }

   protected String[] mergefiles_common_header(
         String jobId,     // IN jobId to distinguish parallel executed requests
         String logfilename,     // IN logfilename without path
         String prefix,          // IN prefix added after filename-start-word
         String[] filestomerge)  // IN abs path with filenames to be merged
   {
      LOGGER.info("trace");

      String InJson = JsonEncoderMerge.mergefilesCommonHeaderToJson(jobId, prefix, filestomerge);
      String OutJson = doRpc(InJson);
      String[] results = null; // FIXME JsonDecoder.responseFromJson(OutJson);

      return results;
   }


   protected String[] mergefiles_reproject(
         String jobId,     // IN jobId to distinguish parallel executed requests
         String prefix,          // IN prefix added after filename-start-word
         String fitsfilename)    // IN logfilename without path
   {
      LOGGER.info("trace");

      String InJson = JsonEncoderMerge.mergefilesReprojectToJson(jobId, prefix, fitsfilename);
      String OutJson = doRpc(InJson);
      String[] results = null; // FIXME JsonDecoder.responseFromJson(OutJson);

      return results;
   }


   protected String[] mergefiles_add_reprojected(
         String jobId,     // IN jobId to distinguish parallel executed requests
         String prefix)          // IN prefix added after filename-start-word
   {
      LOGGER.info("trace");

      String InJson = JsonEncoderMerge.mergefilesAddReprojectedToJson(jobId, prefix);
      String OutJson = doRpc(InJson);
      String[] results = null; // FIXME JsonDecoder.responseFromJson(OutJson);

      return results;
   }

   // END parallel





   // returns selected data in list of strings:
   // -- from cutout: the cutout filename (server local)
   private String[] selectCutPathnames(CutResult[] results) {

      LOGGER.info("selectCutPathnames()");

      // return only data (without MSG's LOG's etc)
      ArrayList<String> data = new ArrayList<String>();

      // sanity check - move after doFunc call (here covered by exception)
      // FIXME consider catch null-pointer-exception
      if(results == null) {
         LOGGER.info("selectCutPathnames: results-table is null.");
         return null;
      }

      for (CutResult res : results) {

         /*/ protect substring() calls below;
         // FIXME consider catch exception index-out-of-bounds
         if(res.length() < 3) {
         LOGGER.warning("Assert(Results.toXML): results msg shorter then 3 chars : " + res);
         continue;
         }

         // decode msg type
         switch(res.substring(0,3)){
         case "URL": // from cutout: the cutout filename for download
         String localfname = res.substring(4);//.replaceAll(FITScutpath, "");
         String[] ssfn = localfname.split(":");
         //String[] ssfn = localfname.substring(4).split(":");
         LOGGER.info("ssfn[0]: " + ssfn[0]);
         LOGGER.info("ssfn[1]: " + ssfn[1]);
         data.add(ssfn[1]);
         //data.add(localfname);
         break;
         case "NVS": // from cutout : NVS_nn:nn:nn
         case "MSG":
         case "LOG":
         case "CUT": // from cutout: the file which was cut
         case "HID": // from search
                     // no data, do nothing
                     break;
                     default:
                     LOGGER.severe("Assert(Results.toXML): results msg has unhandled msgtype code : " + res);
                     }*/
         data.add(res.fileName);
      }

      return data.toArray(new String[data.size()]);
   }


}

