#include <scwx/qt/manager/timeline_manager.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/util/logger.hpp>
#include <scwx/util/map.hpp>
#include <scwx/util/time.hpp>

#include <condition_variable>
#include <mutex>

#include <boost/asio/post.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/thread_pool.hpp>
#include <fmt/chrono.h>

namespace scwx
{
namespace qt
{
namespace manager
{

static const std::string logPrefix_ = "scwx::qt::manager::timeline_manager";
static const auto        logger_    = scwx::util::Logger::Create(logPrefix_);

enum class Direction
{
   Back,
   Next
};

// Wait up to 5 seconds for radar sweeps to update
static constexpr std::chrono::seconds kRadarSweepMonitorTimeout_ {5};

class TimelineManager::Impl
{
public:
   explicit Impl(TimelineManager* self) : self_ {self}
   {
      auto& generalSettings = settings::GeneralSettings::Instance();

      loopDelay_ =
         std::chrono::milliseconds(generalSettings.loop_delay().GetValue());
      loopSpeed_ = generalSettings.loop_speed().GetValue();
      loopTime_  = std::chrono::minutes(generalSettings.loop_time().GetValue());
   }

   ~Impl()
   {
      // Lock mutexes before destroying
      std::unique_lock animationTimerLock {animationTimerMutex_};
      animationTimer_.cancel();

      std::unique_lock selectTimeLock {selectTimeMutex_};
   }

   TimelineManager* self_;

   std::pair<std::chrono::system_clock::time_point,
             std::chrono::system_clock::time_point>
        GetLoopStartAndEndTimes();
   void UpdateCacheLimit(
      std::shared_ptr<manager::RadarProductManager> radarProductManager,
      const std::set<std::chrono::system_clock::time_point>& volumeTimes);

   void RadarSweepMonitorDisable();
   void RadarSweepMonitorReset();
   void RadarSweepMonitorWait(std::unique_lock<std::mutex>& lock);

   void Pause();
   void Play();
   void PlaySync();
   void
   SelectTimeAsync(std::chrono::system_clock::time_point selectedTime = {});
   std::pair<bool, bool>
        SelectTime(std::chrono::system_clock::time_point selectedTime = {});
   void StepAsync(Direction direction);
   void Step(Direction direction);

   boost::asio::thread_pool playThreadPool_ {1};
   boost::asio::thread_pool selectThreadPool_ {1};

   std::size_t                           mapCount_ {0};
   std::string                           radarSite_ {"?"};
   std::string                           previousRadarSite_ {"?"};
   std::chrono::system_clock::time_point pinnedTime_ {};
   std::chrono::system_clock::time_point adjustedTime_ {};
   std::chrono::system_clock::time_point selectedTime_ {};
   types::MapTime                        viewType_ {types::MapTime::Live};
   std::chrono::minutes                  loopTime_;
   double                                loopSpeed_;
   std::chrono::milliseconds             loopDelay_;

   bool                    radarSweepMonitorActive_ {false};
   std::mutex              radarSweepMonitorMutex_ {};
   std::condition_variable radarSweepMonitorCondition_ {};
   std::set<std::size_t>   radarSweepsUpdated_ {};
   std::set<std::size_t>   radarSweepsComplete_ {};

   types::AnimationState     animationState_ {types::AnimationState::Pause};
   boost::asio::steady_timer animationTimer_ {playThreadPool_};
   std::mutex                animationTimerMutex_ {};

