// Copyright 1999-2022 - Universite de Strasbourg/CNRS
// The Aladin Desktop program is developped by the Centre de Donnees
// astronomiques de Strasbourgs (CDS).
// The Aladin Desktop program is distributed under the terms
// of the GNU General Public License version 3.
//
//This file is part of Aladin Desktop.
//
//    Aladin Desktop 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.
//
//    Aladin Desktop 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 Aladin Desktop.
//

package cds.allsky;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Iterator;

import cds.aladin.Calib;
import cds.aladin.MyProperties;
import cds.aladin.Tok;
import cds.fits.Fits;
import cds.moc.SMoc;
import cds.mocmulti.MultiMoc;
import cds.tools.pixtools.CDSHealpix;
import cds.tools.pixtools.Util;

/**
 * Classe abstraite d'écrivant les actions possibles (voir la méthode Task)
 * @author Pierre Fernique [CDS]
 *
 */
public abstract class Builder {
   
   static public final boolean GAUSS = false;
   static final boolean NEWCUBE = false;
//   static final boolean TRIMBORDER = false;
   static final boolean TESTALLSKY = true;
//   static final boolean GZIPBYDEFAULT = false;

   protected Builder b=null;                 // Subtilité pour faire afficher des statistiques

   protected Context context;
   protected Builder(Context context) { this.context=context; }

   /** Constructeur général pour toutes les actions possibles */
   static public Builder createBuilder(Context context,Action action) throws Exception {
      switch(action) {
         case INDEX:     return new BuilderIndex(context);
//         case TINDEX:    return new BuilderTIndex(context);
         case TILES:     return new BuilderTiles(context);
         case ALLSKY:    return new BuilderAllsky(context);
         case JPEG:      return new BuilderJpg(context);
         case PNG:       return new BuilderPng(context);
         case MOC:       return new BuilderMoc(context);
         case MOCERROR:  return new BuilderMocError(context);
         case MOCINDEX:  return new BuilderMocIndex(context);
         case CLEAN:     return new BuilderClean(context);
         case CLEANALL:  return new BuilderCleanAll(context);
         case CLEANINDEX:return new BuilderCleanIndex(context);
         case CLEANTINDEX:return new BuilderCleanTIndex(context);
         case CLEANDETAILS:return new BuilderCleanDetails(context);
         case CLEANTILES:return new BuilderCleanTiles(context);
         case CLEANFITS: return new BuilderCleanFits(context);
         case CLEANJPEG: return new BuilderCleanJpg(context);
         case CLEANPNG:  return new BuilderCleanPng(context);
         case CLEANDATE: return new BuilderCleanDate(context);
         case CLEANWEIGHT:return new BuilderCleanWeight(context);
         case UPDATE:    return new BuilderUpdate(context);
         case CHECKCODE: return new BuilderCheckCode(context);
         case UPDATEDATASUM: return new BuilderUpdateDataSum(context);
         case CHECK:     return new BuilderCheck(context);
         case CHECKDATASUM: return new BuilderCheckDataSum(context);
         case CHECKFAST: return new BuilderCheckFast(context);
         case LINT:      return new BuilderLint(context);
         case GZIP:      return new BuilderGzip(context);
         case GUNZIP:    return new BuilderGunzip(context);
         case TRIM:      return new BuilderTrim(context);
         case UNTRIM:    return new BuilderUntrim(context);
         case RGB:       return new BuilderRgb(context);
         case TREE:      return new BuilderTree(context);
         case CONCAT:    return new BuilderConcat(context);
         case APPEND:    return new BuilderAppend(context);
         case CUBE:      return new BuilderCube(context);
         case DETAILS:   return new BuilderDetails(context);
         case MAPTILES:  return new BuilderMapTiles(context);
         case PROP:      return new BuilderProp(context);
         case MIRROR:    return new BuilderMirror(context);
         case MAP:       return new BuilderMap(context);
         case SMOC:      return new BuilderSMoc(context);
         case TMOC:      return new BuilderTMoc(context);
         case STMOC:     return new BuilderSTMoc(context);
         case VALIDATOR: return new BuilderValidator(context);
         case CUT:       return new BuilderCut(context);
//         case ZIP:       return new BuilderZip(context);
         default: break;
      }
      throw new Exception("No builder associated to this action");
   }

