// Copyright 2019 The MediaPipe Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef MEDIAPIPE_UTIL_TRACKING_BOX_TRACKER_H_
#define MEDIAPIPE_UTIL_TRACKING_BOX_TRACKER_H_

#include <inttypes.h>

#include <map>
#include <unordered_map>
#include <vector>

#include "absl/strings/str_format.h"
#include "absl/synchronization/mutex.h"
#include "mediapipe/framework/port/threadpool.h"
#include "mediapipe/util/tracking/box_tracker.pb.h"
#include "mediapipe/util/tracking/flow_packager.pb.h"
#include "mediapipe/util/tracking/tracking.h"
#include "mediapipe/util/tracking/tracking.pb.h"

namespace mediapipe {

// Describes a rectangle box at an instance of time.
struct TimedBox {
  static const int kNumQuadVertices = 4;
  // Dimensions are in normalized coordinates [0, 1].
  float top = 0;
  float left = 0;
  float bottom = 0;
  float right = 0;
  // Rotation of box w.r.t. center in radians.
  float rotation = 0;
  int64 time_msec = 0;
  // Confidence of the tracked box in range [0, 1].
  float confidence = 0;
  std::vector<Vector2_f> quad_vertices;
  // Aspect ratio (width / height) for the tracked rectangle in physical space.
  float aspect_ratio = -1.0;
  // Whether we want this box to be potentially grouped with other boxes
  // to track together. This is useful for tracking small boxes that lie
  // on a plane. For example, when we detect a plane,
  // track the plane, then all boxes within the plane can share the same
  // homography transform.
  bool request_grouping = false;

  bool operator<(const TimedBox& rhs) const {
    return time_msec < rhs.time_msec;
  }

  // Returns (1.0 - alpha) * lhs + alpha * rhs;
  static TimedBox Blend(const TimedBox& lhs, const TimedBox& rhs, double alpha);

  // Returns lhs * alpha + rhs * beta;
  static TimedBox Blend(const TimedBox& lhs, const TimedBox& rhs, double alpha,
                        double beta);

  std::string ToString() const {
    return absl::StrFormat(
        "top: %.3f left: %.3f bottom: %.3f right: %.3f "
        "rot: %.3f t: %d",
        top, left, bottom, right, rotation, static_cast<int64_t>(time_msec));
  }

  // Returns corners of TimedBox in the requested domain.
  std::array<Vector2_f, 4> Corners(float width, float height) const;

  static TimedBox FromProto(const TimedBoxProto& proto);
  TimedBoxProto ToProto() const;
};

// TimedBox augment with internal states.
struct InternalTimedBox : public TimedBox {
  InternalTimedBox() = default;
  // Convenience constructor.
  InternalTimedBox(const TimedBox& box, const MotionBoxState* state_)
      : TimedBox(box), state(state_) {}

  // Corresponding MotionBoxState a TimedBox.
  std::shared_ptr<const MotionBoxState> state;
};

// Initializes beginning tracking state from a TimedBox.
void MotionBoxStateFromTimedBox(const TimedBox& box, MotionBoxState* state);

// Retrieves box position and time from a tracking state.
void TimedBoxFromMotionBoxState(const MotionBoxState& state, TimedBox* box);

// Downgrade tracking degrees of TrackStepOptions to
// TRACKING_DEGREE_OBJECT_ROTATION_SCALE if originally
// specified TRACKING_DEGREE_OBJECT_PERSPECTIVE, but start pos
// does not contain quad or contains invalid quad.
template <typename T>
void ChangeTrackingDegreesBasedOnStartPos(
    const T& start_pos, TrackStepOptions* track_step_options) {
  if (track_step_options->tracking_degrees() ==
          TrackStepOptions::TRACKING_DEGREE_OBJECT_PERSPECTIVE &&
      (!start_pos.has_quad() || start_pos.quad().vertices_size() != 8)) {
    track_step_options->set_tracking_degrees(
        TrackStepOptions::TRACKING_DEGREE_OBJECT_ROTATION_SCALE);
    VLOG(1) << "Originally specified TRACKING_DEGREE_OBJECT_PERSPECTIVE, but "
               "changed to TRACKING_DEGREE_OBJECT_ROTATION_SCALE";
  }
}

// Set of boxes for a particular checkpoint.
typedef std::deque<InternalTimedBox> PathSegment;

// Stores the PathSegment for each checkpoint time.
typedef std::map<int, PathSegment> Path;

// Returns box at specified time for a specific path segment.
// Returns true on success, otherwise box is left untouched.
// Optionally returns closest known MotionBoxState if state is not null.
bool TimedBoxAtTime(const PathSegment& segment, int64 time_msec, TimedBox* box,
                    MotionBoxState* state = nullptr);

// Tracks timed boxes from cached TrackingDataChunks created by
// FlowPackagerCalculator. For usage see accompanying test.
class BoxTracker {
 public:
  // Initializes a new BoxTracker to work on cached TrackingData from a chunk
  // directory.
  BoxTracker(const std::string& cache_dir, const BoxTrackerOptions& options);

