/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.grib.grib2;

import com.google.common.base.MoreObjects;
import javax.annotation.Nullable;
import ucar.nc2.grib.GribData;
import ucar.nc2.grib.QuasiRegular;
import ucar.nc2.time.CalendarDate;
import ucar.unidata.io.RandomAccessFile;
import ucar.unidata.util.StringUtil2;
import java.io.IOException;
import java.util.Formatter;

/**
 * Read one Record from a GRIB-2 files
 *
 * @author caron
 * @since 3/28/11
 */
public class Grib2Record {

  //////////////////////////////////////////////////////////////
  private final Grib2SectionIndicator is;
  private final Grib2SectionIdentification id;
  private Grib2SectionLocalUse lus; // local use section
  private Grib2SectionGridDefinition gdss;
  private Grib2SectionProductDefinition pdss;
  private Grib2SectionDataRepresentation drss;
  private Grib2SectionBitMap bms;
  private Grib2SectionData dataSection;

  private Grib2Pds pds2;

  private final byte[] header; // anything in between the records - eg idd header
  private int file; // for multiple files in same dataset
  private boolean bmsReplaced;
  private int scanMode;

  public int repeat; // debug = see Grib2Report.doDrsSummary

  /**
   * Construction for Grib2Record.
   *
   * @param header Grib header
   * @param is Grib2IndicatorSection
   * @param id Grib2IdentificationSection
   * @param lus raw bytes of local use section
   * @param gdss Grib2GridDefinitionSection
   * @param pdss Grib2ProductDefinitionSection
   * @param drs Grib2SectionDataRepresentation
   * @param bms Grib2SectionBitMap
   * @param dataSection Grib2SectionData
   * @param bmsReplaced Grib2SectionData
   * @param scanMode from GDS of this record
   */
  public Grib2Record(byte[] header, Grib2SectionIndicator is, Grib2SectionIdentification id, Grib2SectionLocalUse lus,
      Grib2SectionGridDefinition gdss, Grib2SectionProductDefinition pdss, Grib2SectionDataRepresentation drs,
      Grib2SectionBitMap bms, Grib2SectionData dataSection, boolean bmsReplaced, int scanMode) {

    this.header = header;
    this.is = is;
    this.id = id;
    this.lus = lus;
    this.gdss = gdss;
    this.pdss = pdss;
    this.drss = drs;
    this.bms = bms;
    this.dataSection = dataSection;
    this.bmsReplaced = bmsReplaced;

    // stored in index file after 4.5 2/6/2014, otherwise equals Grib2Index.ScanModeMissing, so get it from the GDS,
    // which may have wrong one
    this.scanMode = scanMode;
    if (scanMode == Grib2Index.ScanModeMissing && gdss != null) {
      this.scanMode = gdss.getGDS().getScanMode();
    }
  }

  // copy constructor
  Grib2Record(Grib2Record from) {
    this.header = from.header;
    this.is = from.is;
    this.id = from.id;
    this.lus = from.lus;
    this.gdss = from.gdss;
    this.pdss = from.pdss;
    this.drss = from.drss;
    this.bms = from.bms;
    this.dataSection = from.dataSection;
    this.repeat = from.repeat;
    this.bmsReplaced = from.bmsReplaced;
    this.scanMode = from.scanMode;
  }

  public byte[] getHeader() {
    return header;
  }

  public Grib2SectionIndicator getIs() {
    return is;
  }

  public Grib2SectionIdentification getId() {
    return id;
  }

  public boolean hasLocalUseSection() {
    return lus != null && lus.getRawBytes() != null;
  }

  public Grib2SectionLocalUse getLocalUseSection() {
    return lus;
  }

  public Grib2SectionGridDefinition getGDSsection() {
    return gdss;
  }

  public Grib2SectionProductDefinition getPDSsection() {
    return pdss;
  }

  public Grib2SectionDataRepresentation getDataRepresentationSection() {
    return drss;
  }

  public Grib2SectionBitMap getBitmapSection() {
    return bms;
  }

  public Grib2SectionData getDataSection() {
    return dataSection;
  }

  public int getDiscipline() {
    return is.getDiscipline();
  }

  public CalendarDate getReferenceDate() {
    return id.getReferenceDate();
  }

