// Copyright 2021 - Unistra/CNRS
// The MOC API project is distributed under the terms
// of the GNU General Public License version 3.
//
//This file is part of MOC API java project.
//
//    MOC API java project is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, version 3 of the License.
//
//    MOC API java project is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    The GNU General Public License is available in COPYING file
//    along with MOC API java project.
//

package cds.moc;

import java.io.InputStream;
import java.io.OutputStream;


/**
 * The STMoc class implements the methods specific to spatial temporal MOCs.
 * See:  IVOA MOC 2.0 standard => https://www.ivoa.net/documents/MOC/
 * @author Pierre Fernique [CDS]
 * @version 1.0 - April 2021 - creation
 *
 */
public class STMoc extends Moc2D {
   
   private boolean PROTOSTMOC = false;   // Compatibility with proto STMOC
   private SMoc cacheSmocFull;  // cache: SMoc union of all space coverages

   /** STMoc creator */
   public STMoc() {
      super( new TMoc(), new SMoc() );
   }
   
   /** STMoc creator
    * @param timeOrder MocOrder for time dimension [0..61]
    * @param spaceOrder MocOrder for space dimension [0..29]
    */
   public STMoc( int timeOrder, int spaceOrder) throws Exception {
      this();
      setMocOrder(timeOrder, spaceOrder);
   }
   
  /** STMoc creator
    * @param s String containing a STMOC (ASCII, JSON)
    */  
   public STMoc( String s ) throws Exception { 
      this();
      add( s );
   }
   
   /** STMoc creator
    * @param in Input stream containing a STMOC (ASCII, JSON or FITS)
    */
   public STMoc( InputStream in ) throws Exception { 
      this();
      read(in);
   }
   
   /** STMoc creator from one TMOC and one SMOC
    * @param tmoc
    * @param smoc
    * @throws Exception
    */
   public STMoc( TMoc tmoc, SMoc smoc) throws Exception {
      this();
      setMocOrder(tmoc.getMocOrder(), smoc.getMocOrder());
      Range rs = smoc.seeRangeList();
      for( int i=0; i<tmoc.range.sz; i+=2 ) {
         range.append(tmoc.range.r[i], tmoc.range.r[i+1], rs );
      }
   }
   
   /** STMoc view from a SMoc
    * By associating a full time range to the space coverage 
    * @param moc the SMoc
    * @param timeOrder the timeOrder for the time dimension
    */
   private STMoc( SMoc moc, int timeOrder ) throws Exception {
      this( timeOrder, moc.getMocOrder() );
      if( !moc.isEmpty() ) {
         Range2 r = new Range2(2);
         r.add( 0L, TMoc.NBVAL_T, moc.seeRangeList() );
         setRangeList( r );
      }
   }
   
   /** STMoc view from a TMoc
    * By associating a full space range for each time ranges
    * @param moc the TMoc
    * @param timeOrder the spaceOrder for the space dimension
    */
    private STMoc( TMoc moc, int spaceOrder ) throws Exception {
      this( moc.getMocOrder(), spaceOrder );
      if( !moc.isEmpty() ) {
         Range2 r = new Range2( moc.range.sz );
         SMoc allsky = new SMoc("0/0-11");
         for( int i=0; i<moc.range.sz; i+=2 ) {
            r.append(moc.range.r[i], moc.range.r[i+1], allsky.seeRangeList() );
         }
         setRangeList( r );
      }
   }
   
   /** Create a STMoc from any kind of MOC by adding full space or time coverage if required 
    * WARNING: it is not a copy, may share data from the original Moc
    */
   static protected STMoc asSTMoc(Moc moc) throws Exception {
      moc.flush();
      if( moc instanceof STMoc )  return (STMoc) moc;
      if( moc instanceof TMoc )   return new STMoc( (TMoc)moc, moc.getSpaceOrder() );
      return new STMoc( (SMoc)moc, moc.getTimeOrder() );
   }

   
   /** Clone Moc (deep copy) */
   public STMoc clone() throws CloneNotSupportedException {
      STMoc moc = dup();
      clone1( moc );
      return moc;
   }
   
