package nxm.sys.lib;

import static nxm.sys.lib.FileUtil.recDef;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import nxm.sys.inc.ListFile;
import nxm.sys.inc.ListInterface;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.Tablizable;

/** A file reader for National Geodetic Survey SP3 files. These are the files
    commonly used to convey ECEF state vector data for Global Positioning System
    (GPS) satellites. <br>
    <br>
    Note that since the files typically contain numerous state vectors, there
    is the concept of a "selected" vehicle (see {@link #getSelected()} /
    {@link #setSelected}) such that this file reader will be able to implement
    the {@link ListInterface} as if it only contained a single state vector
    (this is a required behavior for use with {@link StateVector}).<br>
    <br>
    Limitations:
    <pre>
      - At this time writing of SP3 files is not supported.
    </pre>
    References:
    <pre>
      [1] Spofford, Paul R., Benjamin W. Remondi. "The National Geodetic Survey
          Standard GPS Format SP3." NOAA, National Geodetic Survey.
          <a href="ftp://igscb.jpl.nasa.gov/pub/data/format/sp3_docu.txt"><!--
          -->ftp://igscb.jpl.nasa.gov/pub/data/format/sp3_docu.txt</a>
          (Last accessed 8 Mar 2013).
    </pre>

    @author  J. Czechowski IV
    @since   NeXtMidas 3.3.2
 */
public final class Sp3File extends BaseFile implements ListFile {

  // HEADER VALUES
  private char      version;     // Version (should always be 'a')
  private char      type;        // Should be 'P' (POS) or 'V' (POS+VEL)
  private Time      startTime;   // File start time
  private int       numEpochs;   // Number of epochs (num entries per s/c)
  private String    dataUsed;    // Data used identifier
  private String    coordSys;    // Coordinate system (e.g. "WGS84")
  private String    orbitType;   // Orbit type, should be "FIT", "EXT" (predict) or "BCT" (broadcast)
  private String    agency;      // Agency producing file
  private int       gpsWeek;     // GPS week
  private double    gpsSoW;      // GPS sec-of-week
  private double    epochInt;    // Epock interval (e.g. "xdelta")
  private double    mjd;         // Modified Julian Day
  private String[]  satID;       // s/c ID
  private double[]  satAccuracy; // s/c accuracy (meters)
  private String[]  comments;    // Comments

  // DATA VALUES
  private final Map<String,List<SP3Entry>> entries;

  // OTHER
  private String    selected;    // The user-selected ID.


  /** Default Constructor */
  public Sp3File () {
    super();
    this.entries = new LinkedHashMap<>();
  }

  /** @param ref       Midas Reference
      @param filename  file name
      @since NeXtMidas 3.5.0
      @deprecated since NeXtMidas 3.5.0 use {@link #Sp3File(MidasReference, Object)}*/
  @Deprecated
  public Sp3File (Object ref, Object filename) {
    super(ref,filename);
    this.entries = new LinkedHashMap<>();
  }

  /** Constructor
   *  @param ref       Midas Reference
   *  @param filename  file name
   *  @since NeXtMidas 3.5.0
   */
  public Sp3File (MidasReference ref, Object filename) {
    super(ref,filename);
    this.entries = new LinkedHashMap<>();
  }

  @Override
  public String getMimeType () {
    return "text/plain";
  }

  @Override
  public boolean open () {
    if (super.open()) {
      if (isInput) {
        readFile();
      }
      setInternals();
    }
    return isOpen;
  }

  @Override
  public String toString () {
    return "Sp3File "
         + ((io!=null)? io.getTypeString() : "")
         + " Resource";
  }

  @Override
  public String listHeader () {
    StringBuilder str = new StringBuilder();
    String _comments = StringUtil.join(getComments(), "\n                  ");

    String _type = "";
    switch (getType()) {
      case 'P': _type = " (POS)";     break;
      case 'V': _type = " (POS+VEL)"; break;
    }

    str.append("Sp3File         : ").append(getURL()         ).append('\n');
    str.append("Version         : ").append(getVersion()     ).append('\n');
    str.append("Type            : ").append(getType()        ).append(_type).append('\n');
    str.append("Start Time      : ").append(getStartTime()   ).append('\n');
    str.append("Number Epochs   : ").append(getNumEpochs()   ).append('\n');
    str.append("Data Used       : ").append(getDataUsed()    ).append('\n');
    str.append("Coord. System   : ").append(getCoordSys()    ).append('\n');
    str.append("Orbit Type      : ").append(getOrbitType()   ).append('\n');
    str.append("Agency          : ").append(getAgency()      ).append('\n');
    str.append("GPS Week        : ").append(getGpsWeek()     ).append('\n');
    str.append("GPS Sec-of-Week : ").append(getGpsSoW()      ).append('\n');
    str.append("Epoch Interval  : ").append(getEpochInt()    ).append(" sec\n");
    str.append("Mod Julian Day  : ").append(getMJD()         ).append('\n');
    str.append("Vehicle Count   : ").append(getVehicleCount()).append('\n');
    str.append("Comment         : ").append(_comments        ).append('\n');
    return str.toString();
  }

