package nxm.sys.lib;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;

/** Holds information about a vehicle
    <br>
    <br>
    <h2>Important Notes:</h2>
    <pre>
      Positions
      ---------
        The positions returned by the various points in this class return the
        actual positions being used by this class (not copies). Any changed
        made to them can affect the behavior of this class.

      Time
      ----
        This class allows time-dependent positions (e.g. {@link StateVector}s) to
        be used. The proper way of adjusting the time is by using the
        {@link #setTime(double)}. Adjusting the times manually can cause the
        times to become "out of synch."

      Iteration
      ---------
        Suggested way of iterating over the pointing positions:

            String[] keys = getPointNames();
            for (int i = 0; i &lt; keys.length; i++) {
              Point point = getPointObject(keys[i]);
            }

      Attributes
      ----------
        The getAttributes() method provides a table to hold attributes useful
        when rendering an instance of this class on a plot.
    </pre>

    @author  J. Czechowski IV
    @since   NeXtMidas 2.5.0
 */
public class Vehicle {
  private final EventListenerList listeners   = new EventListenerList(); // The set of event listeners
  private final TableListener     tblListener = new TableListener();     // Listener for inner tables.

  protected final Object   ref;         // Midas context or reference object to use.
  protected final String   className;   // Describes this class (set in constructor)
  protected final Location location;    // Vehicle label (set in constructor)
  protected final Site     site;        // Site label (set in constructor)
  protected final Table    pointPosTbl; // Table of all of the points (set in constructor)
  protected final Table    attributes;  // Rendering attributes (set in constructor)


  /** Creates a new instance of Vehicle with given Midas reference.
      @param ref  Reference object to use (usually an instance of Midas).
   */
  public Vehicle(Object ref) {
    this("Vehicle", ref);
  }

  /** Creates a new instance of Vehicle with given Midas reference. */
  protected Vehicle(String className, Object ref) {
    this.className   = className;
    this.ref         = (ref != null)? ref : Shell.getSharedMidasContext();
    this.location    = createLocation();
    this.site        = createSite();
    this.pointPosTbl = new Table();
    this.attributes  = new Table();

    attributes.addListener(tblListener);
  }

  /** Creates a default Location. */
  protected Location createLocation() { return new LocationImpl(this); }

  /** Creates a default site. */
  protected Site createSite() { return new SiteImpl(this); }

  /** Creates a new vehicle based on a table of values.
      @param ref  Reference object to use (usually an instance of Midas).
      @param tbl  The table of values, see {@link #toTable()} for a list of table keys.
   */
  public Vehicle(Object ref, Table tbl) {
    this(ref);
    setValues(tbl);
  }

  /** Creates a new point. Subclasses override as needed.
      @name name of new point
      @return new positioning point */
  protected PointingPos createPointingPos(String name) {
    return new PointingPosImpl(this,name);
  }

  /** Sets the values in a vehicle from a table. See {@link #toTable()} for a list of table keys.
      @param tbl table containing the values in a vehicle */
  public void setValues(Table tbl) {
    if (tbl != null) {
      Table pts  = tbl.getTable("POINTS");
      Table attr = tbl.getTable("ATTRIBUTES");

      if (tbl.containsKey("LOCATION")) {  getLocation().setValues(tbl.getTable("LOCATION")); }
      if (tbl.containsKey("SITE"    )) {  getSite().setValues(tbl.getTable("SITE"));         }
      if (tbl.containsKey("TIME"    )) {  setTime(tbl.getD("TIME"));                         }

      if (pts != null) {
        String[] keys  = pts.getKeys();
        for (int i = 0; i < keys.length; i++) {
          usePointingPos(keys[i]).setValues(pts.getTable(keys[i]));
        }
      }
      if (attr != null) {
        attributes.merge(attr);
      }
    }
  }

  /** Converts this vehicle to a table. The table has the following keys:
      <pre>
        LOCATION    - The value of getLocation().toTable()
        SITE        - The value of getSite().toTable()
        POINTINGPOS - The table for each of the points:
                       [name1]=getPointingPos([name1]).toTable()
                          :              :
                       [nameN]=getPointingPos([nameN]).toTable()
        TIME        - The value of {@link #getTime()}
        ATTRIBUTES  - The rendering attributes.
      </pre>
      @see Vehicle.PointingPos#toTable()
      @return vehicle in table form
   */
  public Table toTable() {
    Table tbl      = new Table();
    Table pointPos = new Table();

    // Use variable rather than method call so any nulls are preserved.
    tbl.put("LOCATION",    location.toTable());
    tbl.put("SITE",        site.toTable());
    tbl.put("POINTINGPOS", pointPos);
    tbl.put("ATTRIBUTES",  getAttributes());
    tbl.put("TIME",        getTime());

    String[] keys = getPointingPosNames();
    for (int i = 0; i < keys.length; i++) {
      pointPos.put(keys[i], getPointingPos(keys[i]).toTable());
    }
    return tbl;
  }