   /** Deep copy. The source is this, the target is the Moc in parameter */
   protected void clone1( Moc moc ) throws CloneNotSupportedException {
      if( !(moc instanceof STMoc) ) throw new CloneNotSupportedException("Uncompatible type of MOC for clone. Must be STMoc");
      ((STMoc)moc).cacheSmocFull=cacheSmocFull==null ? null : cacheSmocFull.clone();
      super.clone1( moc );
   }
   
   /** Create and instance of same class, same sys, but no data nor mocorder */
   public STMoc dup() { 
      STMoc moc = new STMoc();
      moc.protoDim1.sys=protoDim1.sys;
      moc.protoDim2.sys=protoDim2.sys;
      return moc;
   }
   
   
   /** Clear the MOC - data only (not the properties, nor the mocOrder) */
   public void clear() { 
      super.clear();
      cacheSmocFull=null;
   }
   
   protected  void resetCache() {
      super.resetCache();
      cacheSmocFull=null;
   }
   
   /** Return approximatively the amount of memory used for storing this MOC in RAM (in bytes) */
   public long getMem() { 
      long mem = super.getMem();
      if( cacheSmocFull!=null ) mem+=cacheSmocFull.getMem();
      return mem;
   }

   
   /************************************ STMoc specifical methods *****************************************/
   
   
   public void add(HealpixImpl healpix,double alpha, double delta, double jdmin, double jdmax) throws Exception {
      long smin = healpix.ang2pix(SMoc.MAXORD_S, alpha, delta);
      long tmin = Double.isNaN(jdmin) ? 0L : (long)(jdmin*TMoc.DAYMICROSEC);
      long tmax = Double.isNaN(jdmax) ? TMoc.NBVAL_T : (long)(jdmax*TMoc.DAYMICROSEC);
      add(tmin,tmax,smin,smin);
   }

   /** Adding one élément by spaceOrder/npix et [jdmin..jdmax] */
   public void add(int order, long npix, double jdmin, double jdmax) throws Exception  {
      long smin = getStart2(order,npix);
      long smax = getEnd2(order,npix)-1L;
      long tmin = Double.isNaN(jdmin) ? 0L : (long)(jdmin*TMoc.DAYMICROSEC);
      long tmax = Double.isNaN(jdmax) ? TMoc.NBVAL_T : (long)(jdmax*TMoc.DAYMICROSEC);
      add( tmin, tmax, smin, smax );
   }
   
   public void add(long tmin, long tmax, long smin, long smax) throws Exception  {
      add( TMoc.MAXORD_T, tmin, tmax, SMoc.MAXORD_S, smin, smax );
   }
   
   public void add( double jdmin, double jdmax, SMoc smoc) throws Exception  {
      long tmin = Double.isNaN(jdmin) ? 0L : (long)(jdmin*TMoc.DAYMICROSEC);
      long tmax = Double.isNaN(jdmax) ? TMoc.NBVAL_T : (long)(jdmax*TMoc.DAYMICROSEC);
      add(tmin,tmax, new Range( smoc.seeRangeList() ) );
   }
   
   /** Set time order [0..61] */
   public void setTimeOrder( int timeOrder ) throws Exception { setMocOrder1( timeOrder ); }
   
   /** Set space order [0..29] */
   public void setSpaceOrder( int spaceOrder ) throws Exception { setMocOrder2( spaceOrder ); }
   
   /** Get time order */
   public int getTimeOrder()  { return getMocOrder1(); }
   
   /** Get space order */
   public int getSpaceOrder() { return getMocOrder2(); }
   
   /** Set alternative Coosys. All celestial SMoc must be expressed in ICRS (see IVOA MOC 2.0)
    * but alternative is possible for other sphere coverage notably the planets
    * @param coosys alternative coosys keyword (not standardized in IVOA document)
    */
   public void setSpaceSys( String coosys ) { protoDim2.setSys( coosys ); }
   
   /** Get the Coosys. See setSpaceSys() */
   public String getSpaceSys() { return protoDim2.getSys(); }
   
   /** Set alternative Timesys. All celestial STMOC must be expressed in TCD (see IVOA MOC 2.0)
    * but alternative is possible for other coverage notably the planets
    * @param sys alternative timesys keyword (not standardized in IVOA document)
    */
   public void setTimeSys( String timesys ) { protoDim1.setSys( timesys ); }
   