  @Override
  public String listElements (double start, int elem, String format, int flags) {
    if (start >= getNumEpochs()) return null;
    return "" + getDataTable(start) + "\n";
  }

  @Override
  public int listElementsPerLine (int lineWidth, String format, int flags) {
    return 1;
  }

  /** Gets the file version (should always be 'a').
      @return the file version */
  public char getVersion () { return version; }

  /** Gets the file type. This will be 'P' (POS) or 'V' (POS+VEL).
      @return the file type */
  public char getType () { return type; }

  /** Gets the file start time.
      @return the file start time */
  public Time getStartTime () { return startTime; }

  /** Gets tne number of epochs. This is the number of entries per vehicle.
      @return the number of epochs */
  public int getNumEpochs () { return numEpochs; }

  /** Gets the "data used" identifier.
      @return "data used" identifier */
  public String getDataUsed () { return dataUsed; }

  /** Gets the coordinate system. This will often be "WGS84".
      @return the coordinate system */
  public String getCoordSys () { return coordSys; }

  /** Gets the orbit type. This will be "FIT" (post-fit), "EXT" (extrapolation or
      prediction) or "BCT" (broadcast).
      @return orbit type
   */
  public String getOrbitType () { return orbitType; }

  /** Gets the agency producing file.
      @return name of agency that produced the file */
  public String getAgency () { return agency; }

  /** Gets the GPS week.
      @return GPS week */
  public int getGpsWeek () { return gpsWeek; }

  /** Gets the GPS sec-of-week.
      @return GPS second-of-week */
  public double getGpsSoW () { return gpsSoW; }

  /** Gets the epoch interval. This is essentially "xdelta".
      @return the epoch interval (xdelta) */
  public double getEpochInt () { return epochInt; }

  /** Gets the Modified Julian Day (MJD) week. Note that this is *not* referenced
      to Midas (J1950) time.
      @return the Modified Julian Day
   */
  public double getMJD () { return mjd; }

  /** Gets the number of vehicles.
      @return number of vehicles */
  public int getVehicleCount () { return satID.length; }

  /** Gets the GPS week.
      @return the GPS week */
  public String[] getVehicleIDs () { return satID.clone(); }

  /** Gets the comments.
      @return the comments*/
  public String[] getComments () { return comments.clone(); }

  @Override
  public String getComment () { return StringUtil.join(getComments(), "\n"); }

  /** Gets the selected vehicle ID.
      @return selected vehicle id */
  public String getSelected () {
    return (selected != null)? selected : satID[0];
  }

  /** Sets the selected vehicle ID.
      @param id vehicle id */
  public void setSelected (String id) {
    selected = id;
  }

  @Override
  public Table getDataTable (double offset) {
    int    row = (int)offset;
    String id  = getSelected();

    return entries.get(id).get(row).toTable();
  }

  @Override
  public int getRecordDefCount () {
    switch (type) {
      case 'P': return 3;
      case 'V': return 5;
      default:  return 0;
    }
  }

  @Override
  public Table getRecordDefs () {
    Table recDefs = new Table();
    for (int i = 0; i < getRecordDefCount(); i++) {
      Table def = getRecordDef(i);
      recDefs.put(def.getS("NAME"), def);
    }
    return recDefs;
  }

  @Override
  public Table getRecordDef (int i) {
    switch (type + i) {
      case 'V'+0: case 'P'+0: return recDef("POS",  "VD", null, "CARTESIAN", "DISTANCE_M");
      case 'V'+1:             return recDef("VEL",  "VD", null, "CARTESIAN", "SPEED_M_S");
      case 'V'+2: case 'P'+1: return recDef("TIME", "SD", null, "SCALAR",    "TIME_S");
      case 'V'+3: case 'P'+2: return recDef("CLKR", "SD", null, "SCALAR",    "TIME_S");
      case 'V'+4:             return recDef("CROC", "SD", null, "SCALAR",    "DIMENSIONLESS");
      default:                return null;
    }
  }

