# ****************************************************************************
# CUI
#
# The Advanced Framework for Simulation, Integration, and Modeling (AFSIM)
#
# The use, dissemination or disclosure of data in this file is subject to
# limitation or restriction. See accompanying README and LICENSE for details.
# ****************************************************************************
# * * ************************************** * *
# *   ****** Demonstration input file ******   *
# *   ******      UNCLASSIFIED        ******   *
# * * ************************************** * *

# --------------------------------------------------------------
#     This files defines air combat maneuvers to be used
#     with a timeline agent. Maenuvers defined are:
#     - engage/re-engage/pursue/push maneuver
#     - beam maneuver
#     - crank maneuver
#     - drag maneuver
#     Also included is a TurnHeading method that provides a new
#     relative heading to turn to based on maneuver conditions.
# --------------------------------------------------------------

script_interface

   script_debug_writes off

   # Returns a relative heading (degrees) to turn
   # relative to a track based on inputs
   # aPlatform - the platform to get the new relative heading for
   # aTrack - the track the new relative heading is in relation to
   # aTurnAngle - the angle in degrees to turn relative to the track
   # aTolerance - the values in degrees (+/-) within which the Platform will
   #              already be considered to be aTurnAngle from aTrack
   script double TurnHeading(WsfPlatform aPlatform,
                              WsfTrack aTrack,
                              double aTurnAngle,
                              double aTolerance)
      # Variable to store new relative heading return value
      # Defualt to 0 degrees, no turn
      double heading = 0;

      if (aPlatform.IsValid() && aTrack.IsValid())
      {
         double relativeBearing = aPlatform.RelativeBearingTo(aTrack);
         writeln_d("T= ", TIME_NOW);
         writeln_d("   ", aPlatform.Name(), " heading: ", aPlatform.Heading(), " deg.");
         writeln_d("   ", aPlatform.Name(), " relative bearing to track: ", relativeBearing, " deg.");

         # Check that relative bearing is not already within tolerance of
         # aTurnAngle and no need to change heading
         if (!Math.AngleIsBetween(Math.Fabs(relativeBearing), aTurnAngle - aTolerance, aTurnAngle + aTolerance))
         {
            if (relativeBearing > 0 )
            {
               # Turn left
               heading = -aTurnAngle + relativeBearing;
            }
            else
            {
               # Turn right
               heading = aTurnAngle + relativeBearing;
            }

            # Because we are adding angle, make sure relative heading is valid
            heading = Math.NormalizeAngleMinus180_180(heading);
            writeln_d("   ", aPlatform.Name(), " new relative heading: ", heading, " deg.");
         }
         else
         {
            writeln_d("   ", aPlatform.Name(), " already within ",
                      aTolerance, " deg of ", aTurnAngle, " deg to track." );
         }
      }

      return heading;
   end_script

   # Calculate the relative positioning of aPlatform and aTrack
   # Borrowed from common_platform_script
   # aPlatform - the platform of interest for relative position
   # aTrack - the track to determine the relative position to
   #          with respect to the platform
   # aAngleTolerance - angle in degrees used to bound how close
   #                   two heading are to be considered the same
   # returns a string indicating the relative positioning
   #        (head-to-head, head-to-tail, etc.)
   script string GetPositioning(WsfPlatform aPlatform, WsfTrack aTrack, double aAngleTolerance)
      # Are we heading the same direction?
      bool sameHeading = false;
      bool oppHeading = false;
      bool oppHeadingValid = aTrack.HeadingValid();
      double headingDiff = MATH.Fabs(MATH.NormalizeAngleMinus180_180(aPlatform.Heading() - aTrack.Heading()));

      if (oppHeadingValid && headingDiff < aAngleTolerance)
      {
         sameHeading = true;
      }
      if (oppHeadingValid && headingDiff > (180 - aAngleTolerance))
      {
         oppHeading = true;
      }

      # Is either one of us pointing at the other?
      # Apparently bearing is always valid (?)
      bool pointingMeYou = false;
      bool pointingYouMe = false;

      double pMeYou = MATH.Fabs(aPlatform.RelativeBearingTo(aTrack));
      double pYouMe = MATH.Fabs(aTrack.RelativeBearingTo(aPlatform));

      if (pMeYou < aAngleTolerance)
      {
         pointingMeYou = true;
      }
      if (pYouMe < aAngleTolerance)
      {
         pointingYouMe = true;
      }

      # Put them together and we've got relative positioning
      string positioning = "";
      if (sameHeading && pointingMeYou)
      {
         positioning = "head-to-tail";
      }
      else if (sameHeading && pointingYouMe)
      {
         positioning = "tail-to-head";
      }
      else if (oppHeading && (pointingMeYou || pointingYouMe))
      {
         positioning = "head-to-head";
      }
      else if (pointingMeYou && pointingYouMe)
      {
         positioning = "head-to-head";
      }
      else if (oppHeading)
      {
         positioning = "tail-to-tail";
      }
      else if (pointingMeYou)
      {
         positioning = "me-facing-target";
      }
      else if (pointingYouMe)
      {
         positioning = "target-facing-me";
      }
      else
      {
         positioning = "none";
      }
      # Debug, in case angle math is bad