   /** Valide les préconditions à l'exécution de la tâche */
   public abstract void validateContext() throws Exception;

   /** Retourne true si l'exécution de la tâche est inutile (ex: déjà faite) */
   public boolean isAlreadyDone() { return false; }

   /** Exécute la tâche */
   public abstract void run() throws Exception;

   /** Exécute la tâche sans info technique */
   protected void build() throws Exception {}
      
   /** Retourne l'identificateur de l'action */
   public abstract Action getAction();

   /** Affiche des statistiques de progression */
   public void showStatistics() { }

   /** Indique le mode Just-print - not run -> retourne true si c'est le cas avec un message d'info */
   public boolean isFake() {
      if( !context.fake ) return false;
      context.warning("Action "+getAction()+" not run due to the -n option");
      return true;
   }
   
   /** Retourne le temps d'execution (ms) si connue, -1 sinon */
   protected long execTime=-1L;
   public long getExecTime() { return execTime; }

   // Quelques validateurs génériques utilisés par les différents Builders.

   // Vérifie que le répertoire Input a été passé en paramètre et est utilisable
   protected void validateInput() throws Exception {
      if( context.isValidateInput() ) return;
      String input = context.getInputPath();
      if( input==null ) throw new Exception("Argument \"input\" is required");
      File f = new File(input);
      if( !f.canRead()) throw new Exception("Input not available ["+input+"]");
      if( f.isFile() ) {
         context.info("Unique input image detected");
         context.setFlagInputFile(true);
         if( !input.endsWith(".hhh") ) {
            try {
               Fits f1 = new Fits();
               f1.loadHeaderFITS(input, false);
               new Calib(f1.headerFits);
            } catch( Exception e ) {
               String hhh = Fits.getHHHName(input);
               if( (new File(hhh).exists()) ) {
                  context.setInputPath(hhh);
                  context.info("Switch input file to "+hhh);
               }
            } 
         }

      }
      if( context.isExistingAllskyDir(input) && context.hasPropertyFile(input) ) {
         throw new Exception("The input directory must be a image collection, not a HiPS => aborted");
      }
      context.setValidateInput(true);
   }
   
//   // dans le cas où ce n'est pas un fichier hhh, peut être est-ce un fichier image avec un fichier hhh associé ?
//   if( !rootPath.endsWith(".hhh") ) {
//      try {
//         Fits f = new Fits();
//         f.loadHeaderFITS(rootPath, false);
//         new Calib(f.headerFits);
//
//      }catch( Exception e ) {
//         String hhh = Fits.getHHHName(rootPath);
//         if( (new File(hhh).exists()) ) rootPath=hhh;
//      } 
//   }
//   return rootPath;

   
   
   static private String FS = cds.tools.Util.FS;
   
   
   // Vérifie que le répertoire Output a été passé en paramètre, sinon essaye de le déduire
   // du répertoire Input en ajoutant le suffixe HiPS
   // S'il existe déjà, vérifie qu'il s'agit bien d'un répertoire utilisable
   protected void validateOutput() throws Exception { 
      String output = context.getOutputPath();
      

      String name = null;
      String path = null;
      int i = output==null ? -1 : output.lastIndexOf('/');
      if( i==-1 && output!=null ) i=output.lastIndexOf('\\');      

      // Path indiqué spécifiquement ?
      if( i>=0 ) path = output.substring(0,i);

      // non ! donc par défaut le répertoire contenant l'image ou le répertoire des images originales
      else {
         String input = context.getInputPath();
         if( input!=null && !input.startsWith("http://") && !input.startsWith("https://") ) {
            int j = input.lastIndexOf(FS);
            if( j>=0 ) path = input.substring(0, j);
            else path = ".";

            // Sauf en cas de mirroir => le répertoire courant   
         } else path = ".";
      }

      // Nom indiqué spécifiquement ?
      if( output!=null && i<output.length()-1 ) name = output.substring(i+1);

      // non ! alors on le fabrique depuis l'ID
      else {
         String id = context.getHipsId();

         // Pas d'id => On ajoute simplement le suffixe HiPS au répertoire d'origine
         if( id==null ) {
            String input = context.getInputPath();
            if( input!=null && !input.startsWith("http://") && !input.startsWith("https://") ) {
               int j = input.lastIndexOf(FS);
               name = input.substring(j+1)+Constante.HIPS;

               // sauf s'il s'agit d'un MIRROR, ou alors on le prend tel que
            } else {
               int j = input.lastIndexOf('/');
               name = input.substring(j+1);
            }

            // Un Id => on l'utilise comme nom de répertoire cible (avec des _ à la place des / et ?)
         } else {
            id = id.substring(6);
            id = id.replace('/','_');
            id = id.replace('?','_');
            name = id;
         }
      }

      output = path+FS+name;
      context.setOutputPath( output );

      File f = new File(output);
      if( f.exists() && (!f.isDirectory()  || !f.canRead())) throw new Exception("Ouput directory not available ["+output+"]");
      
      context.info("Output directory: "+output);
      
      
      context.setValidateOutput(true);
      
//      if( true ) System.exit(0);
   }
   