  // Initializes a new BoxTracker to work on the passed TrackingDataChunks.
  // If copy_data is true, BoxTracker will retain its own copy of the data;
  // otherwise the passed pointer need to be valid for the lifetime of the
  // BoxTracker.
  BoxTracker(const std::vector<const TrackingDataChunk*>& tracking_data,
             bool copy_data, const BoxTrackerOptions& options);

  // Add single TrackingDataChunk. This chunk must be correctly aligned with
  // existing chunks. If chunk starting timestamp is larger than next valid
  // chunk timestamp, empty chunks will be added to fill the gap. If copy_data
  // is true, BoxTracker will retain its own copy of the data; otherwise the
  // passed pointers need to be valid for the lifetime of the BoxTracker.
  void AddTrackingDataChunk(const TrackingDataChunk* chunk, bool copy_data);
  // Add new TrackingDataChunks. This method allows to streamline BoxTracker
  // usage. Instead of collecting all tracking chunks and then calling the
  // constructor, it makes possible to pass only minimally required chunks to
  // constructor, and then append new chunks when available.
  // Caller is responsible to guarantee that all chunks that are necessary for
  // tracking are at the disposal of BoxTracker before calling NewBoxTrack, i.e.
  // min_msec and max_msec parameters must be in the range of chunks passed to
  // BoxTracker.
  // Chunks will be added in the order they are specified in tracking_data. Each
  // chunk must be correctly aligned with existing chunks. If chunk starting
  // timestamp is larger than next valid chunk timestamp, empty chunks will be
  // added to fill the gap. If copy_data is true, BoxTracker will retain its own
  // copy of the data; otherwise the passed pointers need to be valid for the
  // lifetime of the BoxTracker.
  void AddTrackingDataChunks(
      const std::vector<const TrackingDataChunk*>& tracking_data,
      bool copy_data);

  // Starts a new track for the specified box until tracking terminates or
  // until track length achieves max_length.
  // Does not block caller, returns immediately.
  // Note: Use positive integers for id, we reserve negative ones for debugging
  // and visualization purposes.
  void NewBoxTrack(const TimedBox& initial_pos, int id, int64 min_msec = 0,
                   int64 max_msec = std::numeric_limits<int64>::max());

  // Returns interval for which the state of the specified box is known.
  // (Returns -1, -1 if id is missing or no tracking has been done).
  std::pair<int64, int64> TrackInterval(int id);

  // Returns box position for requested box at specified time.
  // Returns false if no such box exists.
  // Optional, if record_path_states is enabled in options outputs the
  // corresponding states for the resulting TimedBox. Note, as TimedBoxes are
  // generally interpolated from the underlying data and blended across
  // checkpoints, the returned states are the closest (snapped) known tracking
  // states for the left and right checkpoint path. Therefore the size of states
  // is either two or one (if only one checkpoint exists).
  bool GetTimedPosition(int id, int64 time_msec, TimedBox* result,
                        std::vector<MotionBoxState>* states = nullptr);