   std::mutex selectTimeMutex_ {};
};

TimelineManager::TimelineManager() : p(std::make_unique<Impl>(this)) {}
TimelineManager::~TimelineManager() = default;

void TimelineManager::SetMapCount(std::size_t mapCount)
{
   p->mapCount_ = mapCount;
}

void TimelineManager::SetRadarSite(const std::string& radarSite)
{
   if (p->radarSite_ == radarSite)
   {
      // No action needed
      return;
   }

   logger_->debug("SetRadarSite: {}", radarSite);

   p->radarSite_ = radarSite;

   if (p->viewType_ == types::MapTime::Live)
   {
      // If the selected view type is live, select the current products
      p->SelectTime();
   }
   else
   {
      // If the selected view type is archive, select using the selected time
      p->SelectTimeAsync(p->selectedTime_);
   }
}

void TimelineManager::SetDateTime(
   std::chrono::system_clock::time_point dateTime)
{
   logger_->debug("SetDateTime: {}", scwx::util::TimeString(dateTime));

   p->pinnedTime_ = dateTime;

   if (p->viewType_ == types::MapTime::Archive)
   {
      // Only select if the view type is archive
      p->SelectTimeAsync(dateTime);
   }

   // Ignore a date/time selection if the view type is live
}

void TimelineManager::SetViewType(types::MapTime viewType)
{
   logger_->debug("SetViewType: {}", types::GetMapTimeName(viewType));

   p->viewType_ = viewType;

   if (p->viewType_ == types::MapTime::Live)
   {
      // If the selected view type is live, select the current products
      p->SelectTime();
   }
   else
   {
      // If the selected view type is archive, select using the pinned time
      p->SelectTimeAsync(p->pinnedTime_);
   }

   Q_EMIT ViewTypeUpdated(viewType);
}

void TimelineManager::SetLoopTime(std::chrono::minutes loopTime)
{
   logger_->debug("SetLoopTime: {}", loopTime);

   p->loopTime_ = loopTime;
}

void TimelineManager::SetLoopSpeed(double loopSpeed)
{
   logger_->debug("SetLoopSpeed: {}", loopSpeed);

   if (loopSpeed < 1.0)
   {
      loopSpeed = 1.0;
   }

   p->loopSpeed_ = loopSpeed;
}

void TimelineManager::SetLoopDelay(std::chrono::milliseconds loopDelay)
{
   logger_->debug("SetLoopDelay: {}", loopDelay);

   p->loopDelay_ = loopDelay;
}

void TimelineManager::AnimationStepBegin()
{
   logger_->debug("AnimationStepBegin");

   p->Pause();

   if (p->viewType_ == types::MapTime::Live ||
       p->pinnedTime_ == std::chrono::system_clock::time_point {})
   {
      // If the selected view type is live, select the current products
      p->SelectTimeAsync(std::chrono::system_clock::now() - p->loopTime_);
   }
   else
   {
      // If the selected view type is archive, select using the pinned time
      p->SelectTimeAsync(p->pinnedTime_ - p->loopTime_);
   }
}

void TimelineManager::AnimationStepBack()
{
   logger_->debug("AnimationStepBack");

   p->Pause();
   p->StepAsync(Direction::Back);
}

void TimelineManager::AnimationPlayPause()
{
   if (p->animationState_ == types::AnimationState::Pause)
   {
      logger_->debug("AnimationPlay");
      p->Play();
   }
   else
   {
      logger_->debug("AnimationPause");
      p->Pause();
   }
}

void TimelineManager::AnimationStepNext()
{
   logger_->debug("AnimationStepNext");

   p->Pause();
   p->StepAsync(Direction::Next);
}

void TimelineManager::AnimationStepEnd()
{
   logger_->debug("AnimationStepEnd");

   p->Pause();

   if (p->viewType_ == types::MapTime::Live)
   {
      // If the selected view type is live, select the current products
      p->SelectTime();
   }
   else
   {
      // If the selected view type is archive, select using the pinned time
      p->SelectTimeAsync(p->pinnedTime_);
   }
}

void TimelineManager::Impl::RadarSweepMonitorDisable()
{
   radarSweepMonitorActive_ = false;
}

void TimelineManager::Impl::RadarSweepMonitorReset()
{
   radarSweepsUpdated_.clear();
   radarSweepsComplete_.clear();

   radarSweepMonitorActive_ = true;
}

void TimelineManager::Impl::RadarSweepMonitorWait(
   std::unique_lock<std::mutex>& lock)
{
   std::cv_status status =
      radarSweepMonitorCondition_.wait_for(lock, kRadarSweepMonitorTimeout_);
   if (status == std::cv_status::timeout)
   {
      logger_->debug("Radar sweep monitor timed out");
   }
   radarSweepMonitorActive_ = false;
}

void TimelineManager::ReceiveRadarSweepUpdated(std::size_t mapIndex)
{
   if (!p->radarSweepMonitorActive_)
   {
      return;
   }

   std::unique_lock lock {p->radarSweepMonitorMutex_};

   // Radar sweep is updated, but still needs painted
   p->radarSweepsUpdated_.insert(mapIndex);
}

void TimelineManager::ReceiveRadarSweepNotUpdated(std::size_t mapIndex,
                                                  types::NoUpdateReason reason)
{
   if (!p->radarSweepMonitorActive_ ||
       reason == types::NoUpdateReason::NotLoaded)
   {
      return;
   }

   std::unique_lock lock {p->radarSweepMonitorMutex_};

   // Radar sweep is complete, no painting will occur
   p->radarSweepsComplete_.insert(mapIndex);

   // If all sweeps have completed rendering
   if (p->radarSweepsComplete_.size() == p->mapCount_)
   {
      // Notify monitors
      p->radarSweepMonitorActive_ = false;
      p->radarSweepMonitorCondition_.notify_all();
   }
}

void TimelineManager::ReceiveMapWidgetPainted(std::size_t mapIndex)
{
   if (!p->radarSweepMonitorActive_)
   {
      return;
   }

   std::unique_lock lock {p->radarSweepMonitorMutex_};

   // If the radar sweep has been updated
   if (p->radarSweepsUpdated_.contains(mapIndex) &&
       !p->radarSweepsComplete_.contains(mapIndex))
   {
      // Mark the radar sweep complete
      p->radarSweepsComplete_.insert(mapIndex);

      // If all sweeps have completed rendering
      if (p->radarSweepsComplete_.size() == p->mapCount_)
      {
         // Notify monitors
         p->radarSweepMonitorActive_ = false;
         p->radarSweepMonitorCondition_.notify_all();
      }
   }
}

void TimelineManager::Impl::Pause()
{
   // Cancel animation
   std::unique_lock animationTimerLock {animationTimerMutex_};
   animationTimer_.cancel();

   if (animationState_ != types::AnimationState::Pause)
   {
      animationState_ = types::AnimationState::Pause;
      Q_EMIT self_->AnimationStateUpdated(animationState_);
   }
}

std::pair<std::chrono::system_clock::time_point,
          std::chrono::system_clock::time_point>
TimelineManager::Impl::GetLoopStartAndEndTimes()
{
   // Determine loop end time
   std::chrono::system_clock::time_point endTime;

   if (viewType_ == types::MapTime::Live ||
       pinnedTime_ == std::chrono::system_clock::time_point {})
   {
      endTime = std::chrono::floor<std::chrono::minutes>(
         std::chrono::system_clock::now());
   }
   else
   {
      endTime = pinnedTime_;
   }

   // Determine loop start time and current position in the loop
   std::chrono::system_clock::time_point startTime = endTime - loopTime_;

   return {startTime, endTime};
}

void TimelineManager::Impl::UpdateCacheLimit(
   std::shared_ptr<manager::RadarProductManager>          radarProductManager,
   const std::set<std::chrono::system_clock::time_point>& volumeTimes)
{
   // Calculate the number of volume scans in the loop
   auto [startTime, endTime] = GetLoopStartAndEndTimes();
   auto startIter = util::GetBoundedElementIterator(volumeTimes, startTime);
   auto endIter   = util::GetBoundedElementIterator(volumeTimes, endTime);
   std::size_t numVolumeScans = std::distance(startIter, endIter) + 1;

   // Dynamically update maximum cached volume scans to the lesser of
   // either 1.5x the loop length or 5 greater than the loop length
   radarProductManager->SetCacheLimit(std::min(
      static_cast<std::size_t>(numVolumeScans * 1.5), numVolumeScans + 5u));
}

void TimelineManager::Impl::Play()
{
   if (animationState_ != types::AnimationState::Play)
   {
      animationState_ = types::AnimationState::Play;
      Q_EMIT self_->AnimationStateUpdated(animationState_);
   }

   {
      std::unique_lock animationTimerLock {animationTimerMutex_};
      animationTimer_.cancel();
   }

   boost::asio::post(playThreadPool_,
                     [this]()
                     {
                        try
                        {
                           PlaySync();
                        }
                        catch (const std::exception& ex)
                        {
                           logger_->error(ex.what());
                        }
                     });
}

void TimelineManager::Impl::PlaySync()
{
   using namespace std::chrono_literals;

   // Take a lock for time selection
   std::unique_lock lock {selectTimeMutex_};

   auto [startTime, endTime] = GetLoopStartAndEndTimes();
   std::chrono::system_clock::time_point currentTime = selectedTime_;
   std::chrono::system_clock::time_point newTime;

   if (currentTime < startTime || currentTime >= endTime)
   {
      // If the currently selected time is out of the loop, select the
      // start time
      newTime = startTime;
   }
   else
   {
      // If the currently selected time is in the loop, increment
      newTime = currentTime + 1min;
   }

   // Unlock prior to selecting time
   lock.unlock();

   // Lock radar sweep monitor
   std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_};

