// 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 FTMoc class implements the methods specific to time frequency MOCs.
 * See:  IVOA MOC 2.0 standard => https://www.ivoa.net/documents/MOC/
 * @author Pierre Fernique [CDS]
 * @version 1.0 - February 2023 - creation
 *
 */
public class FTMoc extends Moc2D {
   
   /** FTMoc creator */
   public FTMoc() {
      super( new TMoc(), new FMoc() );
   }
   
   /** FTMoc creator
    * @param timeOrder MocOrder for time dimension [0..61]
    * @param freqOrder MocOrder for frequency dimension [0..59]
    */
   public FTMoc( int timeOrder,int freqOrder ) throws Exception {
      this();
      setMocOrder(timeOrder, freqOrder);
   }
   
  /** FTMoc creator
    * @param s String containing a FTMoc (ASCII, JSON)
    */  
   public FTMoc( String s ) throws Exception { 
      this();
      add( s );
   }
   
   /** FTMoc creator
    * @param in Input stream containing a FTMoc (ASCII, JSON or FITS)
    */
   public FTMoc( InputStream in ) throws Exception { 
      this();
      read(in);
   }
   
   /** SFMoc creator from one TMOC and one FMOC
    * @param tmoc
    * @param fmoc
    * @throws Exception
    */
   public FTMoc( TMoc tmoc, FMoc fmoc ) throws Exception {
      this();
      init( tmoc, fmoc);
   }
   
   /** FTMoc view from a FMoc
    * By associating a full freqency range to the space coverage 
    * @param moc the FMoc
    * @param timeOrder the timeOrder for the time dimension
    */
   private FTMoc( FMoc moc, int timeOrder ) throws Exception {
      this( timeOrder, moc.getMocOrder() );
      initDim2(moc);
   }
   
   /** FTMoc view from a TMoc
    * By associating a full space range for each frequency ranges
    * @param moc the TMoc
    * @param freqOrder the freqOrder for the frequency dimension
    */
    private FTMoc( TMoc moc, int freqOrder ) throws Exception {
      this( moc.getMocOrder(), freqOrder );
      initDim1(moc);
   }
    
   /** Create a FTMoc from any kind of MOC by adding full time or frequency coverage if required 
    * WARNING: it is not a copy, may share data from the original Moc
    */
   static protected FTMoc asFTMoc(Moc moc) throws Exception {
      moc.flush();
      if( moc instanceof FTMoc )  return (FTMoc) moc;
      if( moc instanceof FMoc )   return new FTMoc( (FMoc)moc, moc.getTimeOrder() );
      if( moc instanceof TMoc )   return new FTMoc( (TMoc)moc, moc.getFreqOrder() );
      throw new Exception("No frequency or time dimension");
   }

   
   /** Clone Moc (deep copy) */
   public FTMoc clone() throws CloneNotSupportedException {
      FTMoc 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 FTMoc) ) throw new CloneNotSupportedException("Uncompatible type of MOC for clone. Must be FTMoc");
      super.clone1( moc );
   }
   
   /** Create and instance of same class, same sys, but no data nor mocorder */
   public FTMoc dup() { 
      FTMoc moc = new FTMoc();
      moc.protoDim1.sys=protoDim1.sys;
      moc.protoDim2.sys=protoDim2.sys;
      return moc;
   }
   
   /************************************ FTMoc specifical methods *****************************************/
   
   
   public void add(double jdmin, double jdmax, double freqMin, double freqMax) throws Exception {
      long tmin = Double.isNaN(jdmin) ? 0L : (long)(jdmin*TMoc.DAYMICROSEC);
      long tmax = Double.isNaN(jdmax) ? TMoc.NBVAL_T : (long)(jdmax*TMoc.DAYMICROSEC);
      long fmin = Double.isNaN(freqMin) ? 0L : FMoc.getHash(freqMin);
      long fmax = Double.isNaN(freqMax) ? FMoc.NBVAL_F : FMoc.getHash(freqMax);
      add(tmin,tmax,fmin,fmax);
   }
   
   public void add(long tmin, long tmax, long fmin, long fmax) throws Exception  {
      add( TMoc.MAXORD_T, tmin, tmax, FMoc.MAXORD_F, fmin, fmax );
   }
   
   public void add( double jdmin, double jdmax, FMoc fmoc) 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( fmoc.seeRangeList() ) );
   }
   
   /** Set space order [0..61] */
   public void setTimeOrder( int timeOrder ) throws Exception { setMocOrder1( timeOrder ); }
   
   /** Set time order [0..59] */
   public void setFreqOrder( int freqOrder ) throws Exception { setMocOrder2( freqOrder ); }
   
   /** Get time order */
   public int getTimeOrder() { return getMocOrder1(); }
   
   /** Get Freq order */
   public int getFreqOrder()  { return getMocOrder2(); }
   
   /** 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 FTMOC */
   public TMoc getTimeMoc() throws Exception {
      TMoc moc = new TMoc( getTimeOrder() );
      moc.setRangeList( new Range(range) );
      return moc;
   }

   /** TMoc from the intersection with the freqMoc */
   public TMoc getTimeMoc( FMoc freqMoc) throws Exception {
      if( freqMoc==null || freqMoc.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( freqMoc.range.overlaps(m) ) r1.append( range.r[i], range.r[i+1] );
      }
      
      moc.range = r1;
      return moc;
   }
   
   /** FMoc covering the whole FTMOC */
   public FMoc getFreqMoc() throws Exception {
      return getFreqMoc(-1,Long.MAX_VALUE);
   }
   

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

      // FTMOC vide => TMOC vide
      if( range.sz==0 ) {
         FMoc moc = new FMoc( getFreqOrder() );
         return moc;
      }
      
      // 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 ) {
            FMoc moc = new FMoc( getFreqOrder() );
            moc.setRangeList(range.rr[0]);
            return moc;
         }
         
         // Déja calculé et disponible en cache ?
         if( cacheDim2Full!=null ) return (FMoc) cacheDim2Full;
      }
      
      int pos = range.indexOf(tmin);
      if( (pos&1)==1 ) {
         if( pos<0 ) pos++;
         else pos--;
      }
      