   /** Vérifie que le répertoire HpxIndex existe et peut être utilisé */
   protected void validateIndex() throws Exception {
      String path = context.getHpxFinderPath();
      if( path==null ) throw new Exception("HEALPix index directory [HpxFinder] not defined => specify the output (or input) directory");
      File f = new File(path);
      if( !f.exists() || !f.isDirectory() || !f.canRead() ) throw new Exception("HEALPix index directory not available ["+path+"]");
   }

   // Récupère l'ordre en fonction d'un répertoire. Si un order particulier a été passé en paramètre,
   // vérifie sa cohérence avec celui trouvé
   protected void validateOrder(String path) throws Exception {
      int order = context.getOrder();
      int orderIndex = Util.getMaxOrderByPath( path );
      if( order==-1 || context instanceof ContextGui ) {
         context.info("Order retrieved from ["+path+"] => "+orderIndex);
         context.setOrder(orderIndex);
      } else if( orderIndex!=-1 && order!=orderIndex ) throw new Exception("Detected order ["+orderIndex+"] does not correspond to the param order ["+order+"]");
   }
   
   // Récupère le tile side en fonction d'une tuile pré-existante. 
   protected void validateTileSide(String path) throws Exception {
      int tileSide = context.getTileSide();
      if( tileSide==-1 ) return;
      int tileSideByNpixFile = context.getTileWidthByNpixFile(path);
      if( tileSideByNpixFile!=-1 && tileSide!=tileSideByNpixFile ) {
         context.warning("Size of pre-existing tiles incoherent with declarative tileSize ("+tileSide+"). Will assume "+tileSideByNpixFile);
         context.setTileOrder( (int)CDSHealpix.log2(tileSideByNpixFile ));
      }
   }

   /** Récupération de la profondeur (cube) */
   protected void validateDepth() throws Exception {

      if( context.depthInit ) return;

      // tentative de récupération de la profondeur par une image étalon
      if( !context.isColor() ) {
         String img = context.getImgEtalon();
         if( img==null && context.getInputPath()!=null) {
            img = context.justFindImgEtalon( context.getInputPath() );
            if( img!=null ) context.info("Reference image => "+img);
         }
         if( img!=null ) {
            try { context.setImgEtalon(img); }
            catch( Exception e) { context.warning("Reference image problem ["+img+"] => "+e.getMessage()); }
         }
      }

      // Tentative de récupération de la profondeur par le fichier des properties
      if( !context.depthInit ) {
         try {
            context.loadProperties();
            String s = context.prop.getProperty(Constante.KEY_CUBE_DEPTH);
            if( s==null ) s = context.prop.getProperty(Constante.OLD_CUBE_DEPTH);
            if( s!=null ) {
               int depth = Integer.parseInt(s);
               if( depth>1 ) context.setDepth( depth );
            }
         } catch( Exception e ) { context.warning("Propertie file problem => "+e.getMessage()); }
      }

      if( context.depthInit && context.depth>1 ) {
         String s="";
         if( context.isCubeCanal() ) {
            s=" (crpix3="+context.crpix3+" crval3="+context.crval3+" cdelt3="+context.cdelt3+")";
         }
         context.info("Working on HiPS cube => depth="+context.depth+s);
      }
   }