   /** Get the Timesys */
   public String getTimeSys() { return protoDim1.getSys(); }
   
   /** Return minimal time in JD - -1 if empty*/
   public double getTimeMin() {
      if( isEmpty() ) return -1;
      return range.begins(0) / TMoc.DAYMICROSEC;
   }

   /** Return maximal time in JD - -1 if empty*/
   public double getTimeMax() {
      if( isEmpty() ) return -1;
      return range.ends( range.nranges()-1 ) / TMoc.DAYMICROSEC;
   }
   
   public int getTimeRanges() { return getNbRanges(); }
   
   /** TMoc covering from the whole STMOC */
   public TMoc getTimeMoc() throws Exception {
      TMoc moc = new TMoc( getTimeOrder() );
      moc.setRangeList( new Range(range) );
      return moc;
   }

   /** TMoc from the intersection with the spaceMoc */
   public TMoc getTimeMoc( SMoc spaceMoc) throws Exception {
      if( spaceMoc==null || spaceMoc.isEmpty() ) return getTimeMoc();
      TMoc moc = new TMoc( getTimeOrder() );
      Range r1 = new Range();
      
      for( int i=0; i<range.sz; i+=2 ) {
         Range m = range.rr[i>>>1];
         if( spaceMoc.range.overlaps(m) ) r1.append( range.r[i], range.r[i+1] );
      }
      
      moc.range = r1;
      return moc;
   }
   
   /** SMoc covering the whole STMOC */
   public SMoc getSpaceMoc() throws Exception {
      return getSpaceMoc(-1,Long.MAX_VALUE);
   }
   

   /** SMoc extraction from a temporal time
    * @param tmin  min of range (order 61)
    * @param tmax max of range (included - order 61)
    */
   public SMoc getSpaceMoc(long tmin,long tmax) throws Exception {
      if( tmin>tmax ) throw new Exception("bad time range");

      // STMOC vide => SMOC vide
      if( range.sz==0 ) {
         SMoc moc = new SMoc( getSpaceOrder() );
         moc.setSpaceSys( getSpaceSys() );
      }
      
      // Global ? use a cache
      boolean isSFull = tmin<=range.r[0] && tmax>=range.r[ range.sz-1 ];
      if( isSFull ) {
         
         // Un seul intervalle temporel => rien à agréger
         if( range.sz==2 ) {
            SMoc moc = new SMoc( getSpaceOrder() );
            moc.setSpaceSys( getSpaceSys() );
            moc.setRangeList(range.rr[0]);
            return moc;
         }
         
         // Déja calculé et disponible en cache ?
         if( cacheSmocFull!=null ) return cacheSmocFull;
      }
      
      int pos = range.indexOf(tmin);
      if( (pos&1)==1 ) {
         if( pos<0 ) pos++;
         else pos--;
      }
      
//      long t0 = System.currentTimeMillis();
      
      SMoc moc = new SMoc( getSpaceOrder() );
      moc.setSpaceSys( getSpaceSys() );
      moc.bufferOn(2000000);
      for( int i=pos; i<range.sz; i+=2 ) {
         if( range.r[i]>tmax ) break;
         Range m = range.rr[i>>>1];
         for( int j=0; j<m.sz; j+=2 ) moc.add(SMoc.MAXORD_S, m.r[j], m.r[j+1]-1L );
      }
      moc.bufferOff();
//      long dt = System.currentTimeMillis()-t0;
      
      // Mémorisation en cache si le calcul est trop lent
      if( isSFull /* && dt>1 */ ) {
//         System.out.println("getSpaceMoc in "+dt+"ms");
         cacheSmocFull=moc;
      }
      
      return moc;
   }

   /** True if the npix (deepest level) and jd date is in the STMoc */
   public  boolean contains(long npix, double jd) {
      long npixTime = (long)( jd * TMoc.DAYMICROSEC );
      int i = range.indexOf(npixTime);
      if( (i&1)!=0 ) return false;
      return range.rr[i/2].contains(npix);
   }

   /***************************************************** Operations ******************************************/
   
   public boolean isIncluding(Moc moc) throws Exception { 
      if( moc instanceof SMoc ) return getSpaceMoc().isIncluding(moc);
      if( moc instanceof TMoc ) return getTimeMoc().isIncluding(moc);
      flush();
      return range.contains( moc.seeRangeList() );
   }   
   