   // Reset radar sweep monitor in preparation for update
   RadarSweepMonitorReset();

   // Select the time
   auto selectTimeStart = std::chrono::steady_clock::now();
   SelectTime(newTime);
   auto selectTimeEnd = std::chrono::steady_clock::now();
   auto elapsedTime   = selectTimeEnd - selectTimeStart;

   // Wait for radar sweeps to update
   RadarSweepMonitorWait(radarSweepMonitorLock);

   // Calculate the interval until the next update, prior to selecting
   std::chrono::milliseconds interval;
   if (newTime != endTime)
   {
      // Determine repeat interval (speed of 1.0 is 1 minute per second)
      interval = std::chrono::duration_cast<std::chrono::milliseconds>(
         std::chrono::milliseconds(std::lroundl(1000.0 / loopSpeed_)) -
         elapsedTime);
   }
   else
   {
      // Pause at the end of the loop
      interval = std::chrono::duration_cast<std::chrono::milliseconds>(
         loopDelay_ - elapsedTime);
   }

   std::unique_lock animationTimerLock {animationTimerMutex_};
   animationTimer_.expires_after(interval);
   animationTimer_.async_wait(
      [this](const boost::system::error_code& e)
      {
         if (e == boost::system::errc::success)
         {
            if (animationState_ == types::AnimationState::Play)
            {
               Play();
            }
         }
         else if (e == boost::asio::error::operation_aborted)
         {
            logger_->debug("Play timer cancelled");
         }
         else
         {
            logger_->warn("Play timer error: {}", e.message());
         }
      });
}

void TimelineManager::Impl::SelectTimeAsync(
   std::chrono::system_clock::time_point selectedTime)
{
   boost::asio::post(selectThreadPool_,
                     [=, this]()
                     {
                        try
                        {
                           SelectTime(selectedTime);
                        }
                        catch (const std::exception& ex)
                        {
                           logger_->error(ex.what());
                        }
                     });
}

std::pair<bool, bool> TimelineManager::Impl::SelectTime(
   std::chrono::system_clock::time_point selectedTime)
{
   bool volumeTimeUpdated   = false;
   bool selectedTimeUpdated = false;

   if (selectedTime_ == selectedTime && radarSite_ == previousRadarSite_)
   {
      // Nothing to do
      return {volumeTimeUpdated, selectedTimeUpdated};
   }
   else if (selectedTime == std::chrono::system_clock::time_point {})
   {
      // If a default time point is given, reset to a live view
      selectedTime_      = selectedTime;
      adjustedTime_      = selectedTime;
      previousRadarSite_ = radarSite_;

      logger_->debug("Time updated: Live");

      Q_EMIT self_->LiveStateUpdated(true);
      Q_EMIT self_->VolumeTimeUpdated(selectedTime);
      Q_EMIT self_->SelectedTimeUpdated(selectedTime);

      volumeTimeUpdated   = true;
      selectedTimeUpdated = true;

      return {volumeTimeUpdated, selectedTimeUpdated};
   }

   // Take a lock for time selection
   std::unique_lock lock {selectTimeMutex_};

   // Request active volume times
   auto radarProductManager =
      manager::RadarProductManager::Instance(radarSite_);
   auto volumeTimes = radarProductManager->GetActiveVolumeTimes(selectedTime);

   // Dynamically update maximum cached volume scans
   UpdateCacheLimit(radarProductManager, volumeTimes);

   // Find the best match bounded time
   auto elementPtr = util::GetBoundedElementPointer(volumeTimes, selectedTime);

   // The timeline is no longer live
   Q_EMIT self_->LiveStateUpdated(false);

   if (elementPtr != nullptr)
   {
      // If the adjusted time changed, or if a new radar site has been selected
      if (adjustedTime_ != *elementPtr || radarSite_ != previousRadarSite_)
      {
         // If the time was found, select it
         adjustedTime_ = *elementPtr;

         logger_->debug("Volume time updated: {}",
                        scwx::util::TimeString(adjustedTime_));

         volumeTimeUpdated = true;
         Q_EMIT self_->VolumeTimeUpdated(adjustedTime_);
      }
   }
   else
   {
      // No volume time was found
      logger_->info("No volume scan found for {}",
                    scwx::util::TimeString(selectedTime));
   }

   logger_->trace("Selected time updated: {}",
                  scwx::util::TimeString(selectedTime));

   selectedTime_       = selectedTime;
   selectedTimeUpdated = true;
   Q_EMIT self_->SelectedTimeUpdated(selectedTime);

   previousRadarSite_ = radarSite_;

   return {volumeTimeUpdated, selectedTimeUpdated};
}

void TimelineManager::Impl::StepAsync(Direction direction)
{
   boost::asio::post(selectThreadPool_,
                     [=, this]()
                     {
                        try
                        {
                           Step(direction);
                        }
                        catch (const std::exception& ex)
                        {
                           logger_->error(ex.what());
                        }
                     });
}

void TimelineManager::Impl::Step(Direction direction)
{
   // Take a lock for time selection
   std::unique_lock lock {selectTimeMutex_};

   std::chrono::system_clock::time_point newTime = selectedTime_;

   if (newTime == std::chrono::system_clock::time_point {})
   {
      if (direction == Direction::Back)
      {
         newTime = std::chrono::floor<std::chrono::minutes>(
            std::chrono::system_clock::now());
      }
      else
      {
         // Cannot step forward any further
         return;
      }
   }

   // Unlock prior to selecting time
   lock.unlock();

   // Lock radar sweep monitor
   std::unique_lock radarSweepMonitorLock {radarSweepMonitorMutex_};

   // Attempt to step forward or backward up to 30 minutes until an update is
   // received on at least one map
   for (std::size_t i = 0; i < 30; ++i)
   {
      using namespace std::chrono_literals;

      // Increment/decrement selected time by one minute
      if (direction == Direction::Back)
      {
         newTime -= 1min;
      }
      else
      {
         newTime += 1min;

         // If the new time is more than 2 minutes in the future, stop stepping
         if (newTime > std::chrono::system_clock::now() + 2min)
         {
            break;
         }
      }

      // Reset radar sweep monitor in preparation for update
      RadarSweepMonitorReset();

      // Select the time
      SelectTime(newTime);

      // Wait for radar sweeps to update
      RadarSweepMonitorWait(radarSweepMonitorLock);

      // Check for updates
      if (!radarSweepsUpdated_.empty())
      {
         break;
      }
   }
}

std::shared_ptr<TimelineManager> TimelineManager::Instance()
{
   static std::weak_ptr<TimelineManager> timelineManagerReference_ {};
   static std::mutex                     instanceMutex_ {};

   std::unique_lock lock(instanceMutex_);

   std::shared_ptr<TimelineManager> timelineManager =
      timelineManagerReference_.lock();

   if (timelineManager == nullptr)
   {
      timelineManager           = std::make_shared<TimelineManager>();
      timelineManagerReference_ = timelineManager;
   }

   return timelineManager;
}

} // namespace manager
} // namespace qt
} // namespace scwx