   // Valide les cuts passés en paramètre, ou à défaut cherche à en obtenir depuis une image étalon
   protected void validateCut() throws Exception {
      if( context.isValidateCut() ) return;
      double [] cut = null;
      //      double [] cut = context.getCut();

      double [] pixelGood = context.pixelGood;
      boolean missingGood = pixelGood!=null && context.good!=null;

      boolean missingCut   = !Context.hasCut(cut);
      boolean missingRange = !Context.hasRange(cut);

      // S'il y a des pixelRange et/ou pixelCut indiqués sur la ligne de commande, il faut les convertir
      // en cut[] à récupérant le bzero et le bscale depuis une tuile ou depuis une image étalon
      double [] pixelRangeCut = context.getPixelRangeCut();
      if( (missingCut  || missingRange ) && pixelRangeCut!=null || missingGood ) {
         try {
            setBzeroBscaleFromPreviousAllsky( context.findOneNpixFile(context.getOutputPath(), "fits"));
         } catch( Exception e ) {

            String img = context.getImgEtalon();
            if( img==null && context.getInputPath()!=null) {
               img = context.justFindImgEtalon( context.getInputPath() );
               if( img!=null ) context.info("Reference image => "+img);
            }
            if( img!=null ) {
               try { context.setImgEtalon(img); }
               catch( Exception e1) { context.warning("Reference image problem ["+img+"] => "+e.getMessage()); }
            }
         }

         try {
            if( cut==null ) cut = new double[7];
            for( int i=0; i<4; i++ ) {
               if( Double.isNaN(pixelRangeCut[i]) ) continue;
               cut[i] = (pixelRangeCut[i] - context.bzero)/context.bscale;
               //               System.out.println("Retreiving from user pixelRangeCut["+i+"]="+pixelRangeCut[i]+" => cut["+i+"]="+cut[i]);
            }

            if( missingGood ) {
               context.good = new double[2];
               for( int i=0; i<2; i++ ) {
                  context.good[i] = (context.pixelGood[i] - context.bzero)/context.bscale;
               }
            }

         } catch( Exception e ) {
            throw new Exception("Cannot retrieve BZERO & BSCALE from previous Allsky.fits file or reference image");
         }
      }

      // S'il me manque le cut du pixelCut ou du pixelRange, il faut que je récupère une image étalon
      // que j'en déduise les cutOrig, bzeroOrig, bscaleOrig, puis que j'en calcule le bzero, bscale et donc les cut
      missingCut   = !Context.hasCut(cut);
      if( missingCut ) {
         String img = context.getImgEtalon();
         if( img==null && context.getInputPath()!=null) {
            img = context.justFindImgEtalon( context.getInputPath() );
            if( img!=null ) context.info("Reference image => "+img);
         }
         if( img!=null ) {
            try { context.setImgEtalon(img); }
            catch( Exception e) { context.warning("Reference image problem ["+img+"] => "+e.getMessage()); }
         }

         context.initParameters();

         double [] imgCut = context.getCut();
         if( cut==null ) cut = new double[7];
         if( missingCut )   {
            cut[Context.CUTMIN]= imgCut[Context.CUTMIN];
            cut[Context.CUTMAX]= imgCut[Context.CUTMAX];
            if( cut[Context.CUTMIN]!=cut[Context.CUTMAX] ) context.info("Estimating pixel cut from the reference image => ["
                     +cut[Context.CUTMIN]+" .. "+cut[Context.CUTMAX]+"]");
         }
      }

      // S'il me manque toujours le pixelCut, je vais tenter de les récupérer par le fichier des properties
      missingCut   = !Context.hasCut(cut);
      if( missingCut ) updateCutByProperties(cut);

      context.setCut(cut);

      double bz=context.bzero;
      double bs=context.bscale;
      if( !context.cutByRegion ) {
         if( cut==null || cut[Context.CUTMIN]==0 && cut[Context.CUTMAX]==0 ) throw new Exception("Argument \"pixelCut\" required");
         if( !( cut[Context.CUTMIN] < cut[Context.CUTMAX] ) ) 
            throw new Exception("pixelCut error ["+ip(cut[Context.CUTMIN],bz,bs)+" .. "+ip(cut[Context.CUTMAX],bz,bs)+"]");
         context.info("pixel cut ["+ip(cut[Context.CUTMIN],bz,bs)+" .. "+ip(cut[Context.CUTMAX],bz,bs)+"]");
      } else {
         context.info("pixel cut by regions");
      }
      context.setValidateCut(true);
   }