  @Override public double  getSize   () { return getNumEpochs(); }
  @Override public double  getOffset () { return getSize(); }  // always at EOF
  @Override public double  seek      () { return getSize(); }  // always at EOF
  @Override public double  avail     () { return 0; }          // always at EOF
  @Override public boolean isReady (double b) { return false; } // always at EOF

  @Override public double  getNumberOfRows () { return getNumEpochs(); }
  @Override public boolean getProtected    () { return true; }

  @Override public int  setData    (double o,           Table  t) { throw readonly(); }
  @Override public void setData    (double o, String n, Object v) { throw readonly(); }
  @Override public void insertData (double o,           Object v) { throw readonly(); }
  @Override public void removeData (double o                    ) { throw readonly(); }
  @Override public void removeData (double o, long c            ) { throw readonly(); }
  @Override public void setRecordDefs (Table d                  ) { throw readonly(); }
  @Override public void update        (                         ) { throw readonly(); }

  /** Got an error in attempted write. */
  private MidasException readonly () {
    return new MidasException("Sp3File "+getName()+" is read only.");
  }

  /** Got an error in readFile(..). */
  private MidasException error (int lineNum, String msg) {
    return new MidasException("Error on line "+lineNum+" of "+getName()+": "+msg);
  }

  /** Reads the file. */
  private void readFile () {
    int lineNum = 0;

    if (isOpen) {
      try {
        BufferedReader in = new BufferedReader(new InputStreamReader(io.getInputStream()));
        String         line;

        // ==== READ HEADER ====================================================
        while (lineNum < 22) {
          lineNum++;
          line = in.readLine();

          if (line == null) {
            throw error(lineNum, "Missing header information.");
          }
          else if (lineNum == 1) {
            if (!line.startsWith("#a")) throw error(lineNum, "Invalid file format or version.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");

            String year = line.substring( 3, 7);
            String mon  = line.substring( 8,10).replace(' ','0');
            String day  = line.substring(11,13).replace(' ','0');
            String hour = line.substring(14,16).replace(' ','0');
            String min  = line.substring(17,19).replace(' ','0');
            String sec  = line.substring(20,31).replace(' ','0');
            String time = year+":"+mon+":"+day+"::"+hour+":"+min+":"+sec;

            version   = line.charAt(1);
            type      = line.charAt(2);
            startTime = new Time(time);
            numEpochs = Integer.parseInt(line.substring(32,39).trim());
            dataUsed  = line.substring(40,45).trim();
            coordSys  = line.substring(46,51).trim();
            orbitType = line.substring(52,55).trim();
            agency    = line.substring(56,60).trim();
          }
          else if (lineNum == 2) {
            if (!line.startsWith("##")) throw error(lineNum, "Invalid syntax.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");

            gpsWeek  = Integer.parseInt(  line.substring( 3, 7).trim());
            gpsSoW   = Double.parseDouble(line.substring( 8,23).trim());
            epochInt = Double.parseDouble(line.substring(24,38).trim());
            mjd      = Integer.parseInt(  line.substring(39,44).trim())
                     + Double.parseDouble(line.substring(45,60).trim());
          }
          else if (lineNum <= 7) {
            if (!line.startsWith("+ ")) throw error(lineNum, "Invalid syntax.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");

            if (lineNum == 3) {
              int numSC = Integer.parseInt(line.substring(4,6).trim());
              satID       = new String[numSC];
              satAccuracy = new double[numSC];
            }

            int off = (lineNum-3)*17;
            for (int i = 9; (i < 60) && (off < satID.length); i+=3) {
              satID[off++] = line.substring(i,i+3).trim();
            }
          }
          else if (lineNum <= 12) {
            if (!line.startsWith("++")) throw error(lineNum, "Invalid syntax.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");

            int off = (lineNum-3)*17;
            for (int i = 9; (i < 60) && (off < satID.length); i+=3) {
              int accuracy = Integer.parseInt(line.substring(i,i+3).trim());

              satAccuracy[off++] = Math.pow(2, accuracy) * 0.001;
            }
          }
          else if (lineNum <= 14) {
            if (!line.startsWith("%c")) throw error(lineNum, "Invalid syntax.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");
          }
          else if (lineNum <= 16) {
            if (!line.startsWith("%f")) throw error(lineNum, "Invalid syntax.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");
          }
          else if (lineNum <= 18) {
            if (!line.startsWith("%i")) throw error(lineNum, "Invalid syntax.");
            if ((line.length() != 60) ) throw error(lineNum, "Invalid line length.");
          }
          else if (lineNum <= 22) {
            if (!line.startsWith("/*")) throw error(lineNum, "Invalid syntax.");
            if (lineNum == 19) comments = new String[4];

            comments[lineNum-19] = line.substring(3).trim();
          }
        }

        // ==== INIT DATA READ =================================================
        for (String id : satID) {
          entries.put(id, new ArrayList<SP3Entry>(numEpochs));
        }

        // ==== READ DATA ======================================================
        for (int i = 0; i < numEpochs; i++) {
          lineNum++;
          line = in.readLine();
          if ((line == null) || !line.startsWith("* ")) {
            throw error(lineNum, "Missing epoch.");
          }

          String year  = line.substring( 3, 7);
          String mon   = line.substring( 8,10).replace(' ','0');
          String day   = line.substring(11,13).replace(' ','0');
          String hour  = line.substring(14,16).replace(' ','0');
          String min   = line.substring(17,19).replace(' ','0');
          String sec   = line.substring(20,31).replace(' ','0');
          String time  = year+":"+mon+":"+day+"::"+hour+":"+min+":"+sec;
          Time   epoch = new Time(time);

          for (int j = 0; j < satID.length; j++) {
            lineNum++;
            line = in.readLine();
            if ((line == null) || !line.startsWith("P ")) {
              throw error(lineNum, "Missing position.");
            }

            String   id    = line.substring(2,4).trim();
            SP3Entry entry = new SP3Entry(id, epoch);

            entry.initPos(Double.parseDouble(line.substring( 4,18).trim()) * 1e3,
                          Double.parseDouble(line.substring(18,32).trim()) * 1e3,
                          Double.parseDouble(line.substring(32,46).trim()) * 1e3,
                          Double.parseDouble(line.substring(46,60).trim()));

            if (type == 'V') {
              lineNum++;
              line = in.readLine();
              if ((line == null) || !line.startsWith("V ")) {
                throw error(lineNum, "Missing velocity.");
              }
              String idV = line.substring(2,4).trim();
              if (!id.equals(idV)) {
                throw error(lineNum, "Missing velocity for '"+id+"'.");

              }
              entry.initVel(Double.parseDouble(line.substring( 4,18).trim()) * 0.1,
                            Double.parseDouble(line.substring(18,32).trim()) * 0.1,
                            Double.parseDouble(line.substring(32,46).trim()) * 0.1,
                            Double.parseDouble(line.substring(46,60).trim()));
            }
            entries.get(id).add(entry);
          }
        }

      }
      catch (MidasException e) {
        throw e;
      }
      catch (Exception e) {
        throw new MidasException("Error on line "+lineNum+" of "+getName()+": "+e.getMessage(), e);
      }
    }
  }

