package ucar.nc2.adde;

import edu.wisc.ssec.mcidas.*;

import ucar.ma2.*;
import ucar.nc2.util.DebugFlags;

import java.io.*;
import java.lang.*;
import java.net.URL;

/**
 * AreaFile interface with McIDAS 'area' file format image data. The
 * data is made into an ucar.ma2.Array object.
 *
 * This will allow 'area' format data to be read from disk; the
 * navigation block is made available (see GVARnav for example).
 *
 * This implementation does not do calibration (other than
 * accounting for its presence in the data).  Also, the 'valcode'
 * is not checked on each line.
 *
 * @author John Caron : adapt to MultiArray, add getAreaNavigation
 * @author Tom Whittaker & Tommy Jasmin at SSEC
 *
 */

public class AreaFile3 {

  // these should be static in AREAnav
  private static final int indexLine=1;
  private static final int indexEle=0;
  private static final int indexLat=0;
  private static final int indexLon=1;

  private int numBands, numLines, numElems, dataSize;
  private int datLoc, linePrefixLength, lineDataLength, lineLength;
  private long position = 0;

  private int[] dir;
  private int[] navBlock;
  private int[] cal;
  private int[] aux;

  private Array data;
  private AreaDirectory areaDirectory = null;
  private AREAnav areaNavigation = null;
  private String imageSource;

  private boolean showBBcalc = false;

  /**
   * Creates an AreaFile object that allows reading
   * of McIDAS 'area' file format image data from a URL
   *
   * @param urlString - the adde url String
   *
   */

  public AreaFile3(String urlString) throws java.io.IOException, java.net.MalformedURLException {
    long timeStart = System.currentTimeMillis();

    URL url = thredds.util.URLStreamHandlerFactory.makeURL( urlString);
    DataInputStream af = new DataInputStream(new BufferedInputStream(url.openStream()));
    readMetaData( af);
    readData( af);

    /* long timeEnd = System.currentTimeMillis();
    if (DebugFlags.isSet("AreaFile/readTiming"))
      System.out.println("Time to read AreaFile = "+ .001*(timeEnd - timeStart)+" sec");

    if ((log != null) && Debug.isSet("AreaFile/showFileStats")) {
      log.println("  numBands = "+ numBands);
      log.println("  numLines = "+ numLines);
      log.println("  numElems = "+ numElems);
      log.println("  dataElementSize = "+ dataSize);
      log.println("  memory = "+ numBands*numLines*numElems*dataSize);
    }

    if ((log != null) && Debug.isSet("AreaFile/showAreaNav")) {
      try {
        AREAnav anav = getAreaNavigation();
        log.println("  nav = "+ anav.getClass().getName()+"=="+anav);
        log.println("  bb = "+ getBoundingBox());
      } catch (AreaFileException afe) {
        throw new IOException( afe.getMessage());
      }
    } */
  }

  public Array getData() { return data; }

  /**
   *  Read the metadata for an area file (directory, nav,  and cal).
   */

