-- command a Copter to climb a wall at a specific distance
--
-- CAUTION: This script only works for Copter
--    a) waits for the vehicle to be switched to Guided mode (does nothing)
--    b) accepts pilot roll, pitch, throttle and yaw input
--    c) manual mode
--        - roll and pitch cause vehicle to fly horizontally in body frame at up to WP_SPEED
--        - yaw controls turn rate
--        - throttle controls climb/descent rate at up to PILOT_SPEED_UP/DOWN?  (WP_SPEED_UP, WPNAV_SPEED_DN?)
--    d) autonomous mode
--        - roll is unchanged from manual mode
--        - yaw is unchanged from manual mode
--        - pitch controls target distance to wall (limited to no less than 2m, no more than 8m)
--        - throttle input causes vehicle to switch back to manual mode
--        - climbs at 50cm/s (need parameter?) stopping at 2m intervals
--    e) ZIGZAG_SaveWP aux switch controls switching in and out of auto mode

-- constants
local update_rate_ms = 10           -- script updates at 100hz
local update_rate_dt = update_rate_ms / 1000.0 -- update rate in seconds
local copter_guided_mode_num = 4    -- Copter's guided flight mode is mode 4
local aux_switch_function = 61      -- auxiliary switch function controlling mode.  61 is ZIGZAG_SaveWP
local climb_accel_max = 0.25        -- climb rate acceleration max in m/s/s
local climb_rate_max = 2            -- climb rate max in m/s
local climb_rate_chg_max = climb_accel_max * update_rate_dt -- max change in climb rate in a single iteration
local roll_pitch_speed_max = 2      -- horizontal speed max in m/s
local roll_pitch_accel_max = 0.25      -- horizontal acceleration max in m/s/s
local roll_pitch_speed_chg_max = roll_pitch_accel_max * update_rate_dt  -- max change in roll or pitch speed in a single iteration
local rc_roll_ch = 1                -- RC input channel for roll
local rc_pitch_ch = 2               -- RC input channel for pitch
local rc_throttle_ch = 3            -- RC input channel for throttle
local rc_pwm_dz = 50                -- RC input deadzone in pwm
local wall_pitch_speed_max = roll_pitch_speed_max / 2.0 -- wall control will never overpower pilot
local wall_dist_to_speed_P = 0.5    -- P gain for converting distance to wall to forward speed
local climb_pause_sec = 3           -- pause for this many seconds during each interval
local climb_pause_counter_max = climb_pause_sec / update_rate_dt -- pause control's loop counter's initial value

-- user parmeters
local climb_dist_max = 10           -- vehicle moves to next lane at this many meters above home
local climb_stop_interval = 1       -- vehicle stops after climbing this many meters
local lane_width = 2                -- each lane is 2m apart horizontally

-- lane variables
local lane_number = 0               -- current lane number
local lane_shift_roll_speed = 1     -- vehicle moves right at this speed in m/s
local lane_shift_counter_max = lane_width / (lane_shift_roll_speed * update_rate_dt)
local lane_shift_counter = 0        -- current lane shift counter value
local lane_climb_rate = climb_rate_max / 2.0 -- autonomous climb is always 1/2 maximum

-- global variables
local aux_sw_pos_prev = 0           -- aux switch previous position
local climb_mode = 0                -- 0:manual, 1:automatic climb
local climb_rate = 0                -- current climb rate (negative is down, positive is up)
local climb_total = 0               -- total number of meters above home
local climb_interval_total = 0      -- meters climbed since last stop
local climb_pause_counter = 0       -- current pause counter value
local roll_speed = 0                -- target horizontal roll speed in m/s
local pitch_speed = 0               -- target horizontal pitch speed in m/s
local wall_dist_target = 5          -- target distance to wall

-- get the maximum speed in order to decelerate to zero within the given distance
function get_speed_max(distance, accel_max)
  return math.sqrt(2.0 * math.max(0, distance) * accel_max)
end

