# Copyright 2018 Stuart Buchanan
# This file is part of FlightGear.
#
# FlightGear 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, either version 2 of the License, or
# (at your option) any later version.
#
# FlightGear 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.
#
# You should have received a copy of the GNU General Public License
# along with FlightGear.  If not, see <http://www.gnu.org/licenses/>.
#
# MFD Surround
#
# Header fields at the top of the page
#
# PageGroup navigation, displayed in the bottom right of the
# FMS, and controlled by the FMS knob

# Set of pages, references by SVG ID
var PAGE_GROUPS = [

  { label: "MapPageGroupLabel",
          group: "MapPageGroup",
          pages: [ "NavigationMap", "TrafficMap", "Stormscope", "WeatherDataLink", "TAWSB"],
  },
  { label: "WPTGroupLabel",
          group: "WPTPageGroup",
          pages: [ "AirportInfo", "IntersectionInfo", "NDBInfo", "VORInfo", "UserWPTInfo"],
  },

  { label: "AuxGroupLabel",
          group: "AuxPageGroup",
          pages: [ "TripPlanning", "Utility", "GPSStatus", "XMRadio", "SystemStatus"],
  },

  { label: "FPLGroupLabel",
          group: "FPLPageGroup",
          pages: [ "ActiveFlightPlanNarrow", "FlightPlanCatalog", "StoredFlightPlan"],
  },

  { label: "LstGroupLabel",
          group: "LstPageGroup",
          pages: [ "Checklist"],
  },

  { label: "NrstGroupLabel",
          group: "NrstPageGroup",
          pages: [ "NearestAirports", "NearestIntersections", "NearestNDB", "NearestVOR", "NearestUserWaypoints", "NearestFrequencies", "NearestAirspaces"],
  }
];

# Mapping for header labels to specific FMS or ADC messages, and sprintf formatting
# to use
var HEADER_MAPPING = {
  "BRG" : { message : "FMSLegBearingMagDeg",      format : "%d°"},
  "XTK" : { message : "FMSLegCourseError",        format : "%.1fnm"},
  "DIS" : { message : "FMSDistance",              format : "%.1fnm"},
  "DTK" : { message : "FMSLegDesiredTrack",       format : "%d°"},
  "END" : { message : "EnduranceHrs",             format : "%.1fhrs"},
  "ESA" : { message : "EnRouteSafeAltitude",      format : "%dft"},    # TODO
  "ETA" : { message : "FMSEstimatedTimeArrival",  format : ""},        # TODO
  "ETE" : { message : "FMSEstimatedTimeEnroute",     format : ""},
  "FOD" : { message : "FMSFuelOverDestination",      format : "%dgal"},
  "FOB" : { message : "FuelOnBoard",                 format : "%dgal"},
  "GS"  : { message : "FMSGroundspeed",              format : "%dkts"},
  "MSA" : { message : "MinimumSafeAltitude",         format : "%dft"},    # TODO
  "TAS" : { message : "ADCTrueAirspeed",             format : "%dkts"},
  "TKE" : { message : "FMSLegTrackErrorAngle",       format : "%d°"},
  "TRK" : { message : "FMSLegTrack",                 format : "%d°"},
  "VSR" : { message : "FMSLegVerticalSpeedRequired", format : "%dfpm"},   # TODO
};

# Style element use for the AP Status indicator.  This is normally green text
# on a black background, but is highlighted when disengaged as black text on a yellow
# background for 5 seconds.
var AP_STATUS_STYLE = {
  CURSOR_BLINK_PERIOD : 0.5,
  HIGHLIGHT_COLOR :  "#ffff00",
  HIGHLIGHT_TEXT_COLOR : "#000000",
  NORMAL_TEXT_COLOR : "#00ff00",
};

# Style element for use by the flight director modes and armed indicators.
# This is normally green text on a black background, but when highlighted is
# black text on a green background
var FD_STATUS_STYLE = {
  CURSOR_BLINK_PERIOD : 0.5,
  HIGHLIGHT_COLOR :  "#00ff00",
  HIGHLIGHT_TEXT_COLOR : "#000000",
  NORMAL_TEXT_COLOR : "#00ff00",
};