   /** Met à jour les champs manquants du cut[] via les infos du fichier des properties */
   protected void updateCutByProperties(double [] cut) {
      try {
         String propFile = context.getOutputPath()+Util.FS+Constante.FILE_PROPERTIES;
         MyProperties prop = new MyProperties();
         File f = new File( propFile );
         if( f.exists() ) {
            InputStreamReader in = new InputStreamReader( new BufferedInputStream( new FileInputStream(propFile) ), "UTF-8");
            prop.load(in);
            in.close();
            String s = prop.getProperty(Constante.KEY_HIPS_PIXEL_CUT);
            if( s==null ) s = prop.getProperty(Constante.OLD_HIPS_PIXEL_CUT);
            context.setPixelCut(s);
            double [] pixelRangeCut = context.getPixelRangeCut();

            // Il me faut alors BZERO  et BSCALE
            try {
//               setBzeroBscaleFromPreviousAllsky(context.getOutputPath()+Util.FS+"Norder3"+Util.FS+"Allsky.fits");
               setBzeroBscaleFromPreviousAllsky( context.findOneNpixFile(context.getOutputPath(), "fits") );
               if( cut==null ) cut = new double[7];
               for( int i=0; i<4; i++ ) {
                  if( Double.isNaN(pixelRangeCut[i]) ) continue;
                  cut[i] = (pixelRangeCut[i] - context.bzero)/context.bscale;
               }
               context.info("Pixel cut from the propertie file => ["+ip(cut[0],context.bzero,context.bscale)+" .. "+ip(cut[1],context.bzero,context.bscale)+"]");
            } catch( Exception e ) { }
         }
      } catch( Exception e ) {}
   }

   protected void validateBitpix() {
      if( context.bitpix!=-1 ) return;
      try {
//         setBzeroBscaleFromPreviousAllsky(context.getOutputPath()+Util.FS+"Norder3"+Util.FS+"Allsky.fits");
         setBzeroBscaleFromPreviousAllsky( context.findOneNpixFile(context.getOutputPath(), "fits") );
      } catch( Exception e ) { }
   }

   protected void validateLabel() {
      if( context.title!=null ) return;
      String label = getALabel(context.getOutputPath(),context.getInputPath());
      if( label!=null && label.length()>0 ) context.title=label;
   }

   protected String getALabel(String outputPath,String inputPath) {
      String label=null;

      // Je vais essayé de le récupérer depuis le fichier des propriétés
      try {
         String propFile = outputPath+Util.FS+Constante.FILE_PROPERTIES;
         MyProperties prop = new MyProperties();
         File f = new File( propFile );
         if( f.exists() ) {
            InputStreamReader in = new InputStreamReader( new BufferedInputStream( new FileInputStream(propFile) ), "UTF-8");
            prop.load(in);
            in.close();
            
            String s=prop.getProperty(Constante.KEY_OBS_TITLE);
            if( s==null ) s=prop.getProperty(Constante.OLD_OBS_COLLECTION);
            if( s==null ) s=context.getLabelFromHipsId( MultiMoc.getID(prop) );
            if( s!=null && s.length()>0 ) label=s;
         }
      } catch( Exception e ) { }
      if( label!=null ) return label;

      // Je vais le construire à partir du nom du répertoire
      if( inputPath!=null ) {
         int offset = inputPath.lastIndexOf(Util.FS);
         if( offset<0 ) offset = inputPath.lastIndexOf('/');
         if( offset>=0 ) label=inputPath.substring(offset+1);
         else label=inputPath;
      } else {
         int offset = outputPath.lastIndexOf(Util.FS);
         if( offset<0 ) offset = outputPath.lastIndexOf('/');
         if( offset>=0 ) label=outputPath.substring(offset+1);
         else label=outputPath;
      }

      return label;
   }

   // Retourne le code HEALPix correspondant au système de référence des coordonnées
   // du survey HEALPix
   protected String getFrame() {
      if( context.hasFrame() ) return context.getFrameCode();
      try {
         if( context.prop==null ) context.loadProperties();
         String s = context.prop.getProperty(Constante.KEY_HIPS_FRAME);
         if( s==null ) s = context.prop.getProperty(Constante.OLD_HIPS_FRAME);
         if( s==null ) s="G";
         if( s.equals("equatorial") ) return "C";
         if( s.equals("ecliptic")) return "E";
         if( s.equals("galactic")) return "G";
         return s;
      } catch( Exception e ) { e.printStackTrace(); }
      return context.getFrameCode();
   }

