
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.FileOutputStream;
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

import vo.parameter.*;

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

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


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


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


   private String genRegionForVlkbOverlapCmd(Pos pos, Band band)
   {
      String region = "";

      if(pos != null)
      {
         String skySystem = pos.system.name();

         if(pos.shape.equals("CIRCLE"))
         {
            double l = pos.circle.lon;
            double b = pos.circle.lat;
            double r = pos.circle.radius;
            region = region + "skysystem=" + skySystem + "&l=" + String.valueOf(l) + "&b=" + String.valueOf(b)
               + "&r=" + String.valueOf(r);
         }
         else if(pos.shape.equals("RANGE"))
         {
            double l =  (pos.range.lon1 + pos.range.lon2)/2.0;
            double b =  (pos.range.lat1 + pos.range.lat2)/2.0;
            double dl = (pos.range.lon2 - pos.range.lon1);
            double db = (pos.range.lat2 - pos.range.lat1);
            region = region + "skysystem=" + skySystem + "&l=" + String.valueOf(l) + "&b=" + String.valueOf(b)
               + "&dl=" + String.valueOf(dl) + "&db=" + String.valueOf(db);
         }
         else 
         {
            LOGGER.info("FIXME here Exception: POLYGON not supported or pos.shape invalid: " + pos.shape);
         }

      }

      if(band != null)
      {
         String specSystem = band.system.name();
         double vl = band.getMin();
         double vu = band.getMax();

         region =region + "specsystem=" + specSystem + "&vl=" + String.valueOf(vl) + "&vu=" + String.valueOf(vu);
      }

      return region;
   }

   public void doStream(String relPathname, int hdunum,
         Pos pos, Band band, Time time, Pol pol, String pixels,
         OutputStream outputStream)  throws IOException, InterruptedException
   {
      Instant start = Instant.now();

      boolean has_overlap  = false;
      boolean pixels_valid = (pixels != null);

      String boundsString = "";
      String absPathname = settings.fitsPaths.surveys() + "/" + relPathname;

      if( !pixels_valid )
      {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         if(bos == null)
            throw new AssertionError("byte output stream for bounds was not created, is null");

         JsonEncoder jReq = new JsonEncoder();
         jReq.add(pos);
         jReq.add(band);
         jReq.add(time);
         jReq.add(pol);
         String coordString = jReq.toString();
         LOGGER.info("coordString: " + coordString);

         /* calc bounds */

         String[] cmdBounds = new String[4];
         cmdBounds[0] = "/usr/local/bin/vlkb";
         cmdBounds[1] = "overlap";
         cmdBounds[2] = absPathname;
         cmdBounds[3] = coordString;

         ExecCmd execBounds = new ExecCmd();
         execBounds.doRun(bos, cmdBounds);
         LOGGER.info("execBounds exitValue: " + execBounds.exitValue);

         boolean has_result = (execBounds.exitValue == 0);

         if(has_result)
         {
            boundsString = new String(bos.toByteArray());

            boundsString = replaceWithGrid(boundsString, pos, band, time, pol);
            LOGGER.info("boundsString(with GRID): " + boundsString);

            has_overlap = !((boundsString != null) && boundsString.trim().isEmpty());

            if( !has_overlap )
            {
               throw new IllegalArgumentException(
                     "region in file does not overlap with region defined by SODA parameters");
            }
         }
         bos.close();

         Instant boundsDone = Instant.now();
         LOGGER.info("EXECTIME boundsDone: " + Duration.between(start, boundsDone));
      }

      if(has_overlap || pixels_valid)
      {
         /* cutout -> outputStream */

         String pixFilterString = pixels_valid ? pixels : boundsString;

         String[] cmdCut = new String[6];
         cmdCut[0] = "/usr/local/bin/vlkb";
         cmdCut[1] = "imcopy";
         cmdCut[2] = absPathname;
         cmdCut[3] = String.valueOf(hdunum-1);
         cmdCut[4] = pixFilterString;
         cmdCut[5] = settings.fitsPaths.cutouts();

         if(outputStream == null)
            LOGGER.info("supplied outputStream for cut-file is null");

         ExecCmd execCut = new ExecCmd();
         execCut.doRun(outputStream, cmdCut);

         LOGGER.info("execCut exitValue: " + execCut.exitValue);

         boolean cut_successful = (execCut.exitValue == 0);

         if(!cut_successful)
         {
            throw new IllegalArgumentException("cut by pixels not completed for pixels : " + pixFilterString);
         }

         Instant cutDone = Instant.now();
         LOGGER.info("EXECTIME    cutDone: " + Duration.between(start, cutDone));
      }
      else
      {
         throw new IllegalArgumentException(
               "overlap computation could not be completed with the given arguments");
      }
   }



   private String replaceWithGrid(String wcsBounds, Pos pos, Band band, Time time, Pol pol)
   {
      // remove end-of-line (was added by vlkb_ast.cpp: cout << ... << endl)
      String lineSeparator = System.lineSeparator();
      wcsBounds = wcsBounds.replace(lineSeparator, "");
      LOGGER.info("BOUNDS: " + wcsBounds);

      // replace in wcsBounds those bounds where pos,band,time or pol has system=GRID

      String[] substr = wcsBounds.split("(?=AXES)", 2);
      for(String ss : substr) LOGGER.info("boundsResult: " + ss);

      String boundsString = substr[0];
      boolean noOverlap = ((boundsString != null) && boundsString.trim().isEmpty());
      if(noOverlap)
      {
         boundsString = ""; // no overlap
      }
      else
      {
         String axesTypes = "";
         if(substr.length > 1)
         {
            axesTypes = substr[1].replace("AXES"," ").trim();
            LOGGER.info("AXES TYPES: " + axesTypes);

            String[] bnds  = normalize(boundsString);
            String[] types = normalize(axesTypes);
            // assert: bnds.length == types.length
            LOGGER.info("boundsCount: " + bnds.length  + " typesCount: " + types.length);

            if(bnds.length == types.length)
               boundsString = replaceBounds(bnds, types, pos, band);
         }
      }
      return boundsString;
   }

   private String replaceBounds(String[] bnds, String[] types, Pos pos, Band band)
   {
      int ix;
      for(ix=0; ix<bnds.length; ix++)
      {
         if(types[ix].equals("LON") && (pos.system == Pos.System.GRID))
         {
            bnds[ix] = pos.lonBoundsString();
         }
         else if(types[ix].equals("LAT") && (pos.system == Pos.System.GRID))
         {
            bnds[ix] = pos.latBoundsString();
         }
         else if(types[ix].equals("BAND") && (band.system == Band.System.GRID))
         {
            bnds[ix] = band.boundsString();
         }
      }

      LOGGER.info("replaced: " + String.join(" ", bnds)) ;

      return "[" + String.join(" ", bnds)  + "]";
   }

   // MAKE SURE vlkb overlap returns space delimited bounds: [ a:b c:d ]
   // normalize: strip [,] if any, and split into array by space
   private String[] normalize(String spaceStr)
   {
      String other = spaceStr.replace("[","").replace("]","");
      LOGGER.info("normalize: " + other);
      return other.split("\\s+");
   }


   private NullValueCount doCountNullValues(String absPathname, int hdunum)
         throws IOException, InterruptedException
      {
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         if(bos == null)
            throw new AssertionError("byte output stream for bounds was not created, is null");

         String[] cmdBounds = new String[3];
         cmdBounds[0] = "/usr/local/bin/vlkb";
         cmdBounds[1] = "nullvals";
         cmdBounds[2] = absPathname;

         ExecCmd exec = new ExecCmd();
         exec.doRun(bos, cmdBounds);
         LOGGER.info("exec NullVals exitValue: " + exec.exitValue);

         bos.close();

         boolean hasResult = (exec.exitValue == 0);
         if(hasResult)
         {
            String nullValsString = new String(bos.toByteArray());
            LOGGER.info("vlkb nullvals: " + nullValsString);

            if((nullValsString != null) && nullValsString.trim().isEmpty())
            {
               throw new AssertionError("'vlkb nullvals' returned empty string");
            }

            // parse result: '<fill-ratio> <nullvals-count> <tot-count>'

            String[] splitStr = nullValsString.trim().split("\\s+");
            if(splitStr.length != 3) throw new AssertionError("'vlkb nullvals' did not return 3 numbers but: " + nullValsString);

            NullValueCount nvc = new NullValueCount();
            nvc.percent = Double.parseDouble(splitStr[0]);
            nvc.nullCount = Long.parseLong(splitStr[1]);
            nvc.totalCount = Long.parseLong(splitStr[2]);
            return nvc;
         } 
         else
         {
            throw new AssertionError("'vlkb nullvals' exited without results for: " + absPathname);
         }
      }



   public CutResult doFile(String relPathname, int hdunum,
         Pos pos, Band band, Time time, Pol pol, String pixels,
         boolean countNullValues, FitsCard[] extraCards)
         throws IOException, InterruptedException
      {
         LOGGER.info("trace: " + pos.toString() );

         CutResult cutResult = new CutResult();

         if(settings.amqpConn.isHostnameEmpty())
         {
            LOGGER.info("Using doStream() to local file");

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

            LOGGER.info("Uses local filename : " + absSubimgPathname);

            OutputStream fileOutputStream = new FileOutputStream( new File(absSubimgPathname) );

            doStream(relPathname, hdunum, pos, band, time, pol, pixels, fileOutputStream);

            // engine returns absPathname see common/cutout.cpp::do_cutout_file()
            cutResult.fileName = absSubimgPathname;
            cutResult.fileSize = Files.size(Paths.get(absSubimgPathname));

            if(countNullValues)
            {
               cutResult.nullValueCount = doCountNullValues(absSubimgPathname, 1);
            }

            if(extraCards == null || (extraCards.length < 1))
            {
               LOGGER.info("Adding extraCards to cut-file not implemented when using 'vlkb' exec (implemented in engine vlkbd/AMQP)");
            }

            cutResult.pixels = null;
         }
         else
         {
            LOGGER.info("Using AMQP");

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

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

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

            String outJson = doRpc( jReq.toString() );

            cutResult = JsonDecoder.responseFromCutoutJson( outJson );
         }

         return cutResult;
      }



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

}