  private void readMetaData(DataInputStream af) throws java.io.IOException {

    int i;
    boolean flipwords = false;

    // read dir
    dir = new int[AreaFile.AD_DIRSIZE];
    for (i=0; i < AreaFile.AD_DIRSIZE; i++)
      dir[i] = af.readInt();

    position += AreaFile.AD_DIRSIZE * 4;

    // see if the directory needs to be byte-flipped
    if (dir[AreaFile.AD_VERSION] != AreaFile.VERSION_NUMBER) {
      McIDASUtil.flip(dir,0,19);
      // check again
      if (dir[AreaFile.AD_VERSION] != AreaFile.VERSION_NUMBER)
         throw new IOException("Invalid version number - probably not an AREA file");

      // word 20 may contain characters -- if small integer, flip it...
      if ( (dir[20] & 0xffff) == 0) McIDASUtil.flip(dir,20,20);
      McIDASUtil.flip(dir,21,23);
      // words 24-31 contain memo field
      McIDASUtil.flip(dir,32,50);
      // words 51-2 contain cal info
      McIDASUtil.flip(dir,53,55);
      // word 56 contains original source type (ascii)
      McIDASUtil.flip(dir,57,63);
      flipwords = true;
    }
    try {
      areaDirectory = new AreaDirectory(dir);
    } catch (AreaFileException afe) {
      throw new IOException( afe.getMessage());
    }

    // pull together some values needed by other methods
    numBands = dir[AreaFile.AD_NUMBANDS];
    numLines = dir[AreaFile.AD_NUMLINES];
    numElems = dir[AreaFile.AD_NUMELEMS];
    dataSize = dir[AreaFile.AD_DATAWIDTH];

    int navLoc = dir[AreaFile.AD_NAVOFFSET];
    int calLoc = dir[AreaFile.AD_CALOFFSET];
    int auxLoc = dir[AreaFile.AD_AUXOFFSET];
    datLoc = dir[AreaFile.AD_DATAOFFSET];

    linePrefixLength = dir[AreaFile.AD_DOCLENGTH] + dir[AreaFile.AD_CALLENGTH] + dir[AreaFile.AD_LEVLENGTH];
    if (dir[AreaFile.AD_VALCODE] != 0) linePrefixLength = linePrefixLength + 4;
    if (linePrefixLength != dir[AreaFile.AD_PFXSIZE])
      throw new IOException("Invalid line prefix length in AREA file.");
    lineDataLength = numBands * numElems * dir[AreaFile.AD_DATAWIDTH];
    lineLength = linePrefixLength + lineDataLength;

    int navbytes = 0, calbytes = 0, auxbytes = 0;
    if (datLoc > 0 && datLoc != McIDASUtil.MCMISSING) {
      navbytes = datLoc - navLoc;
      calbytes = datLoc - calLoc;
      auxbytes = datLoc - auxLoc;
    }
    if (auxLoc > 0 && auxLoc != McIDASUtil.MCMISSING) {
      navbytes = auxLoc - navLoc;
      calbytes = auxLoc - calLoc;
    }
    if (calLoc > 0 && calLoc != McIDASUtil.MCMISSING ) {
      navbytes = calLoc - navLoc;
    }

    // Read in nav block
    if (navLoc > 0 && navbytes > 0) {
      navBlock = new int[navbytes/4];

      long newPosition = (long) navLoc;
      int skipByteCount = (int) (newPosition - position);
      af.skipBytes(skipByteCount);

      for (i=0; i<navbytes/4; i++)
        navBlock[i] = af.readInt();

      if (flipwords) flipnav( navBlock);
      position = navLoc + navbytes;
    }


    // Read in cal block
    if (calLoc > 0 && calbytes > 0) {
      cal = new int[calbytes/4];

      long newPosition = (long)calLoc;
      int skipByteCount = (int) (newPosition - position);
      af.skipBytes(skipByteCount);

      for (i=0; i<calbytes/4; i++)
        cal[i] = af.readInt();

      // if (flipwords) flipcal(cal);
      position = calLoc + calbytes;
    }

    // Read in aux block
    if (auxLoc > 0 && auxbytes > 0) {
      aux = new int[auxbytes/4];
      long newPosition = (long) auxLoc;
      int skipByteCount = (int) (newPosition - position);
      af.skipBytes(skipByteCount);
      for (i = 0; i < auxbytes/4; i++)
        aux[i] = af.readInt();
      position = auxLoc + auxbytes;
    }

    return;
  }

  /**
   * Returns the directory block
   *
   * @return an integer array containing the area directory
   *
   */
  public int[] getDir() {
    return dir;
  }


  /**
   * Returns the AreaDirectory object for this AreaFile
   *
   * @return AreaDirectory
   */
  public AreaDirectory getAreaDirectory() {
    return areaDirectory;
  }

  /**
   * Returns calibration block
   *
   * @return an integer array containing the nav block data
   *
   * @exception AreaFileException if there is a problem
   *                              reading the calibration
   *
   */

  public int[] getCal() throws AreaFileException {
    return cal;
  }


  /**
   * Returns AUX block
   *
   * @return an integer array containing the aux block data
   *
   * @exception AreaFileException if there is a problem
   *                              reading the aux block
   *
   */

  public int[] getAux() throws AreaFileException {
    return aux;
  }

  private void readData(DataInputStream af) {
    int i,j,k;

    // create ucar.ma2.Array as 3D int array
    if (dataSize == 1) {
      // assume unsigned byte, must be promoted (2x storage !!)
      data = new ArrayShort.D3(numBands, numLines, numElems);
    } else if (dataSize == 2) {
      data = new ArrayShort.D3(numBands, numLines, numElems);
    } else if (dataSize == 4) {
      data = new ArrayInt.D3(numBands, numLines, numElems);
    }
    Index ima = data.getIndex();

    for (i = 0; i<numLines; i++) {

      try {
        long newPosition = (long) (datLoc + linePrefixLength + i*lineLength) ;
        int skipByteCount = (int) (newPosition - position);
        af.skipBytes(skipByteCount);
        position = newPosition;

      } catch (IOException e) {
        e.printStackTrace();
        break;
      }

      for (j = 0; j<numElems; j++) {
        for (k=0; k<numBands; k++) {

          if (j > lineDataLength) {
            data.setInt( ima.set(k, i, j), 0);
          } else {

            try {
              if (dataSize == 1) {
                short ival = (short) af.readByte();
                if (ival < 0) ival += 256;
                data.setShort( ima.set(k, i, j), ival);
                position = position + 1;
              }
              if (dataSize == 2) {
                data.setShort( ima.set(k, i, j), af.readShort());
                position = position + 2;
              }
              if (dataSize == 4) {
                data.setInt( ima.set(k, i, j), af.readInt());
                position = position + 4;
              }
            }
            catch (IOException e) {
              e.printStackTrace();
            }
          }
        }
      }

    }

    return ;
  }