//      long t0 = System.currentTimeMillis();
      
      FMoc moc = new FMoc( getFreqOrder() );
      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(FMoc.MAXORD_F, 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");
         cacheDim2Full=moc;
      }
      
      return moc;
   }

   /** True if jd and freq is in the FTMoc */
   public  boolean contains(double jd, double freq) throws Exception {
      long t = (long)(jd*TMoc.DAYMICROSEC);
      int i = range.indexOf(t);
      if( (i&1)!=0 ) return false;
      long f = FMoc.getHash(freq);
      return range.rr[i/2].contains(f);
   }

   /***************************************************** Operations ******************************************/
   
   public boolean isIncluding(Moc moc) throws Exception { 
      if( moc instanceof FMoc ) return getFreqMoc().isIncluding(moc);
      if( moc instanceof TMoc ) return getTimeMoc().isIncluding(moc);
      if( !(moc instanceof FTMoc) ) throw new Exception("no frequency or time dimension");
      flush();
      return range.contains( moc.seeRangeList() );
   }   
   
   public boolean isIntersecting(Moc moc) throws Exception { 
      if( moc instanceof FMoc ) return getFreqMoc().isIntersecting(moc);
      if( moc instanceof TMoc ) return getTimeMoc().isIntersecting(moc);
      if( !(moc instanceof FTMoc) ) throw new Exception("no frequency or time dimension");
      flush(); 
      return range.overlaps( moc.seeRangeList() );
   }
   
   /** Return the Union with another Moc */
   public FTMoc union(Moc moc) throws Exception {
      return (FTMoc) super.union( asFTMoc(moc));
   }

   /** Return the subtraction with another Moc */
   public FTMoc subtraction(Moc moc) throws Exception {
      return (FTMoc) super.subtraction( asFTMoc(moc) );
   }
   
   /** Return the Intersection with another Moc */
   public FTMoc intersection(Moc moc) throws Exception {
      return (FTMoc) super.intersection( asFTMoc(moc) );
   }
   
   /** Return the complement */
   public FTMoc complement() throws Exception {
      FTMoc moc = new FTMoc(  getTimeOrder() , getFreqOrder());
      moc.add("t0/0 f0/0");
      return moc.subtraction(this);
   }

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

   /** Write specifical SFMOC properties  */
   protected int writeSpecificFitsProp( OutputStream out  ) throws Exception {
      int n=0;
      out.write( getFitsLine("MOCDIM","TIME.FREQUENCY","FTMOC: 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_F",""+ getFreqOrder(),"Frequency MOC resolution") );   n+=80;      
      out.write( getFitsLine("TIMESYS",getTimeSys(),"Time ref system") );                   n+=80;
      return n;
   }
   
   protected long codeDim1(long a) { return codeTime(a); };
   protected long codeDim2(long a) { return codeFreq(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,freqOrder=-1;

      String type = header.getStringFromHeader("MOCDIM");
      if( type!=null ) {
         timeOrder = header.getIntFromHeader("MOCORD_T");
         freqOrder = header.getIntFromHeader("MOCORD_F");
      }  
      
      setTimeOrder(timeOrder);
      setFreqOrder(freqOrder);
      
      byte [] buf = new byte[naxis1*naxis2];
      readFully(in,buf);
      readSpecificDataRange((naxis1*naxis2)/nbyte,buf,RAW);
   }
   
   
   protected boolean isCodedDim1(long a) { return isCodedTime(a); }
   protected long decodeDim1(long a) { return decodeTime(a); }
}