   protected String ip(double raw,double bzero,double bscale) {
      return cds.tools.Util.myRound(raw) + (bzero!=0 || bscale!=1 ? "/"+cds.tools.Util.myRound(raw*bscale+bzero) : "");
   }

   /**
    * Initialisation des paramètres FITS à partir d'un Allsky.fits précédent
    */
   protected void setFitsParamFromPreviousAllsky(String aFitsTile) throws Exception {
      Fits f = new Fits();

      f.loadFITS(aFitsTile);
      double [] cut = f.findAutocutRange(0,0,true);

      context.setBitpix(f.bitpix);
      context.setCut(cut);
      try {
         double blank = f.headerFits.getDoubleFromHeader("BLANK");
         context.blank=blank;
      } catch( Exception e ) { }
      try {
         double bzero = f.headerFits.getDoubleFromHeader("BZERO");
         context.bzero=bzero;
      } catch( Exception e ) { }
      try {
         double bscale = f.headerFits.getDoubleFromHeader("BSCALE");
         context.bscale=bscale;
      } catch( Exception e ) { }

   }

   protected void setBzeroBscaleFromPreviousAllsky(String aFitsTile) throws Exception {
      Fits f = new Fits();
      f.loadHeaderFITS(aFitsTile);
      try {
         double bzero = f.headerFits.getDoubleFromHeader("BZERO");
         context.bzero=bzero;
      } catch( Exception e ) { }
      try {
         double bscale = f.headerFits.getDoubleFromHeader("BSCALE");
         context.bscale=bscale;
      } catch( Exception e ) { }

      // J'en profite
      if( context.bitpix==-1 ) {
         try {
            double bitpix = f.headerFits.getDoubleFromHeader("BITPIX");
            context.bitpix=(int)bitpix;
         } catch( Exception e ) { }

      }
   }
   
   protected void setTileWidthFromPreviousAllsky(String aFitsTile) throws Exception {
      Fits f = new Fits();
      f.loadHeaderFITS(aFitsTile);
      try {
         int tileWidth = f.headerFits.getIntFromHeader("NAXIS1");
         context.setTileOrder( (int)CDSHealpix.log2(tileWidth));
      } catch( Exception e ) { }
   }     
   
   
   // Gestion des paramètres pour le stockage dans une partition alternative
   private class Part {
      double size;   // taille courante en Ko
      double max;    // taille max de la partition en Ko ou <=0 si non utilisé
      String dir;    // Nom du répertoire root de la partition
   }
   
   static final int MAXPART = 10;   // Nombre max de partitions d'un HiPS
   protected Part part [];            // Gestion des partitions alternatives (mais pas la partition originale)
   
   // Retourne le nombre de Ko correspondant à une expression du genre 4.2g
   private double getMem( String s ) throws Exception {
      char u = Character.toLowerCase( s.charAt( s.length()-1 ) );
      double fct = u=='k' ? 1 : u=='m' ? 1024 : u=='g' ? 1024*1024 : u=='t' ? 1024*1024*1024 : -1;
      if( fct==-1 ) throw new Exception("Invalid mem unit");
      return Double.parseDouble( s.substring( 0,s.length()-1 )) * fct;
   }
   
   // Retourne la taille moyenne d'une tuile (tous les formats additionnés) - en bytes
   private long getTileSize( int bitpix, int width, int depth, String fmt ) {
      
      // Cas d'un catalogue HiPS (on estime à 20Ko chaque tuile)
      if( fmt.toLowerCase().indexOf("tsv")>=0 ) return 20*1024;  
      
      long nbpix = width*width;
      
      // Cas couleur
      if( bitpix==0 ) {
         long nbBytesJpg = fmt.toLowerCase().indexOf("jpeg")>=0 ? (nbpix*4)/10L : 0L;
         long nbBytesPng = fmt.toLowerCase().indexOf("png")>=0  ? (nbpix*4)/8L : 0L;
         long nbBytes = (nbBytesJpg + nbBytesPng ) * depth;
         return nbBytes;
      } 
      
      // Cas classique
      long nbBytesFits = fmt.toLowerCase().indexOf("fits")>=0 ? 2880 + Math.abs(bitpix)/8 * nbpix : 0L;
      long nbBytesJpg = fmt.toLowerCase().indexOf("jpeg")>=0  ? nbBytesFits/10L : 0L;
      long nbBytesPng = fmt.toLowerCase().indexOf("png")>=0   ? nbBytesFits/8L : 0L;
      long nbBytes = (nbBytesFits + nbBytesJpg + nbBytesPng ) * depth;
      return nbBytes;
   }
   
