class EarthRoverChassis:
    def __init__(self):
        rospy.init_node("earth_rover_chassis",
                        # log_level=rospy.DEBUG
                        )

        # rospy.on_shutdown(self.shutdown)

        # robot dimensions
        self.wheel_radius = rospy.get_param("~wheel_radius_meters", 1.0)
        self.wheel_distance = rospy.get_param("~wheel_distance_meters", 1.0)
        self.ticks_per_rotation = rospy.get_param("~ticks_per_rotation", 1.0)

        # speed parameters
        self.left_min_speed_meters_per_s = rospy.get_param(
            "~left_min_speed_meters_per_s", -1.0)
        self.left_max_speed_meters_per_s = rospy.get_param(
            "~left_max_speed_meters_per_s", 1.0)
        self.right_min_speed_meters_per_s = rospy.get_param(
            "~right_min_speed_meters_per_s", -1.0)
        self.right_max_speed_meters_per_s = rospy.get_param(
            "~right_max_speed_meters_per_s", 1.0)

        self.left_min_usable_command = rospy.get_param(
            "~left_min_usable_command", 0.10)
        self.right_min_usable_command = rospy.get_param(
            "~right_min_usable_command", 0.10)

        self.min_measurable_speed = rospy.get_param("~min_measurable_speed",
                                                    0.0001)

        # cmd_vel parameters
        # self.min_cmd_lin_vel = rospy.get_param("~min_cmd_lin_vel", 0.0)
        # self.min_cmd_ang_vel = rospy.get_param("~min_cmd_lin_vel", 0.0)

        # PID parameters
        self.enable_pid = rospy.get_param("~enable_pid", True)
        self.min_command = rospy.get_param("~min_command", -1.0)
        self.max_command = rospy.get_param("~max_command", 1.0)
        self.kp = rospy.get_param("~kp", 1.0)
        self.ki = rospy.get_param("~ki", 0.0)
        self.kd = rospy.get_param("~kd", 0.0)
        self.speed_smooth_k = rospy.get_param("~speed_smooth_k", 1.0)
        # self.output_deadzone = rospy.get_param("~output_deadzone", 0.1)
        self.output_noise = rospy.get_param("~output_noise", 0.01)

        # TF parameters
        self.child_frame = rospy.get_param("~odom_child_frame", "base_link")
        self.parent_frame = rospy.get_param("~odom_parent_frame", "odom")
        self.tf_broadcaster = tf.TransformBroadcaster()

        # Ultrasonic parameters
        self.stopping_distances_cm = rospy.get_param("~stopping_distances_cm",
                                                     None)
        self.easing_offset_cm = rospy.get_param("~easing_offset_cm", 5.0)
        self.easing_offset_cm = rospy.get_param("~min_tracking_lin_vel", 0.07)
        self.easing_offset_cm = rospy.get_param("~min_tracking_ang_vel", 5.0)
        self.front_sensors = rospy.get_param("~front_sensors", None)
        self.right_sensors = rospy.get_param("~right_sensors", None)
        self.left_sensors = rospy.get_param("~left_sensors", None)
        self.back_sensors = rospy.get_param("~back_sensors", None)

        assert self.stopping_distances_cm is not None
        assert self.front_sensors is not None
        assert self.right_sensors is not None
        assert self.left_sensors is not None
        assert self.back_sensors is not None

        # Motor controller objects
        left_motor_info = MotorInfo(
            "left motor", self.kp, self.ki, self.kd, self.speed_smooth_k,
            self.wheel_radius, self.ticks_per_rotation,
            self.left_min_usable_command, self.left_min_speed_meters_per_s,
            self.left_max_speed_meters_per_s, self.min_command,
            self.max_command, self.output_noise, self.min_measurable_speed)

        right_motor_info = MotorInfo(
            "right motor", self.kp, self.ki, self.kd, self.speed_smooth_k,
            self.wheel_radius, self.ticks_per_rotation,
            self.right_min_usable_command, self.right_min_speed_meters_per_s,
            self.right_max_speed_meters_per_s, self.min_command,
            self.max_command, self.output_noise, self.min_measurable_speed)

        self.left_motor = MotorController(left_motor_info)
        self.right_motor = MotorController(right_motor_info)

        self.linear_speed_mps = 0.0
        self.rotational_speed_mps = 0.0

        # Ultrasonic state tracking objects
        self.num_sensors = len(self.stopping_distances_cm)

        self.sensor_directions = [[Direction.FRONT, self.front_sensors],
                                  [Direction.BACK, self.back_sensors],
                                  [Direction.LEFT, self.left_sensors],
                                  [Direction.RIGHT, self.right_sensors]]

        self.trackers_indexed = [None for _ in range(self.num_sensors)]
        self.trackers_directed = {
            Direction.FRONT: TrackerCollection(),
            Direction.BACK: TrackerCollection(),
            Direction.LEFT: TrackerCollection(),
            Direction.RIGHT: TrackerCollection(),
        }

        for direction, sensor_indices in self.sensor_directions:
            for sensor_index in sensor_indices:
                stop_dist = self.stopping_distances_cm[sensor_index]
                ease_dist = stop_dist + self.easing_offset_cm
                tracker = UltrasonicTracker(stop_dist, ease_dist)
                self.trackers_indexed[sensor_index] = tracker
                self.trackers_directed[direction].append(tracker)

        # prev state tracking
        self.prev_left_output = 0.0
        self.prev_right_output = 0.0

        # odometry state
        self.odom_x = 0.0
        self.odom_y = 0.0
        self.odom_t = 0.0

        self.odom_vx = 0.0
        self.odom_vy = 0.0
        self.odom_vt = 0.0

        # Odometry message
        self.odom_msg = Odometry()
        self.odom_msg.header.frame_id = self.parent_frame
        self.odom_msg.child_frame_id = self.child_frame

        # Subscribers
        self.twist_sub = rospy.Subscriber("cmd_vel",
                                          Twist,
                                          self.twist_callback,
                                          queue_size=5)
        self.left_encoder_sub = rospy.Subscriber("left/left_encoder/ticks",
                                                 Int64,
                                                 self.left_encoder_callback,
                                                 queue_size=50)
        self.right_encoder_sub = rospy.Subscriber("right/right_encoder/ticks",
                                                  Int64,
                                                  self.right_encoder_callback,
                                                  queue_size=50)
        self.ultrasonic_sub = rospy.Subscriber(
            "earth_rover_teensy_bridge/ultrasonic",
            Float32MultiArray,
            self.ultrasonic_callback,
            queue_size=15)

        # Publishers
        self.left_dist_pub = rospy.Publisher("left/left_encoder/distance",
                                             Float64,
                                             queue_size=5)
        self.right_dist_pub = rospy.Publisher("right/right_encoder/distance",
                                              Float64,
                                              queue_size=5)
        self.left_speed_pub = rospy.Publisher("left/left_encoder/speed",
                                              Float64,
                                              queue_size=5)
        self.right_speed_pub = rospy.Publisher("right/right_encoder/speed",
                                               Float64,
                                               queue_size=5)
        self.left_command_pub = rospy.Publisher("left/command_speed",
                                                Float64,
                                                queue_size=5)
        self.right_command_pub = rospy.Publisher("right/command_speed",
                                                 Float64,
                                                 queue_size=5)
        self.odom_pub = rospy.Publisher("odom", Odometry, queue_size=5)

        # Services
        self.tuning_service = rospy.Service("tune_motor_pid", TuneMotorPID,
                                            self.tune_motor_pid)

    def tune_motor_pid(self, request):
        kp = request.kp
        ki = request.ki
        kd = request.kd
        rospy.loginfo("Updated PID constants to kp=%s, ki=%s, kd=%s" %
                      (kp, ki, kd))

        self.left_motor.tune_pid(kp, ki, kd)
        self.right_motor.tune_pid(kp, ki, kd)

        return TuneMotorPIDResponse()

    def twist_callback(self, twist_msg):
        self.linear_speed_mps = twist_msg.linear.x  # m/s
        angular_speed_radps = twist_msg.angular.z  # rad/s

        # if abs(self.linear_speed_mps) < self.min_cmd_lin_vel and self.linear_speed_mps != 0.0:
        #     # assign linear_speed_mps the minimum speed with direction sign applied
        #     self.linear_speed_mps = math.copysign(self.min_cmd_lin_vel, self.linear_speed_mps)

        # if abs(angular_speed_radps) < self.min_cmd_ang_vel and angular_speed_radps != 0.0:
        #     angular_speed_radps = math.copysign(self.min_cmd_ang_vel, angular_speed_radps)

        # arc = angle * radius
        # rotation speed at the wheels
        self.rotational_speed_mps = angular_speed_radps * self.wheel_distance / 2

    def left_encoder_callback(self, enc_msg):
        self.left_motor.enc_tick = -enc_msg.data

        dt = self.left_motor.get_dt(rospy.Time.now())
        left_output = self.left_motor.update(dt)
        if left_output is not None:
            self.command_left_motor(left_output)

    def right_encoder_callback(self, enc_msg):
        self.right_motor.enc_tick = enc_msg.data

        dt = self.right_motor.get_dt(rospy.Time.now())
        right_output = self.right_motor.update(dt)
        if right_output is not None:
            self.command_right_motor(right_output)

    def ultrasonic_callback(self, ultrasonic_msg):
        for index, distance in enumerate(ultrasonic_msg.data):
            self.trackers_indexed[index].update(distance)

    def scale_targets(self, lin_vel, ang_vel):
        if lin_vel > 0:
            lin_vel = self.trackers_directed[Direction.FRONT].scale_v(lin_vel)
        elif lin_vel < 0:
            lin_vel = self.trackers_directed[Direction.BACK].scale_v(lin_vel)
        elif ang_vel < 0:  # if moving right, check the left side
            ang_vel = self.trackers_directed[Direction.LEFT].scale_v(ang_vel)
        elif ang_vel > 0:  # if moving left, check the right side
            ang_vel = self.trackers_directed[Direction.RIGHT].scale_v(ang_vel)

        return lin_vel, ang_vel

    def run(self):
        clock_rate = rospy.Rate(30)

        prev_ultrasonic_report_t = rospy.Time.now()
        while not rospy.is_shutdown():
            self.compute_odometry(self.left_motor.get_delta_dist(),
                                  self.right_motor.get_delta_dist(),
                                  self.left_motor.get_speed(),
                                  self.right_motor.get_speed())

            self.publish_chassis_data()

            linear_speed_mps, rotational_speed_mps = self.scale_targets(
                self.linear_speed_mps, self.rotational_speed_mps)
            if linear_speed_mps != self.linear_speed_mps or rotational_speed_mps != self.rotational_speed_mps:
                current_time = rospy.Time.now()
                if current_time - prev_ultrasonic_report_t > rospy.Duration(
                        0.5):
                    prev_ultrasonic_report_t = current_time

                    print "Scaling speed based on distance sensors"
                    for direction in self.trackers_directed:
                        print "\t%s, dist: %s, scale: %s" % (
                            Direction.name(direction),
                            self.trackers_directed[direction].get_dists(),
                            self.trackers_directed[direction].get_scale())

            self.left_motor.set_target(linear_speed_mps - rotational_speed_mps)
            self.right_motor.set_target(linear_speed_mps +
                                        rotational_speed_mps)

            clock_rate.sleep()

    def command_left_motor(self, command):
        if self.enable_pid and self.prev_left_output != command:
            self.left_command_pub.publish(command)
            self.prev_left_output = command

    def command_right_motor(self, command):
        if self.enable_pid and self.prev_right_output != command:
            self.right_command_pub.publish(command)
            self.prev_right_output = command

    def publish_chassis_data(self):
        self.left_dist_pub.publish(self.left_motor.get_dist())
        self.right_dist_pub.publish(self.right_motor.get_dist())
        self.left_speed_pub.publish(self.left_motor.get_speed())
        self.right_speed_pub.publish(self.right_motor.get_speed())

        odom_quaternion = tf.transformations.quaternion_from_euler(
            0.0, 0.0, self.odom_t)
        self.tf_broadcaster.sendTransform((self.odom_x, self.odom_y, 0.0),
                                          odom_quaternion, rospy.Time.now(),
                                          self.child_frame, self.parent_frame)

        self.odom_msg.header.stamp = rospy.Time.now()
        self.odom_msg.pose.pose.position.x = self.odom_x
        self.odom_msg.pose.pose.position.y = self.odom_y
        self.odom_msg.pose.pose.position.z = 0.0

        self.odom_msg.pose.pose.orientation.x = odom_quaternion[0]
        self.odom_msg.pose.pose.orientation.y = odom_quaternion[1]
        self.odom_msg.pose.pose.orientation.z = odom_quaternion[2]
        self.odom_msg.pose.pose.orientation.w = odom_quaternion[3]

        self.odom_msg.twist.twist.linear.x = self.odom_vx
        self.odom_msg.twist.twist.linear.y = self.odom_vy
        self.odom_msg.twist.twist.linear.z = 0.0

        self.odom_msg.twist.twist.angular.x = 0.0
        self.odom_msg.twist.twist.angular.y = 0.0
        self.odom_msg.twist.twist.angular.z = self.odom_vt

        self.odom_pub.publish(self.odom_msg)

    def compute_odometry(self, delta_left, delta_right, left_speed,
                         right_speed):
        delta_dist = (delta_right + delta_left) / 2

        # angle = arc / radius
        delta_angle = (delta_right - delta_left) / self.wheel_distance
        self.odom_t += delta_angle

        dx = delta_dist * math.cos(self.odom_t)
        dy = delta_dist * math.sin(self.odom_t)

        self.odom_x += dx
        self.odom_y += dy

        speed = (right_speed + left_speed) / 2
        self.odom_vx = speed * math.cos(self.odom_t)
        self.odom_vy = speed * math.sin(self.odom_t)
        self.odom_vt = (right_speed - left_speed) / (self.wheel_distance / 2)