   public boolean isIntersecting(Moc moc) throws Exception { 
      if( moc instanceof SMoc ) return getSpaceMoc().isIntersecting(moc);
      if( moc instanceof TMoc ) return getTimeMoc().isIntersecting(moc);
      flush(); 
      return range.overlaps( moc.seeRangeList() );
   }
   
   /** Return the Union with another Moc */
   public STMoc union(Moc moc) throws Exception {
      return (STMoc) super.union( asSTMoc(moc));
   }

   /** Return the subtraction with another Moc */
   public STMoc subtraction(Moc moc) throws Exception {
      return (STMoc) super.subtraction( asSTMoc(moc) );
   }
   
   /** Return the Intersection with another Moc */
   public STMoc intersection(Moc moc) throws Exception {
      return (STMoc) super.intersection( asSTMoc(moc) );
   }
   
   /** Return the complement */
   public STMoc complement() throws Exception {
      STMoc moc = new STMoc(  getTimeOrder() , getSpaceOrder());
      moc.add("t0/0 s0/0-11");
      return moc.subtraction(this);
   }

   /*************************************************************** I/O *****************************************************/

   /** Write specifical STMOC properties  */
   protected int writeSpecificFitsProp( OutputStream out  ) throws Exception {
      int n=0;
      out.write( getFitsLine("MOCDIM","TIME.SPACE","STMOC: Time dimension first, ") );  n+=80;      
      out.write( getFitsLine("ORDERING","RANGE","Range coding") );                      n+=80;      
      out.write( getFitsLine("MOCORD_T",""+ getTimeOrder(),"Time MOC resolution") );    n+=80;      
      out.write( getFitsLine("MOCORD_S",""+ getSpaceOrder(),"Space MOC resolution") );  n+=80;      
      out.write( getFitsLine("COORDSYS",getSpaceSys(),"Space reference frame") );       n+=80;
      out.write( getFitsLine("TIMESYS",getTimeSys(),"Time ref system") );               n+=80;
      return n;
   }
   
   protected long codeDim1(long a) { return codeTime(a); };


   /** Internal method: read FITS data according to the type of MOC.
    * @param in The input stream
    * @param naxis1 size of FITS row (in bytes) (generally ==nbyte, but may be 1024 for buffering)
    * @param naxis2 number of values
    * @param nbyte size of each value (in bytes)
    * @param header HDU1 header
    * @throws Exception
    */
   protected void readSpecificData( InputStream in, int naxis1, int naxis2, int nbyte, HeaderFits header) throws Exception {
      
      int timeOrder=-1,spaceOrder=-1;

      // MOC V2.0
      String type = header.getStringFromHeader("MOCDIM");
      if( type!=null ) {
         timeOrder  = header.getIntFromHeader("MOCORD_T");
         spaceOrder = header.getIntFromHeader("MOCORD_S");
         
      // For compatibility with STMOC protos
      } else {
         type = header.getStringFromHeader("MOC");
         if( type==null || type.equals("SPACETIME") ) {
            timeOrder  = header.getIntFromHeader("TORDER")*2+3;
            spaceOrder = header.getIntFromHeader("MOCORDER");
         } else {
            timeOrder  = header.getIntFromHeader("MOCORDER")*2+3;
            spaceOrder = header.getIntFromHeader("MOCORD_1");
         }
         PROTOSTMOC=true;
      }
      
      setTimeOrder(timeOrder);
      setSpaceOrder(spaceOrder);
      
      byte [] buf = new byte[naxis1*naxis2];
      readFully(in,buf);
      readSpecificDataRange((naxis1*naxis2)/nbyte,buf,RAW);
   }
   
   protected boolean isCodedTime(long a)  {
      if( PROTOSTMOC ) return a<0;
      return super.isCodedTime(a);
   }
   
   protected long decodeTime(long a) {
      if( PROTOSTMOC ) return -a;
      return super.decodeTime(a);
   }
   
   protected boolean isCodedDim1(long a) { return isCodedTime(a); }
   
   protected long decodeDim1(long a) { return decodeTime(a); }

}