   /** Génération des liens symboliques et des répertoires Dirnn à l'order le plus profond afin de
     * pouvoir par la suite répartir le HiPS sur plusieurs partitions
     */
   protected void validateSplit(String outputPath, String split, SMoc moc, int order, int bitpix, int tileWidth, int depth, String fmt) throws Exception {
      
      // Détermination de la taille totale requise (en Ko)
      moc.setMocOrder(order);
      long numberOfTiles = moc.getNbValues();
      long tileSize = getTileSize( bitpix, tileWidth, depth, fmt);
      if( tileSize==0 ) throw new Exception("No remote tile found");
      long fullSize = (long)( ( 1.3 * tileSize * numberOfTiles + 8 ) / 1024. );
      context.stat("Max order="+order+", Number of tiles="+numberOfTiles+", tile size("+fmt+")="+cds.tools.Util.getUnitDisk( tileSize )+", HiPS size estimation="+ cds.tools.Util.getUnitDisk( fullSize*1024L ) );
      
      // Scan du paramètre de split
      part = new Part[ MAXPART ];
      String s = split;
      Tok tok = new Tok(s,";");
      double firstPartSize = getMem( tok.nextToken() );
//      System.out.println("First part => "+Util.getUnitDisk( (long)( firstPartSize*1024L) ));
      if( fullSize<=firstPartSize ) {
         context.info("Enough space in master partition quota => no split required");
         return;   // tout tient dans la première partition
      }
      for( int n=0; tok.hasMoreTokens(); n++ ) {
         if( n==MAXPART ) throw new Exception("Too many splitting partitions");
         part[n] = new Part();
         String s1 = tok.nextToken();
         int i = s1.lastIndexOf(' ');
         part[n].max = i<0 ? -1 : getMem( s1.substring(i+1) );
         part[n].dir = i<0 ? s1 : s1.substring(0,i);
         
         char a = part[n].dir.charAt(0);
         if( a!='\\' && a!='/' ) throw new Exception("alternative target partitions must use an absolute path");
//         System.out.println(part[n].dir+" => "+Util.getUnitDisk( (long)( part[n].max)*1024L ));
      }
      
      // Détermination de la taille de chaque Dirnn de niveau le plus profond (en Ko)
      HashMap<Long, Double> hashDir = new HashMap<>();
      Iterator<Long> it = moc.valIterator();
      while( it.hasNext() ) {
         long ndirLink = it.next()/10000;
         Double mem = hashDir.get(ndirLink);
         if( mem==null ) hashDir.put(ndirLink, tileSize/1024. );
         else hashDir.put( ndirLink, tileSize/1024. + mem);
      }
      
      // Génération des répertoires et liens symboliques vers les partitions alternatives
      String mainPart = outputPath;   // Path de la partition principale
      int nAltPart = 0;   // indice de la partition alternative courante
      int nlinks=0;       // Nombre de links pour la partition altenative courante
      int totNlinks=0;    // Total du nombre de links vers les partitions alternatives
      boolean first = true;
      
      for( Long norderLink : hashDir.keySet() ) {
         
         // Que va-t-on gagner comme place ?
         double sizeLink = hashDir.get(norderLink);
         fullSize -= sizeLink;
         
         // Plus de place sur la partition alternative courante ? on passe à la partition suivante
         if( part[nAltPart].size > part[nAltPart].max && part[nAltPart].max>0 ) { 
            context.stat(part[nAltPart].dir+" will content "+cds.tools.Util.getUnitDisk( (long)( part[nAltPart].size*1024L))+" thanks to "+nlinks+" links");
            nAltPart++;
            nlinks=0;
         }
         if( nAltPart>=part.length ) throw new Exception("Not enough space on alternative target partitions");
         part[nAltPart].size += sizeLink;
         nlinks++;
         totNlinks++;
         
         // Création du répertoire Ndir sur la partition alternative, ainsi que du lien symbolique associé
         if( !context.fake ) {
            String sOrig = cds.tools.pixtools.Util.getFileDir( mainPart, order, norderLink*10000);
            String sTarg = cds.tools.pixtools.Util.getFileDir( part[nAltPart].dir, order, norderLink*10000);

            File fTarg = new File( sTarg );
            File fOrig = new File( sOrig );

            if( first ) cds.tools.Util.createPath(sOrig);
            fTarg.mkdirs();
            Files.createSymbolicLink(fOrig.toPath(), fTarg.toPath() );
         }
         first=false;
         
         // si on a fait assez de liens symboliques, pas la peine d'aller plus loin
         if( fullSize<firstPartSize ) break;

      }
      
      if( part[nAltPart].size!=0 ) context.stat(part[nAltPart].dir+" will content "+cds.tools.Util.getUnitDisk( (long)( part[nAltPart].size*1024L))+" thanks to "+nlinks+" links");
      int nbdir = (hashDir.size()-totNlinks);
      context.stat(mainPart+" will content "+cds.tools.Util.getUnitDisk( fullSize*1024L )+" (HiPS hierarchy"+(nbdir>0 ?" and "+nbdir+" directories)":")"));

   }
   