  /**
   * selectively flip the bytes of words in nav block
   */
  private void flipnav(int[] nav) {

    // first word is always the satellite id in ASCII
    // check on which type:

    if (nav[0] == AREAnav.GVAR) {

      McIDASUtil.flip(nav,2,126);
      McIDASUtil.flip(nav,129,254);
      McIDASUtil.flip(nav,257,382);
      McIDASUtil.flip(nav,385,510);
      McIDASUtil.flip(nav,513,638);
    }

    else if (nav[0] == AREAnav.DMSP) {
      McIDASUtil.flip(nav,1,43);
      McIDASUtil.flip(nav,45,51);
    }

    else if (nav[0] == AREAnav.POES) {
      McIDASUtil.flip(nav,1,119);
    }

    else {
      McIDASUtil.flip(nav,1,nav.length-1);
    }

    return;
  }

  /**
   * Return an AREAnav based on the input nav block.
   */
  public AREAnav getAreaNavigation( ) throws AreaFileException {
    if (areaNavigation != null)
      return areaNavigation;

    try {
        switch (navBlock[0]) {
            case AREAnav.GVAR:
                areaNavigation = new GVARnav(navBlock);
                break;
            case AREAnav.MOLL:
                areaNavigation = new MOLLnav(navBlock);
                break;
            case AREAnav.MSAT:
                areaNavigation = new MSATnav(navBlock);
                break;
            case AREAnav.RADR:
                areaNavigation = new RADRnav(navBlock);
                break;
            case AREAnav.RECT:
                areaNavigation = new RECTnav(navBlock);
                break;
            case AREAnav.GMSX:
                areaNavigation = new GMSXnav(navBlock);
                break;
            case AREAnav.GOES:
                areaNavigation = new GOESnav(navBlock);
                break;
            case AREAnav.PS:
                areaNavigation = new PSnav(navBlock);
                break;
            case AREAnav.MERC:
                areaNavigation = new MERCnav(navBlock);
                break;
            case AREAnav.LAMB:
                areaNavigation = new LAMBnav(navBlock);
                break;
            case AREAnav.TANC:
                areaNavigation = new TANCnav(navBlock);
                break;
            default:
                throw new AreaFileException(
                     "AreaFile2.getAreaNav: Unknown navigation type" + navBlock[0]);
        }
    } catch (IllegalArgumentException excp) {
      throw new AreaFileException( "AreaFile2.getAreaNav: bad nav block "+excp.getMessage());
    }

    areaNavigation.setImageStart(dir[5], dir[6]);
    areaNavigation.setRes(dir[11], dir[12]);
    areaNavigation.setStart(1,1);
    areaNavigation.setMag(1,1);

    return areaNavigation;
  }

  public java.awt.geom.Rectangle2D getBoundingBox() throws AreaFileException {
    AREAnav anav = getAreaNavigation();

    //double[][] linelem = makePerimeter(numElems, numLines);
    double[][] linelem = makeArea(numElems, numLines);
    int size = linelem[0].length;

    // convert to lat/lon
    double[][] latlon =  anav.toLatLon(linelem);

    // get bounding box
    double maxLon = Double.MIN_VALUE;
    double maxLat = Double.MIN_VALUE;
    double minLon = Double.MAX_VALUE;
    double minLat = Double.MAX_VALUE;
    for (int i=0; i<size; i++) {
      double lat = latlon[indexLat][i];
      double lon = latlon[indexLon][i];

      if (showBBcalc && (i % 300 == 0))
        System.out.println("  "+linelem[0][i]+" (elem) "+ linelem[1][i]+ " (line) = "+
          latlon[0][i]+" (lat) "+latlon[1][i]+" (lon) ");

      if (!Double.isNaN(lat)) {
        if (lat > maxLat) maxLat = lat;
        if (lat < minLat) minLat = lat;
      }
      if (!Double.isNaN(lon)) {
        if (lon > maxLon) maxLon = lon;
        if (lon < minLon) minLon = lon;
      }
    }
    return new java.awt.geom.Rectangle2D.Double(minLon, minLat, maxLon-minLon, maxLat-minLat);
  }

  private double [][] makePerimeter(int numElems, int numLines) {
    int size = 2*(numElems+numLines);
    double[][] linelem = new double[2][size];

    int count = 0;

    // top line
    for (int i=0; i<numElems; i++) {
      linelem[indexLine][count] = 0;
      linelem[indexEle][count] = i;
      count++;
    }

    // bottom line
    for (int i=0; i<numElems; i++) {
      linelem[indexLine][count] = numLines-1;
      linelem[indexEle][count] = i;
      count++;
    }

    // right side
    for (int j=0; j<numLines; j++) {
      linelem[indexLine][count] = j;
      linelem[indexEle][count] = 0;
      count++;
    }

    // left side
    for (int j=0; j<numLines; j++) {
      linelem[indexLine][count] = j;
      linelem[indexEle][count] = numElems-1;
      count++;
    }

    return linelem;
  }

  private double [][] makeArea(int numElems, int numLines) {
    int size = numElems * numLines;
    double[][] linelem = new double[2][size];
    int count = 0;

    // top line
    for (int j=0; j<numLines; j++) {
      for (int i=0; i<numElems; i++) {
        linelem[indexLine][count] = j;
        linelem[indexEle][count] = i;
        count++;
      }
    }

    return linelem;
  }

}