  // Returns chunk index for specified time.
  int ChunkIdxFromTime(int64 msec) const {
    return msec / options_.caching_chunk_size_msec();
  }

  // Returns true if any tracking is ongoing for the specified id.
  bool IsTrackingOngoingForId(int id) ABSL_LOCKS_EXCLUDED(status_mutex_);

  // Returns true if any tracking is ongoing.
  bool IsTrackingOngoing() ABSL_LOCKS_EXCLUDED(status_mutex_);

  // Cancels all ongoing tracks. To avoid race conditions all NewBoxTrack's in
  // flight will also be canceled. Future NewBoxTrack's will be canceled.
  // NOTE: To resume execution, you have to call ResumeTracking() before
  //       issuing more NewBoxTrack calls.
  void CancelAllOngoingTracks() ABSL_LOCKS_EXCLUDED(status_mutex_);
  void ResumeTracking() ABSL_LOCKS_EXCLUDED(status_mutex_);

  // Waits for all ongoing tracks to complete.
  // Optionally accepts a timeout in microseconds (== 0 for infinite wait).
  // Returns true on success, false if timeout is reached.
  // NOTE: If WaitForAllOngoingTracks timed out, CancelAllOngoingTracks() must
  // be called before destructing the BoxTracker object or dangeling running
  // threads might try to access invalid data.
  bool WaitForAllOngoingTracks(int timeout_us = 0)
      ABSL_LOCKS_EXCLUDED(status_mutex_);

  // Debug function to obtain raw TrackingData closest to the specified
  // timestamp. This call will read from disk on every invocation so it is
  // expensive.
  // To not interfere with other tracking requests it is recommended that you
  // use a unique id here.
  // Returns true on success.
  bool GetTrackingData(int id, int64 request_time_msec,
                       TrackingData* tracking_data,
                       int* tracking_data_msec = nullptr);

 private:
  // Asynchronous implementation function for box tracking. Schedules forward
  // and backward tracking.
  void NewBoxTrackAsync(const TimedBox& initial_pos, int id, int64 min_msec,
                        int64 max_msec);

  typedef std::pair<const TrackingDataChunk*, bool> AugmentedChunkPtr;
  // Attempts to read chunk at chunk_idx if it exists. Reads from cache
  // directory or from in memory cache.
  // Important: 2nd part of return value indicates if returned tracking data
  // will be owned by the caller (if true). In that case caller is responsible
  // for releasing the returned chunk.
  AugmentedChunkPtr ReadChunk(int id, int checkpoint, int chunk_idx);

  // Attempts to read specified chunk from caching directory. Blocks and waits
  // until chunk is available or internal time out is reached.
  // Returns nullptr if data could not be read.
  std::unique_ptr<TrackingDataChunk> ReadChunkFromCache(int id, int checkpoint,
                                                        int chunk_idx);

  // Waits with timeout for chunkfile to become available. Returns true on
  // success, false if waited till timeout or when canceled.
  bool WaitForChunkFile(int id, int checkpoint, const std::string& chunk_file)
      ABSL_LOCKS_EXCLUDED(status_mutex_);

  // Determines closest index in passed TrackingDataChunk
  int ClosestFrameIndex(int64 msec, const TrackingDataChunk& chunk) const;

  // Adds new TimedBox to specified checkpoint with state.
  void AddBoxResult(const TimedBox& box, int id, int checkpoint,
                    const MotionBoxState& state);

  // Callback can only handle 5 args max.
  // Set own_data to true for args to assume ownership.
  struct TrackingImplArgs {
    TrackingImplArgs(AugmentedChunkPtr chunk_ptr,
                     const MotionBoxState& start_state_, int start_frame_,
                     int chunk_idx_, int id_, int checkpoint_, bool forward_,
                     bool first_call_, int64 min_msec_, int64 max_msec_)
        : start_state(start_state_),
          start_frame(start_frame_),
          chunk_idx(chunk_idx_),
          id(id_),
          checkpoint(checkpoint_),
          forward(forward_),
          first_call(first_call_),
          min_msec(min_msec_),
          max_msec(max_msec_) {
      if (chunk_ptr.second) {
        chunk_data_buffer.reset(chunk_ptr.first);
      }

      chunk_data = chunk_ptr.first;
    }