   /**
    * Sauvegarde des poids des pixels d'une tuile sous la forme d'une FITS
    * La nomenclature suit la même que pour les tuiles classiques, avec le suffixe "_w"
    * @param file    Le nom de fichier (sans le suffixe "_w" ni l'extension ".fits)
    * @param weight  La matrice des poids
    * @param w       La largeur de la tuile
    * @throws Exception
    */
   public void writeWeight(String file, double [] weight, int w,int order,long npix) throws Exception {
      if( weight==null ) return;
      Fits fits = new Fits(w,w,-32);
      context.updateHeader(fits,order,npix);
      int i=0;
      for( int y=0; y<fits.height; y++ ) {
         for( int x=0; x<fits.width; x++ ) {
            fits.setPixelDouble(x,y,weight[i++]);
         }
      }
      fits.addDataSum();
      fits.writeFITS(file+"_w.fits");
   }

   /**
    *  Chargement des poids des pixels d'une tuile sous la forme d'une FITS
    * La nomenclature suit la même que pour les tuiles classiques, avec le suffixe "_w"
    * @param file    Le nom de fichier (sans le suffixe "_w" ni l'extension ".fits)
    * @return        La matrice des poids - remplie de la valeur par défaut si le fichier n'existe pas
    * @throws Exception
    */
   public double [] loadWeight(String file,int w,double defaultWeight) throws Exception {
      double [] weight = new double[ w *w ];
      String filename = file+"_w.fits";
      if( !(new File(filename)).exists() ) {
         for( int i=0; i<weight.length; i++ ) weight[i]=defaultWeight;
         return weight;
      }
      
      Fits fits = new Fits();
      fits.loadFITS(filename);
      int i=0;
      for( int y=0; y<fits.height; y++ ) {
         for( int x=0; x<fits.width; x++ ) {
            weight[i++] = fits.getPixelDouble(x,y);
         }
      }
      return weight;
   }

   /** Charge une valeur d'un mot clé d'un fichier de properties pour un répertoire particulier, null sinon */
   protected String loadProperty(String path,String key) throws Exception {
      MyProperties prop = loadProperties(path);
      if( prop==null ) return null;
      return prop.getProperty(key);
   }
   
   /** Charge les propriétés d'un fichier properties, retourne null si problème */
   protected MyProperties loadProperties(String path) {
      InputStreamReader in = null;
      try {
         String propFile = path+Util.FS+Constante.FILE_PROPERTIES;
         MyProperties prop = new MyProperties();
         File f = new File( propFile );
         if( f.exists() ) {
            in = new InputStreamReader( new BufferedInputStream( new FileInputStream(propFile) ), "UTF-8");
            prop.load(in);
            in.close();
            in=null;
            return prop;
         }
      } 
      catch( Exception e ) {}
      finally { if( in!=null ) try { in.close(); } catch( Exception e ) {} }
      return null;
   }
   

}