  public Grib2Pds getPDS() {
    if (pds2 == null)
      pds2 = pdss.getPDS();
    return pds2;
  }

  public synchronized Grib2Gds getGDS() {
    return gdss.getGDS();
  }

  public int getScanMode() {
    return scanMode;
  }

  public void show(Formatter f) {
    f.format("discipline=%d ", is.getDiscipline());
    Grib2Pds pds = getPDS();
    pds.show(f);
  }

  //////////////////////////////////////////
  // setters used by repeating records


  public void setLus(Grib2SectionLocalUse lus) {
    this.lus = lus;
  }

  public void setGdss(Grib2SectionGridDefinition gdss) {
    this.gdss = gdss;
  }

  public void setPdss(Grib2SectionProductDefinition pdss) {
    this.pdss = pdss;
  }

  public void setDrs(Grib2SectionDataRepresentation drs) {
    this.drss = drs;
  }

  public void setBms(Grib2SectionBitMap bms, boolean replaced) {
    this.bms = bms;
    this.bmsReplaced = replaced;
  }

  public void setDataSection(Grib2SectionData dataSection) {
    this.dataSection = dataSection;
  }

  public int getFile() {
    return file;
  }

  public void setFile(int file) {
    this.file = file;
  }

  public boolean isBmsReplaced() {
    return bmsReplaced;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this).add("is", is).add("id", id).add("lus", lus).add("gdss", gdss)
        .add("pdss", pdss).add("drss", drss).add("bms", bms).add("dataSection", dataSection).add("pds2", pds2)
        .add("header", header).add("file", file).add("bmsReplaced", bmsReplaced).add("scanMode", scanMode)
        .add("repeat", repeat).toString();
  }

  // isolate dependencies here - in case we have a "minimal I/O" mode where not all fields are available
  public float[] readData(RandomAccessFile raf) throws IOException {
    Grib2Gds gds = getGDS();

    Grib2DataReader reader = new Grib2DataReader(drss.getDataTemplate(), gdss.getNumberPoints(), drss.getDataPoints(),
        getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drss.getDrs(raf);

    float[] data = reader.getData(raf, bms, gdrs);

    if (gds.isThin())
      data = QuasiRegular.convertQuasiGrid(data, gds.getNptsInLine(), gds.getNxRaw(), gds.getNyRaw(),
          GribData.getInterpolationMethod());

    lastRecordRead = this;
    return data;
  }

  // debugging - do not use
  @Nullable
  public int[] readRawData(RandomAccessFile raf) throws IOException {
    Grib2Gds gds = getGDS();

    Grib2DataReader reader = new Grib2DataReader(drss.getDataTemplate(), gdss.getNumberPoints(), drss.getDataPoints(),
        getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drss.getDrs(raf);

    return reader.getRawData(raf, bms, gdrs);
  }

  // debugging - do not use
  @Nullable
  public Grib2Drs.Type40 readDataTest(RandomAccessFile raf) throws IOException {
    Grib2Gds gds = getGDS();

    Grib2DataReader reader = new Grib2DataReader(drss.getDataTemplate(), gdss.getNumberPoints(), drss.getDataPoints(),
        getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drss.getDrs(raf);
    if (gdrs instanceof Grib2Drs.Type40) {
      reader.getData(raf, bms, gdrs);
      return (Grib2Drs.Type40) gdrs;
    }
    return null;
  }

  /**
   * Read data array
   *
   * @param raf from this RandomAccessFile
   * @param drsPos Grib2SectionDataRepresentation starts here
   * @return data as float[] array
   * @throws IOException on read error
   */
  public float[] readData(RandomAccessFile raf, long drsPos) throws IOException {
    raf.seek(drsPos);
    Grib2SectionDataRepresentation drs = new Grib2SectionDataRepresentation(raf);
    Grib2SectionBitMap bms = new Grib2SectionBitMap(raf);
    Grib2SectionData dataSection = new Grib2SectionData(raf);

    Grib2Gds gds = getGDS();
    Grib2DataReader reader = new Grib2DataReader(drs.getDataTemplate(), gdss.getNumberPoints(), drs.getDataPoints(),
        getScanMode(), gds.getNxRaw(), dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drs.getDrs(raf);

    float[] data = reader.getData(raf, bms, gdrs);

    if (gds.isThin())
      data = QuasiRegular.convertQuasiGrid(data, gds.getNptsInLine(), gds.getNxRaw(), gds.getNyRaw(),
          GribData.getInterpolationMethod());

    lastRecordRead = this;
    return data;
  }

  /**
   * Read data array: use when you want to be independent of the GribRecord
   *
   * @param raf from this RandomAccessFile
   * @param drsPos Grib2SectionDataRepresentation starts here
   * @param bmsPos if non-zero, use the bms that starts here
   * @param gdsNumberPoints gdss.getNumberPoints()
   * @param scanMode gds.scanMode
   * @param nx gds.nx
   * @return data as float[] array
   * @throws IOException on read error
   */
  public static float[] readData(RandomAccessFile raf, long drsPos, long bmsPos, int gdsNumberPoints, int scanMode,
      int nx, int ny, int[] nptsInLine) throws IOException {
    raf.seek(drsPos);
    Grib2SectionDataRepresentation drs = new Grib2SectionDataRepresentation(raf);
    Grib2SectionBitMap bms = new Grib2SectionBitMap(raf);
    Grib2SectionData dataSection = new Grib2SectionData(raf);

    if (bmsPos > 0)
      bms = Grib2SectionBitMap.factory(raf, bmsPos);

    Grib2DataReader reader = new Grib2DataReader(drs.getDataTemplate(), gdsNumberPoints, drs.getDataPoints(), scanMode,
        nx, dataSection.getStartingPosition(), dataSection.getMsgLength());

    Grib2Drs gdrs = drs.getDrs(raf);

    float[] data = reader.getData(raf, bms, gdrs);

    if (nptsInLine != null)
      data = QuasiRegular.convertQuasiGrid(data, nptsInLine, nx, ny, GribData.getInterpolationMethod());

    if (getlastRecordRead)
      lastRecordRead = Grib2RecordScanner.findRecordByDrspos(raf, drsPos);
    return data;
  }

  public void check(RandomAccessFile raf, Formatter f) throws IOException {
    long messLen = is.getMessageLength();
    long startPos = is.getStartPos();
    long endPos = is.getEndPos();

    if (endPos > raf.length()) {
      f.format("End of GRIB message (start=%d len=%d) end=%d > file.length=%d for %s%n", startPos, messLen, endPos,
          raf.length(), raf.getLocation());
      return;
    }

    raf.seek(endPos - 4);
    for (int i = 0; i < 4; i++) {
      if (raf.read() != 55) {
        String clean = StringUtil2.cleanup(header);
        if (clean.length() > 40)
          clean = clean.substring(0, 40) + "...";
        f.format("Missing End of GRIB message (start=%d len=%d) end=%d header= %s for %s (len=%d)%n", startPos, messLen,
            endPos, clean, raf.getLocation(), raf.length());
        break;
      }
    }

    long dataLen = dataSection.getMsgLength();
    long dataStart = dataSection.getStartingPosition();
    long dataEnd = dataStart + dataLen;

    if (dataEnd > raf.length()) {
      f.format("GRIB data section (start=%d len=%d) end=%d > file.length=%d for %s%n", dataStart, dataLen, dataEnd,
          raf.length(), raf.getLocation());
      return;
    }

    if (dataEnd > endPos) {
      f.format("GRIB data section (start=%d len=%d) end=%d > message end=%d for %s%n", dataStart, dataLen, dataEnd,
          endPos, raf.getLocation());
    }

  }

  public GribData.Info getBinaryDataInfo(RandomAccessFile raf) throws IOException {
    GribData.Info info = this.drss.getDrs(raf).getBinaryDataInfo(raf);
    info.bitmapLength = (bms == null) ? 0 : bms.getLength(raf);
    info.msgLength = is.getMessageLength();
    info.dataLength = dataSection.getMsgLength();
    info.ndataPoints = drss.getDataPoints();
    Grib2Gds gds = getGDS();
    info.nPoints = gds.getNx() * gds.getNy();
    return info;
  }

  // debugging do not use
  public static boolean getlastRecordRead;
  public static Grib2Record lastRecordRead;

}