    TrackingImplArgs(const TrackingImplArgs&) = default;

    // Storage for tracking data.
    std::shared_ptr<const TrackingDataChunk> chunk_data_buffer;

    // Pointer to the actual tracking data. Usually points to the buffer
    // but can also point to external data for performance reasons.
    const TrackingDataChunk* chunk_data;

    MotionBoxState start_state;
    int start_frame;
    int chunk_idx;
    int id;
    int checkpoint;
    bool forward = true;
    bool first_call = true;
    int64 min_msec;  // minimum timestamp to track to
    int64 max_msec;  // maximum timestamp to track to
  };

  // Actual tracking algorithm.
  void TrackingImpl(const TrackingImplArgs& args);

  // Ids are scheduled exclusively, run this method to acquire lock.
  // Returns false if id could not be scheduled (e.g. id got canceled during
  // waiting).
  bool WaitToScheduleId(int id) ABSL_LOCKS_EXCLUDED(status_mutex_);

  // Signals end of scheduling phase. Requires status mutex to be held.
  void DoneSchedulingId(int id) ABSL_EXCLUSIVE_LOCKS_REQUIRED(status_mutex_);

  // Removes all checkpoints within vicinity of new checkpoint.
  void RemoveCloseCheckpoints(int id, int checkpoint)
      ABSL_EXCLUSIVE_LOCKS_REQUIRED(status_mutex_);

  // Removes specific checkpoint.
  void ClearCheckpoint(int id, int checkpoint)
      ABSL_EXCLUSIVE_LOCKS_REQUIRED(status_mutex_);

  // Terminates tracking for specific id and checkpoint.
  void CancelTracking(int id, int checkpoint)
      ABSL_EXCLUSIVE_LOCKS_REQUIRED(status_mutex_);

  // Implementation function for IsTrackingOngoing assuming mutex is already
  // held.
  bool IsTrackingOngoingMutexHeld()
      ABSL_EXCLUSIVE_LOCKS_REQUIRED(status_mutex_);

  // Captures tracking status for each checkpoint
  struct TrackStatus {
    // Indicates that all current tracking processes should be canceled.
    bool canceled = false;

    // Number of tracking requests than are currently ongoing.
    int tracks_ongoing = 0;
  };

 private:
  // Stores computed tracking paths_ for all boxes.
  std::unordered_map<int, Path> paths_ ABSL_GUARDED_BY(path_mutex_);
  absl::Mutex path_mutex_;

  // For each id and each checkpoint stores current tracking status.
  std::unordered_map<int, std::map<int, TrackStatus>> track_status_
      ABSL_GUARDED_BY(status_mutex_);

  // Keeps track which ids are currently processing in NewBoxTrack.
  std::unordered_map<int, bool> new_box_track_ ABSL_GUARDED_BY(status_mutex_);
  absl::Mutex status_mutex_;

  bool canceling_ ABSL_GUARDED_BY(status_mutex_) = false;

  // Use to signal changes to status_condvar_;
  absl::CondVar status_condvar_ ABSL_GUARDED_BY(status_mutex_);

  BoxTrackerOptions options_;

  // Caching directory for TrackingData stored on disk.
  std::string cache_dir_;

  // Pointers to tracking data stored in memory.
  std::vector<const TrackingDataChunk*> tracking_data_;
  // Buffer for tracking data in case we retain a deep copy.
  std::vector<std::unique_ptr<TrackingDataChunk>> tracking_data_buffer_;

  // Workers that run the tracking algorithm.
  std::unique_ptr<ThreadPool> tracking_workers_;
};

}  // namespace mediapipe

#endif  // MEDIAPIPE_UTIL_TRACKING_BOX_TRACKER_H_