  /** Gets a short description of the Vehicle.
     @return short description of the vehicle */
  @Override
  public String toString() {
    return className+": "+toTable();
  }

  /** Gets the vehicle's time. This is used for animated vehicles that can move over time.
      @return the vehicles time */
  public double getTime() {
    return getLocation().getTime();
  }

  /** Sets the vehicle's time. This is used for animated vehicles that can move over time.
      @param time The new time.
   */
  public void setTime(double time) {
    if (time != getTime()) {
      // Time is controlled by the time in position, so change that and then do an updateAll()
      getLocation().setTime(time);
      updateAll();
      refresh();
    }
  }

  /** Get the rendering attributes.
      @return table containing the rendering attributes */
  public Table getAttributes() {
    return attributes;
  }

  /** Get the named rendering attribute.
      @param key The name of the attribute.
      @return The value or null if not found.
   */
  public Object getAttribute (String key) {
    return attributes.get(key);
  }

  /** Sets the named rendering attribute.
      @param key The name of the attribute.
      @param val The value of the attribute.
   */
  public void setAttribute (String key, Object val) {
    if (val == null) attributes.remove(key);
    else             attributes.put(key, val);
  }

  /** Gets the location of the vehicle.
      @return location of the vehicle*/
  public Location getLocation() {
    return location;
  }

  /** Gets the location of the vehicle's site
      @return location of the vehicles site */
  public Site getSite() {
    return site;
  }

  /** Gets the names of all of the pointing positions
      @return names of all the positioning points*/
  public String[] getPointingPosNames() {
    return pointPosTbl.getKeys();
  }

  /** Get a named pointing position, returns null if the named point does not exist.
      @see #usePointingPos(String)
      @param name name of positioning point to get
      @return positioning point
   */
  public PointingPos getPointingPos(String name) {
    return (PointingPos)pointPosTbl.get(name);
  }

  /** Use a pointing position, if it does not exist, create it.
      @see #getPointingPos(String)
      @param name name of positioning point
      @return positioning point
   */
  public PointingPos usePointingPos(String name) {
    PointingPos point = getPointingPos(name);
    if (point == null) {
      point = createPointingPos(name);
      pointPosTbl.put(name,point);
      refresh();
    }
    return point;
  }

  /** Removes a named pointing position.
      @param name name of positoningn point to remove*/
  public void removePointingPos(String name) {
    PointingPos old = getPointingPos(name);
    if (old != null) {
      pointPosTbl.remove(name);
      refresh();
    }
  }