#      writeln_d("    ", aPlatform.Name(), " to ", aTrack.TargetName(), ", headingDiff: ", headingDiff, ", pMeYou: ", pMeYou, ", pYouMe: ", pYouMe);
#      writeln_d("    meHeading: ", aPlatform.Heading(), ", youHeading: ", aTrack.Heading());
#      writeln_d("    sameHeading: ", sameHeading, ", oppHeading: ", oppHeading,
#                ", pointingMeYou: ", pointingMeYou, ", pointingYouMe: ", pointingYouMe,
#                ", positioning: ", positioning);

      return positioning;
   end_script


   # Commands passed in platform to do an engage maneuver
   # Relative to passed in track
   # Mode (pure, lead) input string determines if mode will be
   #      pure pursuit or lead to predicted intercept point
   # Return true if engage maneuver command is successful
   script bool Engage(WsfPlatform aPlatform, WsfTrack aTrack, string aMode, double aSpeed, double aAlt, double aGs)
      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.

      double leadTime = 15.0; #sec

      WsfGeoPoint targetPoint = WsfGeoPoint();

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return false;
      }

      # Determine location to fly toward based on pursuit mode
      if (aMode == "intercept")
      {
         WsfWaypoint wpt = WsfWaypoint();
         double tti = aPlatform.InterceptLocation3D(aTrack, wpt);
         if (tti > 0.0)
         {
            targetPoint = wpt.Location();
         }
         else
         {
            targetPoint = aTrack.LocationAtTime(TIME_NOW + leadTime);
         }
      }
      else if (aMode == "lead")
      {
         targetPoint = aTrack.LocationAtTime(TIME_NOW + leadTime);
      }
      else #if (aMode == "pure")
      {
         # All other input will be considered 'pure' pursuit
         targetPoint = aTrack.LocationAtTime(TIME_NOW);
      }

      double relativeBearing = aPlatform.RelativeBearingTo(targetPoint);

      # Fly determined course
      double radialAccel = aGs * Earth.ACCEL_OF_GRAVITY();
      bool ok = aPlatform.TurnToRelativeHeading(relativeBearing, radialAccel);
      ok = ok && aPlatform.GoToSpeed(aSpeed);
      ok = ok && aPlatform.GoToAltitude(aAlt);
      return ok;
   end_script

   # Commands passed in platform to do a fire maneuver
   # Relative to passed in track
   # Return true if fire maneuver command is successful
   script bool FireManeuver(WsfPlatform aPlatform, WsfTrack aTrack)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # TODO make method arguments?
      double fireMach = 1.5; # Speed to fire weapon
      double pitchAngle = 15.0; # Degrees of pitch up (loft shot)
      double distance = 100000.0; # Distance in meters to new altitude point

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return false;
      }

      # Determine heading toward track
      double relativeBearing = aPlatform.RelativeBearingTo(aTrack);

      # Determine new altitude to give desired pitch angle
      # Arbitrary point that is pitch angle up, 100 km distant
      double altitudeDelta = distance * Math.Tan(pitchAngle);
      double newAltitude = aPlatform.Altitude() + altitudeDelta;

      # Determine an approximate climb rate based on
      # desired angle and new point
      double speed = aPlatform.Speed();
      double time = distance / speed;
      double climbRate = altitudeDelta / time;

      # Fly determined course
      bool ok = aPlatform.TurnToRelativeHeading(relativeBearing);
      ok = ok && aPlatform.GoToMachNumber(fireMach);
      ok = ok && aPlatform.GoToAltitude(newAltitude, climbRate);
      return ok;
   end_script

   # Commands passed in platform to do a beam maneuver
   # Relative to passed in track
   # Return final heading if crank maneuver command is successful
   # or -1 if unsuccessful
   script double Beam(WsfPlatform aPlatform, WsfTrack aTrack)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # Angle in degrees to turn to get to "beam" on track.
      double beamAngle = 90;
      # Min/max angle in degrees that aircraft needs to be
      # between relative to track to be considered on "beam"
      double beamTolerance = 1.5;

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return false;
      }

      # Heading when we have finished maneuver
      double finalHeading = 0;

      # Determine new heading
      if (aTrack.HeadingValid())
      {
         # Check for condition when we know track's heading
         # this gives a more accurate "beam" to threat course
         if (aPlatform.RelativeBearingTo(aTrack) > 0)
         {
            # Turn left
            finalHeading = aTrack.Heading() + beamAngle;
         }
         else
         {
            # Turn right
            finalHeading = aTrack.Heading() - beamAngle;
         }
      }
      else
      {
         # Determine a relative heading based only on the track's location
         double relativeHeading = TurnHeading(aPlatform, aTrack, beamAngle, beamTolerance);
         finalHeading = Math.NormalizeAngle0_360(aPlatform.Heading() + relativeHeading);
      }

      # Turn to new heading, maintaining speed and altitude
      bool ok = aPlatform.TurnToHeading(finalHeading);
      if (!ok)
      {
         finalHeading = -1.0;
      }
      return finalHeading;
   end_script

   # Commands passed in platform to do a crank maneuver
   # Relative to passed in track
   # Return final heading if drag maneuver command is successful
   # or -1 if unsuccessful
   script double Crank(WsfPlatform aPlatform, WsfTrack aTrack, double crankAngle, double aSpeed)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # TODO make method arguments?
      # Angle in degrees to offset track. Should be close to,
      # but not at, max radar azimuth
     #double crankAngle = 35;
      # Min/max angle in degrees that aircraft needs to be
      # between relative to track to be considered cranking
      double crankTolerance = 1.5;

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return false;
      }

      double relativeHeading = TurnHeading(aPlatform, aTrack, crankAngle, crankTolerance);

      # Heading when we have finished maneuver
      double finalHeading = Math.NormalizeAngleMinus180_180(aPlatform.Heading() + relativeHeading);

      # Turn to new heading, maintaining speed and altitude
      bool ok = aPlatform.TurnToRelativeHeading(relativeHeading);
      ok = ok && aPlatform.GoToSpeed(aSpeed);
      if (!ok)
      {
         finalHeading = -1.0;
      }
      return finalHeading;
   end_script

   # Commands passed in platform to do a drag maneuver
   # Relative to passed in track
   # aSpeed - speed (m/s) to drag at
   # aAlt - Altitude to offset from current altitude
   # Return final heading if drag maneuver command is successful
   # or -1 if unsuccessful
   script double Drag(WsfPlatform aPlatform, WsfTrack aTrack, double aSpeed, double aAlt, double aGs)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # TODO make method arguments?
      # Flight parameters when dragging for speed (Mach) and altitude offest (meters) from start
      double dragMach = 1.1;
      # Angle in degrees to turn to drag on track.
      double dragAngle = 179;
      # Min/max angle in degrees that aircraft needs to be
      # between relative to track to be considered dragging
      double dragTolerance = 5;

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return false;
      }

      # Heading when we have finished maneuver
      double finalHeading = 0;
      # Determine new heading for drag maneuver
      string positioning = GetPositioning(aPlatform, aTrack, dragTolerance);
      if (aTrack.HeadingValid() &&
          (positioning == "tail-to-head" ||
           positioning == "head-to-head" ||
           positioning == "target-facing-me")
         )
      {
         # Check for condition when we know track's heading and
         # track is facing platform.
         # This gives a more accurate "drag" to threat course
         finalHeading = aTrack.Heading();
      }
      else
      {
         # Don't know track heading or not facing platform, turn
         # based on relative bearing to track location
         double relativeHeading = TurnHeading(aPlatform, aTrack, dragAngle, dragTolerance);
         finalHeading = Math.NormalizeAngle0_360(aPlatform.Heading() + relativeHeading);
      }

      # Fly new course
      double radialAccel = aGs * Earth.ACCEL_OF_GRAVITY();
      bool ok = aPlatform.TurnToHeading(finalHeading, radialAccel);
      ok = ok && aPlatform.GoToSpeed(aSpeed);
      ok = ok && aPlatform.GoToAltitude(aAlt);
      if (!ok)
      {
         finalHeading = -1.0;
      }
      return finalHeading;
   end_script

   # Do a 180 degree turn in the specified direction,
   # at specified mach, maintaining altitude
   script double TurnDirection(WsfPlatform aPlatform, string aDirection, double aSpeed, double aGs)
      # Angle in degrees to turn.
      double turnAngle = 179.5;
      # Heading when we have finished maneuver
      double finalHeading = 0;

      if (aDirection == "left")
      {
         finalHeading = Math.NormalizeAngle0_360(aPlatform.Heading() - turnAngle);
      }
      else
      {   # "right"
         finalHeading = Math.NormalizeAngle0_360(aPlatform.Heading() + turnAngle);
      }

      # Fly new course
      double radialAccel = aGs * Earth.ACCEL_OF_GRAVITY();
      bool ok = aPlatform.TurnToHeading(finalHeading, radialAccel);
      ok = ok && aPlatform.GoToSpeed(aSpeed);
      ok = ok && aPlatform.GoToAltitude(aPlatform.Altitude());
      if (!ok)
      {
         finalHeading = -1.0;
      }
      return finalHeading;
   end_script

   # Commands passed in platform to do a post hole maneuver
   # Direction (left/right) controls direction of post hole
   # Return final heading if drag maneuver command is successful
   # or -1 if unsuccessful
   script double PostHole(WsfPlatform aPlatform, string aDirection)
      # Distance in meters to offset the range to the post hole waypoint
      double offsetRange = 3000;
      # Angle down (deg) to offset post hole waypoint
      double offsetElevation = -75.0;

      if (!aPlatform.IsValid())
      {
         return false;
      }

      # Use left/right direction to determine the offset bearing for
      # the post hole waypoint. Heading + 120 deg will put the point
      # slightly behind current location
      double offsetBearing = 0;
      if (aDirection == "left")
      {
         offsetBearing = Math.NormalizeAngleMinus180_180(aPlatform.Heading() + 120);
      }
      else
      {
         offsetBearing = Math.NormalizeAngleMinus180_180(aPlatform.Heading() - 120);
      }

      # Build point to fly to for post hole
      WsfGeoPoint pt = aPlatform.Location();
      pt.OffsetRBE(offsetRange, offsetBearing, offsetElevation);

      # Create waypoint so we can specify final heading/speed
      WsfWaypoint waypoint = WsfWaypoint();
      waypoint.SetHeading(aPlatform.Heading());
      waypoint.SetSpeed(aPlatform.Speed());
      waypoint.SetLocation(pt);

      # Build route from post hole waypoint
      WsfRoute newRoute = WsfRoute();
      newRoute.Append(waypoint);

      # Heading when we have finished maneuver
      double finalHeading = aPlatform.Heading();

      bool ok = aPlatform.FollowRoute(newRoute);
      if (!ok)
      {
         finalHeading = -1.0;
      }
      return finalHeading;
   end_script



   script double GetTurnRateLimit(WsfPlatform aPlatform)
       Array<double> turnRadiusArray = aPlatform.Mover().PropertyDouble("turn_radius");

       double turnRateLimit = 0;
       if (turnRadiusArray.Size() > 0)
       {
          double turnRate = turnRadiusArray[0];
          double speed = aPlatform.Speed();
          turnRateLimit = speed/turnRate;
          turnRateLimit = turnRateLimit * MATH.DEG_PER_RAD();
       }
       return turnRateLimit;
   end_script

   script double GetDefaultG(WsfPlatform aPlatform)
      Array<double> defaultAccelArray = aPlatform.Mover().PropertyDouble("default_radial_acceleration");
       if (defaultAccelArray.Size() > 0)
       {
          return defaultAccelArray[0];
       }
       return 0.0;
   end_script

   script double TimeToPerformBeam(WsfPlatform aPlatform, WsfTrack aTrack)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # Angle in degrees to turn to get to "beam" on track.
      double beamAngle = 90;
      # Min/max angle in degrees that aircraft needs to be
      # between relative to track to be considered on "beam"
      double beamTolerance = 1.5;

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return -1.0;
      }

      # Heading when we have finished maneuver
      double finalHeading = 0;

      # Determine new heading
      if (aTrack.HeadingValid())
      {
         # Check for condition when we know track's heading
         # this gives a more accurate "beam" to threat course
         if (aPlatform.RelativeBearingTo(aTrack) > 0)
         {
            # Turn left
            finalHeading = aTrack.Heading() + beamAngle;
         }
         else
         {
            # Turn right
            finalHeading = aTrack.Heading() - beamAngle;
         }
      }
      else
      {
         # Determine a relative heading based only on the track's location
         double relativeHeading = TurnHeading(aPlatform, aTrack, beamAngle, beamTolerance);
         finalHeading = Math.NormalizeAngle0_360(aPlatform.Heading() + relativeHeading);
      }


      //calculate time to perform the beam given the calculated heading

      #RadialAccel = Speed^2/Radius

      double RadialAccel = GetDefaultG(aPlatform) * Earth.ACCEL_OF_GRAVITY();
      if (RadialAccel < 2.0)
      {
         RadialAccel = 2.0 * Earth.ACCEL_OF_GRAVITY();
      }

      double TurnAzimuth = MATH.Fabs(finalHeading);
      double Speed = aPlatform.Speed();
      double Radius = Speed * Speed / RadialAccel;
      double TurningCircleCircumference = 2.0 * MATH.PI() * Radius;
      double TurningArc = (TurnAzimuth/360) * TurningCircleCircumference;
      double TimeToTurn = TurningArc / Speed;
      return TimeToTurn;


   end_script

   script double TimeToPerformCrank(WsfPlatform aPlatform, WsfTrack aTrack)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # TODO make method arguments?
      # Angle in degrees to offset track. Should be close to,
      # but not at, max radar azimuth
      double crankAngle = 35;
      # Min/max angle in degrees that aircraft needs to be
      # between relative to track to be considered cranking
      double crankTolerance = 1.5;

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return -1.0;
      }

      double relativeHeading = TurnHeading(aPlatform, aTrack, crankAngle, crankTolerance);

      # Heading when we have finished maneuver
      double finalHeading = Math.NormalizeAngleMinus180_180(aPlatform.Heading() + relativeHeading);

      double RadialAccel = GetDefaultG(aPlatform) * Earth.ACCEL_OF_GRAVITY();
      if (RadialAccel < 2.0)
      {
         RadialAccel = 2.0 * Earth.ACCEL_OF_GRAVITY();
      }

      double TurnAzimuth = MATH.Fabs(finalHeading);
      double Speed = aPlatform.Speed();
      double Radius = Speed * Speed / RadialAccel;
      double TurningCircleCircumference = 2.0 * MATH.PI() * Radius;
      double TurningArc = (TurnAzimuth/360) * TurningCircleCircumference;
      double TimeToTurn = TurningArc / Speed;
      return TimeToTurn;

   end_script

   script double TimeToPerformDrag(WsfPlatform aPlatform, WsfTrack aTrack)

      # Variables used in the script to generate the maneuver.
      # Change these to modify the behavior.
      # TODO make method arguments?
      # Flight parameters when dragging for speed (Mach) and altitude offest (meters) from start
      double dragMach = 1.1;
      double dragAltitudeOffset = -1000;
      # Angle in degrees to turn to drag on track.
      double dragAngle = 179;
      # Min/max angle in degrees that aircraft needs to be
      # between relative to track to be considered dragging
      double dragTolerance = 5;

      if (!aPlatform.IsValid() || !aTrack.IsValid())
      {
         return -1.0;
      }

      # Heading when we have finished maneuver
      double finalHeading = 0;
      # Determine new heading for drag maneuver
      string positioning = GetPositioning(aPlatform, aTrack, dragTolerance);
      if (aTrack.HeadingValid() &&
          (positioning == "tail-to-head" ||
           positioning == "head-to-head" ||
           positioning == "target-facing-me")
         )
      {
         # Check for condition when we know track's heading and
         # track is facing platform.
         # This gives a more accurate "drag" to threat course
         finalHeading = aTrack.Heading();
      }
      else
      {
         # Don't know track heading or not facing platform, turn
         # based on relative bearing to track location
         double relativeHeading = TurnHeading(aPlatform, aTrack, dragAngle, dragTolerance);
         finalHeading = Math.NormalizeAngle0_360(aPlatform.Heading() + relativeHeading);
      }

      double RadialAccel = GetDefaultG(aPlatform) * Earth.ACCEL_OF_GRAVITY();
      if (RadialAccel < 2.0)
      {
         RadialAccel = 2.0 * Earth.ACCEL_OF_GRAVITY();
      }
      double TurnAzimuth = MATH.Fabs(finalHeading);
      double Speed = aPlatform.Speed();
      double Radius = Speed * Speed / RadialAccel;
      double TurningCircleCircumference = 2.0 * MATH.PI() * Radius;
      double TurningArc = (TurnAzimuth/360) * TurningCircleCircumference;
      double TimeToTurn = TurningArc / Speed;
      return TimeToTurn;

   end_script


   script void Offset(WsfPlatform flyer, WsfGeoPoint target, double angle, double alt, double speed, double gees)
      angle = MATH.NormalizeAngleMinus180_180(angle);
      double offsetTrueBearing = flyer.TrueBearingTo(target) + angle;
      double radialAccel = gees * Earth.ACCEL_OF_GRAVITY();
      double climbRate = MATH.Sin(30) * speed;
      bool ok =       flyer.GoToSpeed(speed, 9999, true);
           ok = ok && flyer.GoToAltitude(alt, climbRate, true);
           ok = ok && flyer.TurnToHeading(offsetTrueBearing, radialAccel);
   end_script


end_script_interface