  /** Describes a SP3 entry. */
  private final class SP3Entry implements Tablizable {
    private final String   id;    // s/c ID
    private final Time     epoch; // Epoch
    private final double[] pos;   // ECEF Position
    private final double[] vel;   // ECEF Velocity (null if type='P')
    private       Double   clock; // Clock rate (sec)
    private       Double   rate;  // Clock rate-of-change (sec/sec)

    SP3Entry (String id, Time epoch) {
      this.id    = id;
      this.epoch = epoch;
      this.pos   = new double[3];
      this.vel   = (type == 'V')? new double[3] : null;
    }

    void initPos (double x, double y, double z, double clock_us) {
      this.pos[0] = x;
      this.pos[1] = y;
      this.pos[2] = z;
      this.clock  = (((int)clock_us) == 999999)? null : clock_us*1e-6;
    }

    void initVel (double x, double y, double z, double rate_us) {
      this.vel[0] = x;
      this.vel[1] = y;
      this.vel[2] = z;
      this.rate   = (((int)rate_us) == 999999)? null : rate_us*1e-6;
    }

    @Override
    public String toString () {
      return "SP3 Entry: "+toTable();
    }

    @Override
    public Table toTable () {
      Table tbl = new Table();

      tbl.put("ID",   id);
      tbl.put("TIME", epoch);
      tbl.put("POS",  new Data(pos));

      if (vel   != null) tbl.put("VEL",  new Data(vel));
      if (clock != null) tbl.put("CLKR", clock);
      if (rate  != null) tbl.put("CROC", rate);

      return tbl;
    }

  }
}