  /** Removes all pointing positions. */
  public void removeAllPointingPos() {
    if (!pointPosTbl.isEmpty()) {
      pointPosTbl.clear();
      refresh();
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Listener methods
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Adds a change listener.
      @param listener listener to add */
  public void addChangeListener(ChangeListener listener) {
    listeners.add(ChangeListener.class, listener);
  }

  /** Removes a change listener.
      @param listener listener to remove*/
  public void removeChangeListener(ChangeListener listener) {
    listeners.remove(ChangeListener.class, listener);
  }

  /** Gets a list of the current change listeners.
      @return list of the current change listeners */
  public ChangeListener[] getChangeListeners() {
    return listeners.getListeners(ChangeListener.class);
  }

  /** Fires a change event. */
  protected void fireChangeEvent() {
    // This method is almost an exact copy of the one shown in the docs for EventListenerList
    ChangeEvent event = null;

    Object[] list = listeners.getListenerList();
    for (int i = list.length-2; i>=0; i-=2) {
      if (list[i] == ChangeListener.class) {
        if (event == null) { event = new ChangeEvent(this); }
        ((ChangeListener)list[i+1]).stateChanged(event);
      }
    }
  }

  /** Refreshes the vehicle following a change. */
  protected void refresh() {
    fireChangeEvent();
  }

  /** Implement table listener to call refresh(). */
  private class TableListener implements Table.Listener {
    @Override
    public boolean putAction (String key, Object value) { refresh();   return false; }
    @Override
    public boolean getAction (String key, Object value) { /* ignore */ return false; }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Utility methods
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Updates values in the given position so they are correct. */
  protected void update(Location point) {
    point.setTime(getTime());
  }

  /** Updates values in the given position so they are correct. */
  protected void updateAll() {
    update(getLocation());
    update(getSite());

    String[] keys = getPointingPosNames();
    for (int i = 0; i < keys.length; i++) {
      update(getPointingPos(keys[i]));
    }
  }

  /** Quick conversion to a position. */
  private static Position toPosition(Object val) {
    if (val instanceof Position) {
      return (Position)val;
    } else {
      return Position.fromGeo(Convert.o2t(val));
    }
  }

  /** Add a non-null value to a table. */
  protected static void put(Table tbl, String key, Object obj) {
    if (obj != null) { tbl.put(key,obj); }
  }

  /** Converts a position to a string. */
  private static String toString(Position pos) {
    return (pos == null)? null: "{ALT="+pos.getAlt()+",LAT="+pos.getLat()+",LON="+pos.getLon()+"}";
  }

  /** Converts Location to table. */
  static Table toTable(Location location) { // Used by LocationImpl
    Table    tbl   = new Table();
    String   label = location.getLabel();
    Position pos   = location.getPos();

    if (label != null) tbl.put("LABEL", label);
    if (pos   != null) tbl.put("POS",   toString(pos));
    //if (pos instanceof StateVector) tbl.put("SV",    ((StateVector)pos).df.getName());

    tbl.put("ATTRIBUTES", location.getAttributes());

    if (location instanceof Site) {
      Site site = (Site)location;
      tbl.put("FLINK", site.getLinkFreq());
    }
    if (location instanceof PointingPos) {
      PointingPos ppos = (PointingPos)location;
      tbl.put("CANGLE", ppos.getConeAngle());
      tbl.put("RADIUS", ppos.getRadius());
    }

    return tbl;
  }

  /** Sets values for a location. */
  static void setValues(Location location, Table tbl) { // Used by LocationImpl
    if (tbl != null) {
      Table attr = tbl.getTable("ATTRIBUTES");

      if (tbl.containsKey("POS"   )) { location.setPos(Vehicle.toPosition(tbl.getO("POS"))); }
      if (tbl.containsKey("SV"    )) { location.setStateVector(tbl.getS("SV")); }
      if (tbl.containsKey("LABEL" )) { location.setLabel(tbl.getS("LABEL")); }
      if (tbl.containsKey("RADIUS")) { location.setRadius(tbl.getD("RADIUS")); }

      if (attr != null) {
        location.getAttributes().merge(attr);
      }

      if (location instanceof Site) {
        Site site = (Site)location;
        if (tbl.containsKey("FLINK")) { site.setLinkFreq(tbl.getD("FLINK")); }
      }
      if (location instanceof PointingPos) {
        PointingPos ppos = (PointingPos)location;
        if (tbl.containsKey("CANGLE")) { ppos.setConeAngle(tbl.getD("CANGLE")); }
      }
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // The Location interfaces and associated methods.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** A location. */
  public static interface Location {
    /** Converts this position to a table. The table has the following keys:
        <pre>
          POS        - The value of {@link #getPos()} as {ALT=,LAT=,LON=}
          SV         - The file name of the StateVector file (if present).
          ATTRIBUTES - The rendering attributes.
          LABEL      - A label for the location.
          RADIUS     - The value of {@link #getRadius()}
        </pre>
        @return this position as a table
     */
    Table toTable();

    /** Sets a set of values for this point. See {@link #toTable()} for a list of valid names.
        @param tbl table containing set of values for this point */
    void setValues(Table tbl);

    /** Gets the site position of the vehicle.
        @return site position of the vehicle*/
    Position getPos();

    /** Sets the site position of the vehicle.
        @param pos site position of the vehicle*/
    void setPos(Position pos);

    /** Sets the position of the vehicle.
        @param tbl Table with <code>{ALT=alt,LAT=lat,LON=lon}</code>.
     */
    void setPos(Table tbl);

    /** Sets the site position of the vehicle.
        @param alt  The altitude (in meters).
        @param lat  The latitude (in deg).
        @param lon  The longitude (in deg).
     */
    void setPos(double alt, double lat, double lon);

    /** Sets the site position of the vehicle using a state vector. This is the same as
        <code>setPos(sv)</code>
        @param sv state vector describing site position.
     */
    void setStateVector(StateVector sv);

    /** Sets the position of the vehicle.Sets the site position of the vehicle using a state vector. This is the same as
        <code>setPos(new StateVector(ref, fname))</code>.
        @param fname File name for the state vector.
     */
    void setStateVector(String fname);

    /** Gets the point's time value
        @return points time value*/
    double getTime();

    /** Sets the point's time time value. This updates the time in the point's position.
        @param time point's time value*/
    void setTime(double time);

    /** Get the rendering attributes.
        @return the attributes*/
    Table getAttributes();

    /** Get the named rendering attribute.
        @param key The name of the attribute.
        @return The value or null if not found.
     */
    public Object getAttribute (String key);

    /** Sets the named rendering attribute.
        @param key The name of the attribute.
        @param val The value of the attribute.
     */
    public void setAttribute (String key, Object val);

    /** Gets a label for the point.
        @return label for the point*/
    String getLabel();

    /** Gets a label for the point.
        @param label label for the point*/
    void setLabel(String label);

    /** Get the effective radius in meters, returns NaN if not supported.
        @return radius in meters*/
    double getRadius();

    /** Set the effective radius in meters
        @param value radius in meters*/
    void setRadius(double value);
  }

  /** The site location. */
  public static interface Site extends Location {
    /** Converts this position to a table. In addition to the keys defined in
        {@link Vehicle.Location#toTable()}, the table has the following keys:
        <pre>
          FLINK - The link frequency in Hz.
        </pre>
     */
    @Override
    Table toTable();

    /** Gets the link frequency in Hz.
        @return frequency in Hz */
    public double getLinkFreq ();

    /** Sets the link frequency in Hz.
        @param freq frequency in Hz */
    public void setLinkFreq (double freq);
  }

  /** A point that can be pointed to by a vehicle. */
  public static interface PointingPos extends Location {
    /** Converts this position to a table. In addition to the keys defined in
        {@link Vehicle.Location#toTable()}, the table has the following keys:
        <pre>
          CANGLE - The value of {@link #getConeAngle()}
        </pre>
     */
    @Override
    Table toTable();

    /** Sets a set of values for this point. See {@link #toTable()} for a list of valid names
        @param tbl Table containing set of values for this point. */
    @Override
    void setValues(Table tbl);

    /** Get the cone angle, returns NaN if not supported.
        @return cone angle */
    double getConeAngle();

    /** Set the cone angle.
        @param value cone angle */
    void setConeAngle(double value);

    /** Gets a visibility footprint for the vehicle. This is for cases where the footprint is a
        set of complex polygons that can not be modeled using the values in {@link #getRadius()}
        and {@link #getConeAngle()}.
        @return The polygons that make up the footprints. Each inner array is a closed polygon
                defined by a series of lon,lat,alt values (inner arrays may not be null). The
                number of arrays is based on the total number of polygons required.
                <pre>
                  { { lon1,lat1,alt1, lon2,lat2,alt2 ... lonN,latN,altN, lon1,lat1,alt1 },
                    { lon1,lat1,alt1, lon2,lat2,alt2 ... lonN,latN,altN, lon1,lat1,alt1 },
                    ...
                  }
                </pre>
                If the footprint can be accurately modeled using the values in {@link #getRadius()}
                or {@link #getConeAngle()} this can just return null.
     */
    double[][] getFootprint();

    /** Sets a visibility footprint for the vehicle.
        @param fp The footprint, see {@link #getFootprint()} for details.
     */
    void setFootprint(double[][] fp);
  }

  /** A point that can be pointed to by a vehicle. */
  public static class LocationImpl implements Location {
    protected final String   className;  // Class descriptor (set in constructor).
    protected final Vehicle  vehicle;    // The associated vehicle (set in constructor).
    protected final Table    attributes; // Rendering attributes (set in constructor).
    protected       String   label;      // The label.
    protected       Position pos;        // The position.
    protected       double   radius;     // The effective radius.

    /** Creates a new instance. */
    LocationImpl(Vehicle veh) {
      this("Location",veh,"LOCATION");
    }

    /** Creates a new instance. */
    protected LocationImpl(Vehicle veh, String label) {
      this("Location",veh,label);
    }

    /** Creates a new instance.
        @param className The name of the class.
        @param veh       The vehicle instance to use for refreshes.
        @param label     The label to display for this vehicle
     */
    protected LocationImpl(String className, Vehicle veh, String label) {
      this.className  = className;
      this.vehicle    = veh;
      this.label      = label;
      this.attributes = new Table();
      this.pos        = new Position(Position.GEODETIC);

      attributes.addListener(veh.tblListener);
    }

    /** Get the vehicle. */
    protected final Vehicle getVehicle() {
      return vehicle;
    }

    /** Refresh the point. */
    protected void refresh() {
      getVehicle().refresh();
    }

    /** Gets a description of the class. */
    @Override
    public String toString() {
      return className+": "+toTable();
    }

    @Override
    public void setValues(Table tbl) { Vehicle.setValues(this, tbl); }

    @Override
    public Object getAttribute (String key) { return attributes.get(key); }

    @Override
    public Table    toTable()       { return Vehicle.toTable(this); }
    @Override
    public double   getTime()       { return (pos==null)? Double.NaN: pos.getTime(); }
    @Override
    public Position getPos()        { return pos;        }
    @Override
    public Table    getAttributes() { return attributes; }
    @Override
    public String   getLabel()      { return label;      }
    @Override
    public double   getRadius()     { return radius;     }

    @Override
    public void setAttribute (String key, Object val) {
      if (val == null) attributes.remove(key);
      else             attributes.put(key, val);
    }

    @Override
    public final void setPos(Table tbl) { setPos(Position.fromGeo(tbl)); }
    @Override
    public final void setStateVector(StateVector sv) { setPos(sv); }

    @SuppressWarnings("deprecation")
    @Override
    public final void setStateVector(String fname) {
      // TODO: Chanre definition of ref to be MidasReference, then we won't be using the deprecated
      //       StateVector constructor. However, since ref is protected we need to do this with care.
      StateVector sv = new StateVector(vehicle.ref, fname);
      setStateVector(sv);
    }

    @Override
    public final void setPos(double alt, double lat, double lon) {
      setPos(Position.fromGeo(alt,lat,lon));
    }

    @Override
    public void setPos(Position p) {
      if ((p != null) && !equal(p,pos)) {
        pos = p;
        getVehicle().update(this);
        refresh();
      }
    }

    @Override
    public void setRadius(double value) {
      if (value != radius) {
        radius = value;
        refresh();
      }
    }

    @Override
    public void setTime(double time) {
      if ((pos != null) && (time != getTime())) {
        pos.setTime(time);
        refresh();
      }
    }

    @Override
    public void setLabel(String lbl) {
      if (!equal(label,lbl)) {
        label = lbl;
        refresh();
      }
    }

    /** Quick object comparison that checks for nulls. */
    protected boolean equal(Object a, Object b) {
      return (a == b) || ((a != null) && (b != null) && a.equals(b));
    }
  }

  /** A site location. */
  public static class SiteImpl extends LocationImpl implements Site {
    protected double linkFreq; // The link frequency.

    SiteImpl (Vehicle veh) {
      super("Site",veh,"SITE");
    }

    /** Creates a new instance.
        @param className The name of the class.
        @param veh       The vehicle instance to use for refreshes.
     */
    protected SiteImpl(String className, Vehicle veh) {
      super(className,veh,"SITE");
    }

    @Override
    public double getLinkFreq () { return linkFreq; }

    @Override
    public void setLinkFreq (double freq) {
      if (freq != linkFreq) {
        linkFreq = freq;
        refresh();
      }
    }
  }

  /** A point that can be pointed to by a vehicle. */
  public static class PointingPosImpl extends LocationImpl implements PointingPos {
    protected double     cangle;    // The cone angle.
    protected double[][] footprint; // The complex footprint (can be null)

    /** Creates a new instance. */
    PointingPosImpl(Vehicle veh, String label) {
      this("PointingPos",veh,label);
    }

    /** Creates a new instance.
        @param className The name of the class.
        @param veh       The vehicle instance to use for refreshes.
        @param label     The name of the pointing position.
     */
    protected PointingPosImpl(String className, Vehicle veh, String label) {
      super(className,veh,label);
    }

    @Override
    public double getConeAngle() { return cangle; }

    @Override
    public void setConeAngle(double value) {
      if (value != cangle) {
        cangle = value;
        refresh();
      }
    }

    @Override
    public double[][] getFootprint() {
      return footprint;
    }

    @Override
    public void setFootprint(double[][] fp) {
      if (fp != footprint) {
        footprint = fp;
        refresh();
      }
    }
  }
}