var Surround =
{
  new : func (mfd, myCanvas, device, svg, pfd=0)
  {
    var obj = { parents : [
      Surround,
      MFDPage.new(mfd, myCanvas, device, svg, "Surround", ""),
    ] };

    obj.pfd = pfd;

    var textElements = [
      "Comm1StandbyFreq", "Comm1SelectedFreq",
      "Comm2StandbyFreq", "Comm2SelectedFreq",
      "CommVolume",
      "Nav1StandbyFreq", "Nav1SelectedFreq",
      "Nav2StandbyFreq", "Nav2SelectedFreq",
      "Nav1ID", "Nav2ID",
      "NavVolume",
    ];
    
    var fdTextElements = ["HeaderAPLateralArmed", "HeaderAPLateralActive", "HeaderAPVerticalArmed", "HeaderAPVerticalActive", "HeaderAPVerticalReference"];

    # Labels that show and hide for volume notification
    var volumeLabelElements = ["Comm2Label","CommVolumeLabel","Nav2Label","NavVolumeLabel"];
    
    obj.addTextElements(textElements);
    
    obj.addElements(volumeLabelElements);
    
    if (pfd) {
      obj.addTextElements(["HeaderFrom", "HeaderTo", "LegDistance", "LegBRG"]);
      obj.addTextElements(fdTextElements, FD_STATUS_STYLE);
      obj.setTextElements(fdTextElements, "");
      obj._apStatus = PFD.TextElement.new(obj.pageName, svg, "HeaderAPStatus", "", AP_STATUS_STYLE);
      obj._dto = PFD.HighlightElement.new(obj.pageName, svg, "HeaderDTO", "DTO");
      obj._leg = PFD.HighlightElement.new(obj.pageName, svg, "HeaderActiveLeg", "Leg");
      obj._old_lateral_armed = nil;  # We store the previous armed values so we can detect a transtion from armed to active.
      obj._old_vertical_armed = nil;
      obj._ap_on = 0;
    } else {
      obj.addTextElements(["Header1Label", "Header1Value",
                            "Header2Label", "Header2Value",
                            "Header3Label", "Header3Value",
                            "Header4Label", "Header4Value"]);
    }

    obj._comm1selected = PFD.HighlightElement.new(obj.pageName, svg, "Comm1Selected", "Comm1");
    obj._comm2selected = PFD.HighlightElement.new(obj.pageName, svg, "Comm2Selected", "Comm2");

    obj._nav1selected = PFD.HighlightElement.new(obj.pageName, svg, "Nav1Selected", "Nav1");
    obj._nav2selected = PFD.HighlightElement.new(obj.pageName, svg, "Nav2Selected", "Nav2");

    obj._comm1failed = PFD.HighlightElement.new(obj.pageName, svg, "Comm1Failed", "Comm1");
    obj._comm2failed = PFD.HighlightElement.new(obj.pageName, svg, "Comm2Failed", "Comm2");

    obj._nav1failed = PFD.HighlightElement.new(obj.pageName, svg, "Nav1Failed", "Nav1");
    obj._nav2failed = PFD.HighlightElement.new(obj.pageName, svg, "Nav2Failed", "Nav2");

    obj._canvas = myCanvas;
    obj._menuVisible = 0;
    obj._selectedPageGroup = 0;
    obj._selectedPage = 0;
    obj._selected_comm = 1;
    obj._selected_nav = 1;

    obj._elements = {};

    foreach (var pageGroup; PAGE_GROUPS) {
      var group = svg.getElementById(pageGroup.group);
      var label = svg.getElementById(pageGroup.label);
      assert(group != nil, "Unable to find element " ~ pageGroup.group);
      assert(label != nil, "Unable to find element " ~ pageGroup.label);
      obj._elements[pageGroup.group] = group;
      obj._elements[pageGroup.label] = label;

      foreach(var pg; pageGroup.pages) {
        var page = svg.getElementById(pg);
        assert(page != nil, "Unable to find element " ~ pg);
        obj._elements[pg] = page;
      }
    }

    # Timers to control when to hide the menu after inactivity, and when to load
    # a new page.
    obj._hideMenuTimer = maketimer(3, obj, obj.hideMenu);
    obj._hideMenuTimer.singleShot = 1;

    obj._hideCommVolumeTimer = maketimer(2, obj, obj.hideCommVolume);
    obj._hideCommVolumeTimer.singleShot = 1;

    obj._hideNavVolumeTimer = maketimer(2, obj, obj.hideNavVolume);
    obj._hideNavVolumeTimer.singleShot = 1;
    
    obj._loadPageTimer = maketimer(0.5, obj, obj.loadPage);
    obj._loadPageTimer.singleShot = 1;

    obj.hideMenu();
    
    obj.setController(fg1000.SurroundController.new(obj, svg, pfd));
    obj.hideCommVolume();
    obj.hideNavVolume();

    return obj;
  },
  
  handleNavComData : func(data) {
    foreach(var name; keys(data)) {
      var val = data[name];

      if (name == "Comm1SelectedFreq") me.setTextElement("Comm1SelectedFreq", sprintf("%0.03f", val));
      if (name == "Comm1StandbyFreq") me.setTextElement("Comm1StandbyFreq", sprintf("%0.03f", val));
      if (name == "Comm1Serviceable") {
        if (val == 1) {
          me._comm1failed.setVisible(0);
        } else {
          me._comm1failed.setVisible(1);
        }
      }

      if (name == "Comm2SelectedFreq") me.setTextElement("Comm2SelectedFreq", sprintf("%0.03f", val));
      if (name == "Comm2StandbyFreq") me.setTextElement("Comm2StandbyFreq", sprintf("%0.03f", val));
      if (name == "Comm2Serviceable") {
        if (val == 1) {
          me._comm2failed.setVisible(0);
        } else {
          me._comm2failed.setVisible(1);
        }
      }

      if (name == "CommSelected") {
        if (val == 1) {
          me._selected_comm=1;
          me._comm1selected.setVisible(1);
          me._comm2selected.setVisible(0);
        } else {
          me._selected_comm=2;
          me._comm1selected.setVisible(0);
          me._comm2selected.setVisible(1);
        }
      }

      if (name == "Nav1SelectedFreq") me.setTextElement("Nav1SelectedFreq", sprintf("%0.03f", val));
      if (name == "Nav1StandbyFreq") me.setTextElement("Nav1StandbyFreq", sprintf("%0.03f", val));
      if (name == "Nav1Serviceable") {
        if (val == 1) {
          me._nav1failed.setVisible(0);
        } else {
          me._nav1failed.setVisible(1);
        }
      }

      if (name == "Nav2SelectedFreq") me.setTextElement("Nav2SelectedFreq", sprintf("%0.03f", val));
      if (name == "Nav2StandbyFreq") me.setTextElement("Nav2StandbyFreq", sprintf("%0.03f", val));
      if (name == "Nav2Serviceable") {
        if (val == 1) {
          me._nav2failed.setVisible(0);
        } else {
          me._nav2failed.setVisible(1);
        }
      }

      if (name == "NavSelected") {
        if (val == 1) {
          me._selected_nav=1;
          me._nav1selected.setVisible(1);
          me._nav2selected.setVisible(0);
        } else {
          me._selected_nav=2;
          me._nav1selected.setVisible(0);
          me._nav2selected.setVisible(1);
        }
      }

      if (name == "Nav1ID") {
        if (val==0) {
          me.setTextElement("Nav1ID", "");
        } else {
          me.setTextElement("Nav1ID", val);
        }
      }
      if (name == "Nav2ID") {
          if (val==0) {
            me.setTextElement("Nav2ID", "");
          } else {
            me.setTextElement("Nav2ID", val);
          }
        }
      
      # NAV/COM Volume - display the current volume for 2 seconds in place of the
      # standby frequency.
      
      if (name == "Nav1Volume" or name == "Nav2Volume") {
        me.showNavVolume(val);
      }
      if (name == "Comm1Volume" or name == "Comm2Volume") {
        me.showCommVolume(val);
      }
    }
  },
  showCommVolume: func(val) {
    var commvol = sprintf("%d%%",int(val*100));
    if (me.getTextValue("CommVolume") == commvol) return;
    
    # Hide com2 standby and label
    me.getTextElement("Comm2StandbyFreq").setVisible(0);
    me.getElement("Comm2Label").setVisible(0);
    if (me._selected_comm == 2) me._comm2selected.setVisible(0);
    
    # Set and show COM volume
    me.setTextElement("CommVolume",commvol);
    me.getTextElement("CommVolume").setVisible(1);
    me.getElement("CommVolumeLabel").setVisible(1);
    
    # Start hide timer (2 secs according to the manual)
    me._hideCommVolumeTimer.stop();
    me._hideCommVolumeTimer.restart(2);
  },
  
  hideCommVolume: func() {
    # Hide comm vol and restore standby and label
	me.getTextElement("CommVolume").setVisible(0);
    me.getElement("CommVolumeLabel").setVisible(0);
    me.getTextElement("Comm2StandbyFreq").setVisible(1);
    me.getElement("Comm2Label").setVisible(1);
    if (me._selected_comm == 2) me._comm2selected.setVisible(1);
  },
  showNavVolume: func(val) {
    var navvol = sprintf("%d%%",int(val*100));
    if (me.getTextValue("NavVolume") == navvol) return;
    
    # Hide NAV2 standdby
    me.getTextElement("Nav2StandbyFreq").setVisible(0);
    me.getElement("Nav2Label").setVisible(0);
    if (me._selected_nav == 2) me._nav2selected.setVisible(0);
    
    # Set and show NAV volume
    me.setTextElement("NavVolume",navvol);
    me.getTextElement("NavVolume").setVisible(1);
    me.getElement("NavVolumeLabel").setVisible(1);
    
    # Start hide timer (2 secs according to the manual)
    me._hideNavVolumeTimer.stop();
    me._hideNavVolumeTimer.restart(2);
  },
  hideNavVolume: func() {
    # Hide Nav volume and show NAV2 standby again
    me.getTextElement("NavVolume").setVisible(0);
    me.getElement("NavVolumeLabel").setVisible(0);
    me.getTextElement("Nav2StandbyFreq").setVisible(1);
    me.getElement("Nav2Label").setVisible(1);
    if (me._selected_nav == 2) me._nav2selected.setVisible(1);
  },
  # Update Header data with FMS or ADC data.
  updateHeaderData : func(data) {

    if (me.pfd) {
      # From, To, leg distance and leg bearing headers
      if (data["FMSLegID"]) {
        if (data["FMSLegID"] == "") {
          # No Leg, so hide the headers
          me.setTextElement("HeaderTo", "");
          me.setTextElement("HeaderFrom", "");
          me._dto.setVisible(0);
          me._leg.setVisible(0);
        } else {
          me.setTextElement("HeaderTo", data["FMSLegID"]);
          me._leg.setVisible(1);

          if (data["FMSMode"] == "dto") {
            me.setTextElement("HeaderFrom", "");
            me._dto.setVisible(1);
          } else {
            me._dto.setVisible(0);
            me.setTextElement("HeaderFrom", data["FMSPreviousLegID"]);
          }
        }
      }

      # When the Autopilot Heading or Altitude modes moves from armed to active we flash the appropriate annunicator for 10 seconds.
      # Unfortunately as we use a TriggeredPropertyPublisher, we won't have both the HeaderAP[Vertical|Lateral]Active and HeaderAP[Vertical|Lateral]Armed
      # values at the same time so have to save off any change in the armed values to check against.

      if ((data["AutopilotHeadingMode"] != nil) and
          (data["AutopilotHeadingMode"] != me.getTextValue("HeaderAPLateralActive"))) {

        me.setTextElement("HeaderAPLateralActive", data["AutopilotHeadingMode"]);

        if ((data["AutopilotHeadingMode"] != "") and
            ((data["AutopilotHeadingMode"] == me._old_lateral_armed) or
             (data["AutopilotHeadingMode"] == me.getTextValue("HeaderAPLateralArmed"))))
        {
          # Transition from an armed mode to a new mode, so flash
          me.highlightTextElement("HeaderAPLateralActive", 10);
        }
      }

      if ((data["AutopilotAltitudeMode"] != nil) and
          (data["AutopilotAltitudeMode"] != me.getTextValue("HeaderAPVerticalActive"))) {

        me.setTextElement("HeaderAPVerticalActive", data["AutopilotAltitudeMode"]);

        if ((data["AutopilotAltitudeMode"] != "") and
            ((data["AutopilotAltitudeMode"] == me._old_vertical_armed) or
             (data["AutopilotAltitudeMode"] == me.getTextValue("HeaderAPVerticalArmed"))))
        {
          # Transition from an armed mode to a new mode, so flash
          me.highlightTextElement("HeaderAPVerticalActive", 10);
        }
      }

      if (data["AutopilotHeadingModeArmed"] != nil) {
        if (data["AutopilotHeadingModeArmed"] != me.getTextValue("HeaderAPLateralArmed")) me._old_lateral_armed = me.getTextValue("HeaderAPLateralArmed");
        me.setTextElement("HeaderAPLateralArmed", data["AutopilotHeadingModeArmed"]);
      }

      if (data["AutopilotAltitudeModeArmed"] != nil) {
        if (data["AutopilotAltitudeModeArmed"] != me.getTextValue("HeaderAPVerticalArmed")) me._old_lateral_armed = me.getTextValue("HeaderAPVerticalArmed");
        me.setTextElement("HeaderAPVerticalArmed", data["AutopilotAltitudeModeArmed"]);
      }

      # When the Autopilot is disengaged, the AP status element flashes for 5 seconds before disappearing
      if (data["AutopilotEnabled"] != nil) {

        if ((data["AutopilotEnabled"] == 1) and (me._ap_on == 0)) {
          # Toggle the AP on, stopping any flashing that might be occurring.
          me._apStatus.unhighlightElement();
          me._apStatus.setValue("AP");
          me._ap_on = 1;
        }

        if ((data["AutopilotEnabled"] == 0) and me._ap_on) {
          # Toggle the AP off, by flashing the AP Status element for 5 seconds before removing it.
          # Only do this if we're not already flashing.
          me._ap_on = 0;
          me._apStatus.highlightElement(5.0, "");
        }
      }

      if (data["AutopilotTargetVertical"] != nil) me.setTextElement("HeaderAPVerticalReference", data["AutopilotTargetVertical"]);
      if (data["FMSLegDesiredTrack"]) me.setTextElement("LegBRG", sprintf("%i°", data["FMSLegDesiredTrack"]));
      if (data["FMSLegDistanceNM"]) me.setTextElement("LegDistance", sprintf("%.1fnm", data["FMSLegDistanceNM"]));
    } else {
      # MFD - 4 configurable Headers
      var headers = ["Header1", "Header2", "Header3", "Header4"];
      foreach (var header; headers) {

        # Get the currently configured heading and set the surround to display it.
        var label = me.mfd.ConfigStore.get("MFD" ~ header);
        assert(label != nil, "No header configured in ConfigStore for " ~ header);
        me.setTextElement(header ~ "Label", label);

        # Determine how it maps to Emesary data notifications
        var mapping = HEADER_MAPPING[label];
        assert(mapping != nil, "No header mapping for " ~ label);

        if (data[mapping.message] != nil) {
          # Format and display the value
          var value = sprintf(mapping.format, data[mapping.message]);

          if (mapping.message == "FMSEstimatedTimeEnroute") {
            # Special case to format time strings.
            var hrs = int(data[mapping.message]);
            var mins = int(60*(data[mapping.message] - hrs));
            var secs = int(3600*(data[mapping.message] - hrs - mins/60));

            if (hrs == 0) {
              value = sprintf("%d:%02d", mins, secs);
            } else {
              value = sprintf("%d:%02d", hrs, mins);
            }
          }
          me.setTextElement(header ~ "Value", value);
        }
      }
    }
  },

  getCurrentPage : func()
  {
    var currentpage = PAGE_GROUPS[me._selectedPageGroup].pages[me._selectedPage];
    return me.getMFD().getPage(currentpage);
  },

  # Go to a define page in the MFD.  Only valid for MFDs, and mainly used as
  # a useability shortcut to avoid having to use the FMS knobs.
  goToPage : func(group, page)
  {
    # Not valid for the PFD.
    if (me.pfd) return;

    # Values may be passed as names or indices.
    if (int(group) == nil) {
      for (var i = 0; i < size(PAGE_GROUPS); i = i + 1) {
        if (group == PAGE_GROUPS[i].group) {
          me._selectedPageGroup = i;
        }
      }
    } else {
      assert(group < size(PAGE_GROUPS), "Page Group index " ~ group ~ " out of bounds");
      me._selectedPageGroup = group;
    }

    if (int(page) == nil) {
      for (var j = 0; j < size(PAGE_GROUPS[me._selectedPageGroup].pages); j = j + 1) {
        if (page == PAGE_GROUPS[me._selectedPageGroup].pages[j]) {
          me._selectedPage = j;
        }
      }
    } else {
      assert(page < size(PAGE_GROUPS[me._selectedPageGroup].pages), "Page Group index " ~ group ~ " out of bounds");
      me._selectedPage = page;
    }

    # Now we've updated the selected pages, then load it
    me.loadPage();
  },

  # Function to change a page based on the selection
  loadPage : func()
  {
    # Not valid for the PFD.
    if (me.pfd) return;

    var pageToLoad = PAGE_GROUPS[me._selectedPageGroup].pages[me._selectedPage];
    var page = me.getMFD().getPage(pageToLoad);

    assert(page != nil, "Unable to find page " ~ pageToLoad);
    me.device.selectPage(page);
  },
  incrPageGroup : func(val) {
    var incr_or_decr = (val > 0) ? 1 : -1;
    me._selectedPageGroup = math.mod(me._selectedPageGroup + incr_or_decr, size(PAGE_GROUPS));
    me._selectedPage = 0;
  },
  incrPage : func(val) {
    var incr_or_decr = (val > 0) ? 1 : -1;
    me._selectedPage = math.mod(me._selectedPage + incr_or_decr, size(PAGE_GROUPS[me._selectedPageGroup].pages));
  },
  showMenu : func()
  {
    # Not valid for the PFD.
    if (me.pfd) return;
    
    foreach(var pageGroup; PAGE_GROUPS)
    {
      if (PAGE_GROUPS[me._selectedPageGroup].label == pageGroup.label)
      {
        # Display the page group and highlight the label
        me._elements[pageGroup.group].setVisible(1);
        me._elements[pageGroup.label].setVisible(1);
        me._elements[pageGroup.label].setColor(0.7,0.7,1.0);

        foreach (var page; pageGroup.pages)
        {
          # Highlight the current page.
          if (pageGroup.pages[me._selectedPage] == page) {
            me._elements[page].setColor(0.7,0.7,1.0);
          } else {
            me._elements[page].setColor(0.7,0.7,0.7);
          }
        }
      }
      else
      {
        # Hide the pagegroup and unhighlight the label on the bottom
        me._elements[pageGroup.group].setVisible(0);
        me._elements[pageGroup.label].setVisible(1);
        me._elements[pageGroup.label].setColor(0.7,0.7,0.7);
      }
    }
    me._menuVisible = 1;
    me._hideMenuTimer.stop();
    me._hideMenuTimer.restart(3);
    me._loadPageTimer.stop();
    me._loadPageTimer.restart(0.5);

  },
  hideMenu : func()
  {
    foreach(var pageGroup; PAGE_GROUPS)
    {
      me._elements[pageGroup.group].setVisible(0);
      me._elements[pageGroup.label].setVisible(0);
    }
    me._menuVisible = 0;
  },
  isMenuVisible : func()
  {
    return me._menuVisible;
  },

};