-- the main update function that uses the takeoff and velocity controllers to fly a rough square pattern
function update()

  -- reset state when disarmed
  if not arming:is_armed() then
    climb_mode = 0
    climb_rate = 0
    climb_total = 0
    climb_pause_counter = 0
    climb_interval_total = 0
    roll_speed = 0
    pitch_speed = 0
    lane_number = 0
    lane_shift_counter = 0
    lane_climb_rate = math.abs(lane_climb_rate)
    return update, update_rate_ms
  end

  -- if not in Guided mode do nothing
  if vehicle:get_mode() ~= copter_guided_mode_num then
    return update, update_rate_ms
  end

  -- read switch input from RCx_FUNCTION
  local aux_switch = rc:find_channel_for_option(aux_switch_function)
  if aux_switch then
    local sw_pos = aux_switch:get_aux_switch_pos()
    if (sw_pos ~= aux_sw_pos_prev) then
      -- only process input if switch position changed
      aux_sw_pos_prev = sw_pos
      if (sw_pos == 0) then
        -- pilot has selected manual mode
        if (climb_mode ~= 0) then
          gcs:send_text(0, "WallClimb: pilot switch to Manual")
          climb_mode = 0
        end
      elseif (sw_pos == 2) then
        -- pilot has selected auto mode
        if (climb_mode ~= 1) then
          gcs:send_text(0, "WallClimb: pilot switch to Auto")
          climb_mode = 1
        end
      end
    end
  end

  -- read normalised roll, pitch and throttle input
  local roll_input = 0
  local rc_roll_pwm = rc:get_pwm(rc_roll_ch)
  if (rc_roll_pwm and rc_roll_pwm >= 1000 and rc_roll_pwm <= 2000 and (math.abs(rc_roll_pwm - 1500) > rc_pwm_dz)) then
    roll_input = (rc_roll_pwm - 1500) / 500.0
  end
  local pitch_input = 0
  local rc_pitch_pwm = rc:get_pwm(rc_pitch_ch)
  if (rc_pitch_pwm and rc_pitch_pwm >= 1000 and rc_pitch_pwm <= 2000 and (math.abs(rc_pitch_pwm - 1500) > rc_pwm_dz)) then
    pitch_input = (rc_pitch_pwm - 1500) / 500.0
  end
  local throttle_input = 0
  local rc_throttle_pwm = rc:get_pwm(rc_throttle_ch)
  if (rc_throttle_pwm and rc_throttle_pwm >= 1000 and rc_throttle_pwm <= 2000 and (math.abs(rc_throttle_pwm - 1500) > rc_pwm_dz)) then
    throttle_input = (rc_throttle_pwm - 1500) / 500.0
  end

  -- manual mode
  if (climb_mode == 0) then
    -- update target climb rate
    local climb_rate_target = throttle_input * climb_rate_max
    climb_rate = math.min(climb_rate_target, climb_rate + climb_rate_chg_max, climb_rate_max)
    climb_rate = math.max(climb_rate_target, climb_rate - climb_rate_chg_max, -climb_rate_max)
  end

  -- autonomous mode
  local wall_pitch_speed = 0
  local wall_roll_speed = 0
  if (climb_mode == 1) then
    -- convert rangefinder distance to pitch speed
    if rangefinder:has_data_orient(0) then
      local distance_m = rangefinder:distance_cm_orient(0) * 0.01
      wall_pitch_speed = (distance_m - wall_dist_target) * wall_dist_to_speed_P
      wall_pitch_speed = math.min(wall_pitch_speed, wall_pitch_speed_max)
      wall_pitch_speed = math.max(wall_pitch_speed, -wall_pitch_speed_max)
    end

    -- switch to manual if pilot provides throttle input
    if (math.abs(throttle_input) ~= 0) then
      gcs:send_text(0, "WallClimb: throttle input, switch to Manual")
      climb_mode = 0
    else
      -- update distance climbed
      local climb_chg = climb_rate * update_rate_dt
      climb_interval_total = climb_interval_total + climb_chg
      climb_total = climb_total + climb_chg

      -- determine if we should pause
      local dist_to_interval = climb_stop_interval - math.abs(climb_interval_total)
      if (dist_to_interval <= 0) then
        gcs:send_text(0, "WallClimb: pausing at " .. string.format("%3.1f", climb_total) .. "m")
        climb_pause_counter = climb_pause_counter_max
        climb_interval_total = 0

        -- determine if we should move to next lane
        if ((climb_total > climb_dist_max) or (climb_total <= 0)) then
          lane_number = lane_number + 1
          lane_climb_rate = -lane_climb_rate
          lane_shift_counter = lane_shift_counter_max
          gcs:send_text(0, "WallClimb: starting lane " .. tostring(lane_number))
        end
      end

      -- default target climb rate to lane climb rate
      local climb_rate_target = lane_climb_rate

      -- limit target speed so vehicle can stop before next interval
	  -- speed limit is always at least 0.1m/s so copter doesn't get stuck near interval
      local speed_max = math.max(get_speed_max(dist_to_interval, climb_accel_max), 0.1)
      climb_rate_target = math.max(climb_rate_target, -speed_max)
      climb_rate_target = math.min(climb_rate_target, speed_max)

      -- if paused set target climb rate to zero
      if (climb_pause_counter > 0) then
        climb_pause_counter = climb_pause_counter - 1
        climb_rate_target = 0
        if (climb_pause_counter == 0) then
          gcs:send_text(0, "WallClimb: restarting")
        end
      end

      -- if shifting lanes pause climb and roll right
      if (lane_shift_counter > 0) then
        lane_shift_counter = lane_shift_counter - 1
        wall_roll_speed = lane_shift_roll_speed
        climb_rate_target = 0
        -- if completing lane shift trigger another pause
        if (lane_shift_counter == 0) then
          climb_pause_counter = climb_pause_counter_max
        end
      end

      -- calculate acceleration limited climb rate
      if (climb_rate_target >= climb_rate) then
        climb_rate = math.min(climb_rate_target, climb_rate + climb_rate_chg_max, climb_rate_max)
      else
        climb_rate = math.max(climb_rate_target, climb_rate - climb_rate_chg_max, -climb_rate_max)
      end
    end
  end

  -- update target roll using both pilot and autonomous control (+ve right, -ve left)
  local roll_speed_target = roll_input * roll_pitch_speed_max + wall_roll_speed
  roll_speed = math.min(roll_speed_target, roll_speed + roll_pitch_speed_chg_max, roll_pitch_speed_max)
  roll_speed = math.max(roll_speed_target, roll_speed - roll_pitch_speed_chg_max, -roll_pitch_speed_max)

  -- update target pitch using both pilot and autonomous control (+ve forward, -ve backwards)
  local pitch_speed_target = -pitch_input * roll_pitch_speed_max + wall_pitch_speed
  pitch_speed = math.min(pitch_speed_target, pitch_speed + roll_pitch_speed_chg_max, roll_pitch_speed_max)
  pitch_speed = math.max(pitch_speed_target, pitch_speed - roll_pitch_speed_chg_max, -roll_pitch_speed_max)

  -- convert targets from body to earth frame and send to guided mode velocity controller
  local yaw_rad = ahrs:get_yaw()
  yaw_cos = math.cos(yaw_rad)
  yaw_sin = math.sin(yaw_rad)
  local target_vel_ned = Vector3f()
  target_vel_ned:x(pitch_speed * yaw_cos - roll_speed * yaw_sin)
  target_vel_ned:y(pitch_speed * yaw_sin + roll_speed * yaw_cos)
  target_vel_ned:z(-climb_rate)
  if not (vehicle:set_target_velocity_NED(target_vel_ned)) then
    gcs:send_text(0, "failed to execute velocity command")
  end

  return update, update_rate_ms
end

return update()
