/*!********************************************************************
*
 Audacity: A Digital Audio Editor

 WaveClipAdjustBorderHandle.cpp

 Vitaly Sverchinsky

 **********************************************************************/

#include "WaveClipAdjustBorderHandle.h"
#include "ProjectAudioIO.h"
#include "RefreshCode.h"

#include <wx/event.h>

#include "ClipParameters.h"
#include "../../../../TrackArt.h"
#include "../../../../TrackArtist.h"
#include "Snap.h"
#include "../../../../TrackPanelDrawingContext.h"
#include "../../../../../images/Cursors.h"
#include "WaveClip.h"
#include "WaveTrack.h"
#include "WaveChannelView.h"
#include "HitTestResult.h"
#include "TrackPanelMouseEvent.h"
#include "ViewInfo.h"
#include "ProjectHistory.h"
#include "UndoManager.h"
#include "WaveClipUIUtilities.h"

namespace {

   void TrimLeftTo(WaveTrack::Interval& interval, double t)
   {
      interval.TrimLeftTo(t);
   }

   void TrimRightTo(WaveTrack::Interval& interval, double t)
   {
      interval.TrimRightTo(t);
   }

   void StretchLeftTo(WaveTrack::Interval& interval, double t)
   {
      interval.StretchLeftTo(t);
   }

   void StretchRightTo(WaveTrack::Interval& interval, double t)
   {
      interval.StretchRightTo(t);
   }
}

//Different policies implement different adjustment scenarios
 class WaveClipAdjustBorderHandle::AdjustPolicy
 {
 public:
    virtual ~AdjustPolicy();

    virtual bool Init(const TrackPanelMouseEvent& event) = 0;
    virtual UIHandle::Result Drag(const TrackPanelMouseEvent& event, AudacityProject& project) = 0;
    virtual void Finish(AudacityProject& project) = 0;
    virtual void Cancel() = 0;

    virtual void Draw(
        TrackPanelDrawingContext &context,
        const wxRect &rect,
        unsigned iPass);

    virtual wxRect DrawingArea(
        TrackPanelDrawingContext&,
        const wxRect &rect,
        const wxRect &panelRect,
        unsigned iPass);
 };

WaveClipAdjustBorderHandle::AdjustPolicy::~AdjustPolicy() = default;

namespace {
double GetLeftAdjustLimit(const WaveTrack::Interval& interval,
                          const WaveTrack& track,
                          bool adjustingLeftBorder,
                          bool isStretchMode)
{
   if (!adjustingLeftBorder)
      return std::min(
         interval.GetSequenceEndTime(),
         interval.Start() + 1.0 / track.GetRate()
      );

   const auto prevInterval = track.GetNextInterval(interval, PlaybackDirection::backward);
   if(isStretchMode)
      return prevInterval ? prevInterval->End() :
                            std::numeric_limits<double>::lowest();
   if(prevInterval)
      return std::max(interval.GetSequenceStartTime(),
                prevInterval->End());
   return interval.GetSequenceStartTime();
}

double GetRightAdjustLimit(
   const WaveTrack::Interval& interval, const WaveTrack& track, bool adjustingLeftBorder,
   bool isStretchMode)
{
   if (adjustingLeftBorder)
      return std::max(
         interval.GetSequenceStartTime(),
         interval.End() - 1.0 / track.GetRate()
      );

   const auto nextInterval = track.GetNextInterval(interval, PlaybackDirection::forward);
   if (isStretchMode)
      return nextInterval ? nextInterval->Start() :
                            std::numeric_limits<double>::max();

   if(nextInterval)
      return std::min(interval.GetSequenceEndTime(),
                      nextInterval->Start());
   return interval.GetSequenceEndTime();
}
} // namespace

class AdjustClipBorder final : public WaveClipAdjustBorderHandle::AdjustPolicy
{
public:
   using AdjustHandler = std::function<void(WaveTrack::Interval&, double)>;

private:
   std::shared_ptr<WaveTrack> mTrack;
   std::shared_ptr<WaveTrack::Interval> mInterval;
   int mDragStartX{ };
   const bool mAdjustingLeftBorder;
   const bool mIsStretchMode;
   const double mInitialBorderPosition;
   double mBorderPosition;
   const std::pair<double, double> mRange;
   AdjustHandler mAdjustHandler;

   std::unique_ptr<SnapManager> mSnapManager;
   SnapResults mSnap;

   void TrimTo(double t)
   {
      mBorderPosition = std::clamp(t, mRange.first, mRange.second);
      mAdjustHandler(*mInterval, mBorderPosition);
   }

   //Search for a good snap points among all tracks except
   //one to which moving interval belongs to
   static SnapPointArray FindSnapPoints(
      const WaveTrack* currentTrack,
      const std::pair<double, double> range)
   {
      SnapPointArray result;

      auto addSnapPoint = [&](double t, const Track* track)
      {
         if(t > range.second || t < range.first)
            return;

         for(const auto& snapPoint : result)
            if(snapPoint.t == t)
               return;
         result.emplace_back(t, track);
      };

      if(const auto trackList = currentTrack->GetOwner())
      {
         for(const auto track : as_const(*trackList))
         {
            if(track == currentTrack)
            {
               //skip track that interval belongs to
               continue;
            }

            for (const auto &interval : track->Intervals()) {
               addSnapPoint(interval->Start(), track);
               if(interval->Start() != interval->End())
                  addSnapPoint(interval->End(), track);
            }
         }
      }
      return result;
   }

public:
   AdjustClipBorder(AdjustHandler adjustHandler,
                    std::shared_ptr<WaveTrack> track,
                    std::shared_ptr<WaveTrack::Interval> interval,
                    bool adjustLeftBorder,
                    bool isStretchMode,
                    const ZoomInfo& zoomInfo)
      : mTrack { std::move(track) }
      , mInterval { std::move(interval) }
      , mAdjustingLeftBorder { adjustLeftBorder }
      , mIsStretchMode { isStretchMode }
      , mInitialBorderPosition { adjustLeftBorder ? mInterval->Start() :
                                             mInterval->End() }
      , mBorderPosition { mInitialBorderPosition }
      , mRange { GetLeftAdjustLimit( *mInterval, *mTrack, adjustLeftBorder, isStretchMode),
                 GetRightAdjustLimit(*mInterval, *mTrack, adjustLeftBorder, isStretchMode) }
      , mAdjustHandler { std::move(adjustHandler) }
   {
      assert(mRange.first <= mRange.second);
      if(const auto trackList = mTrack->GetOwner())
      {
         mSnapManager = std::make_unique<SnapManager>(
            *trackList->GetOwner(),
            FindSnapPoints(mTrack.get(), mRange),
            zoomInfo);
      }
   }

   bool Init(const TrackPanelMouseEvent& event) override
   {
      if (event.event.LeftDown())
      {
         mDragStartX = event.event.GetX();
         return true;
      }
      return false;
   }

   UIHandle::Result Drag(const TrackPanelMouseEvent& event, AudacityProject& project) override
   {
      const auto eventX = event.event.GetX();
      const auto dx = eventX - mDragStartX;

      const auto& viewInfo = ViewInfo::Get(project);

      const auto eventT = viewInfo.PositionToTime(
         viewInfo.TimeToPosition(mInitialBorderPosition, event.rect.x) + dx,
         event.rect.x
      );

      const auto offset = sampleCount(floor((eventT - mInitialBorderPosition) * mTrack->GetRate())).as_double()
         / mTrack->GetRate();
      const auto t = std::clamp(mInitialBorderPosition + offset, mRange.first, mRange.second);
      const auto wasSnapped = mSnap.Snapped();
      if(mSnapManager)
         mSnap = mSnapManager->Snap(mTrack.get(), t, !mAdjustingLeftBorder);
      if(mSnap.Snapped())
      {
         if (mSnap.outTime >= mRange.first && mSnap.outTime <= mRange.second)
         {
            //Make sure that outTime belongs to the adjustment range after snapping
            TrimTo(mSnap.outTime);
            return RefreshCode::RefreshAll;
         }
         mSnap = {};
      }
      TrimTo(t);
      //If there was a snap line, make sure it is removed
      //from the screen by redrawing whole TrackPanel
      return wasSnapped ? RefreshCode::RefreshAll : RefreshCode::RefreshCell;
   }

   void Finish(AudacityProject& project) override
   {
      const auto dt = std::abs(mInitialBorderPosition - mBorderPosition);
      if (dt != 0)
      {
         if (mIsStretchMode)
         {
            WaveClipUIUtilities::PushClipSpeedChangedUndoState(
               project, 100.0 / mInterval->GetStretchRatio());
         }
         else if (mAdjustingLeftBorder)
         {
            /*i18n-hint: This is about trimming a clip, a length in seconds like "2.4 seconds" is shown*/
            ProjectHistory::Get(project).PushState(XO("Adjust left trim by %.02f seconds").Format(dt),
               /*i18n-hint: This is about trimming a clip, a length in seconds like "2.4s" is shown*/
                XO("Trim by %.02fs").Format(dt));
         }
         else
         {
            /*i18n-hint: This is about trimming a clip, a length in seconds like "2.4 seconds" is shown*/
            ProjectHistory::Get(project).PushState(XO("Adjust right trim by %.02f seconds").Format(dt),
            /*i18n-hint: This is about trimming a clip, a length in seconds like "2.4s" is shown*/
                XO("Trim by %.02fs").Format(dt));
         }
      }
   }

   void Cancel() override
   {
      TrimTo(mInitialBorderPosition);
   }

   void Draw(TrackPanelDrawingContext& context, const wxRect& rect, unsigned iPass) override
   {
      if(iPass == TrackArtist::PassSnapping && mSnap.Snapped())
      {
         auto &dc = context.dc;
         TrackArt::DrawSnapLines(&dc, rect.x + mSnap.outCoord, -1);
      }
   }

   wxRect DrawingArea(TrackPanelDrawingContext&, const wxRect& rect, const wxRect& panelRect, unsigned iPass) override
   {
      if(iPass == TrackArtist::PassSnapping)
        return TrackPanelDrawable::MaximizeHeight(rect, panelRect);
      return rect;
   }
};

HitTestPreview WaveClipAdjustBorderHandle::HitPreviewTrim(const AudacityProject*, bool unsafe, bool isLeftBorder)
{
   static auto disabledCursor =
      MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
   static auto trimCursorLeft =
      MakeCursor(wxCURSOR_SIZEWE, ClipTrimLeftXpm , 16, 16);
   static auto trimCursorRight =
      MakeCursor(wxCURSOR_SIZEWE, ClipTrimRightXpm, 16, 16);
   auto message = XO("Click and drag to move clip boundary in time");

   return {
      message,
      (unsafe
         ? &*disabledCursor
         : &*(isLeftBorder ? trimCursorLeft : trimCursorRight))
   };
}

HitTestPreview WaveClipAdjustBorderHandle::HitPreviewStretch(const AudacityProject*, bool unsafe, bool isLeftBorder)
{
   static auto disabledCursor =
      MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
   static auto stretchCursorLeft =
      MakeCursor(wxCURSOR_SIZEWE, ClipStretchLeftXpm, 16, 16);
   static auto stretchCursorRight =
      MakeCursor(wxCURSOR_SIZEWE, ClipStretchRightXpm, 16, 16);
   auto message = XO("Click and drag to stretch clip");

   return {
      message,
      (unsafe
         ? &*disabledCursor
         : &*(isLeftBorder ? stretchCursorLeft : stretchCursorRight))
   };
}

WaveClipAdjustBorderHandle::WaveClipAdjustBorderHandle(
   std::unique_ptr<AdjustPolicy>& adjustPolicy,
   std::shared_ptr<const WaveTrack> pTrack,
   bool stretchMode,
   bool leftBorder)
   : mAdjustPolicy{ std::move(adjustPolicy) }
   , mpTrack{ move(pTrack) }
   , mIsStretchMode{stretchMode}
   , mIsLeftBorder{leftBorder}
{

}

WaveClipAdjustBorderHandle::~WaveClipAdjustBorderHandle() = default;

std::shared_ptr<const Track> WaveClipAdjustBorderHandle::FindTrack() const
{
   return mpTrack;
}

WaveClipAdjustBorderHandle::WaveClipAdjustBorderHandle(WaveClipAdjustBorderHandle&&) noexcept = default;

WaveClipAdjustBorderHandle& WaveClipAdjustBorderHandle::operator=(WaveClipAdjustBorderHandle&&) noexcept = default;

void WaveClipAdjustBorderHandle::AdjustPolicy::Draw(TrackPanelDrawingContext&, const wxRect&, unsigned) { }

wxRect WaveClipAdjustBorderHandle::AdjustPolicy::DrawingArea(TrackPanelDrawingContext&, const wxRect& rect, const wxRect&, unsigned)
{
   return rect;
}

UIHandlePtr WaveClipAdjustBorderHandle::HitAnywhere(
   std::weak_ptr<WaveClipAdjustBorderHandle>& holder,
   const std::shared_ptr<WaveTrack>& waveTrack,
   const AudacityProject* pProject,
   const TrackPanelMouseState& state)
{
   const auto rect = state.rect;

   const auto px = state.state.m_x;

   auto& zoomInfo = ViewInfo::Get(*pProject);

   std::shared_ptr<WaveTrack::Interval> leftInterval;
   std::shared_ptr<WaveTrack::Interval> rightInterval;

   //Test left and right boundaries of each clip
   //to determine which kind of adjustment is
   //more appropriate
   for (const auto &interval : waveTrack->Intervals()) {
      const auto& clip = *interval;
      if(!WaveChannelView::ClipDetailsVisible(clip, zoomInfo, rect))
         continue;

      auto clipRect = ClipParameters::GetClipRect(clip, zoomInfo, rect);
      if(std::abs(px - clipRect.GetLeft()) <= BoundaryThreshold * 2)
         rightInterval = interval;
      else if (std::abs(px - clipRect.GetRight()) <= BoundaryThreshold * 2)
         leftInterval = interval;
   }

   std::shared_ptr<WaveTrack::Interval> adjustedInterval;
   bool adjustLeftBorder {false};
   if (leftInterval && rightInterval)
   {
      //between adjacent clips
      if(ClipParameters::GetClipRect(*leftInterval, zoomInfo, rect).GetRight() > px)
      {
         adjustedInterval = leftInterval;
         adjustLeftBorder = false;
      }
      else
      {
         adjustedInterval = rightInterval;
         adjustLeftBorder = true;
      }
   }
   else
   {
      adjustedInterval = leftInterval ? leftInterval : rightInterval;
      if (adjustedInterval)
      {
         //single clip case, determine the border,
         //hit testing area differs from one
         //used for general case
         const auto clipRect = ClipParameters::GetClipRect(*adjustedInterval, zoomInfo, rect);
         if (std::abs(px - clipRect.GetLeft()) <= BoundaryThreshold)
            adjustLeftBorder = true;
         else if (std::abs(px - clipRect.GetRight()) <= BoundaryThreshold)
            adjustLeftBorder = false;
         else
            adjustedInterval.reset();
      }
   }

   if(adjustedInterval)
   {
      const auto isStretchMode = state.state.AltDown();
      AdjustClipBorder::AdjustHandler adjustHandler = isStretchMode
         ? (adjustLeftBorder ? StretchLeftTo : StretchRightTo)
         : (adjustLeftBorder ? TrimLeftTo : TrimRightTo);

      std::unique_ptr<AdjustPolicy> policy =
         std::make_unique<AdjustClipBorder>(
            adjustHandler, waveTrack, adjustedInterval, adjustLeftBorder,
            isStretchMode, zoomInfo);

      return AssignUIHandlePtr(
         holder,
         std::make_shared<WaveClipAdjustBorderHandle>(policy, waveTrack,
            isStretchMode,
            adjustLeftBorder));
   }
   return { };
}

UIHandlePtr WaveClipAdjustBorderHandle::HitTest(std::weak_ptr<WaveClipAdjustBorderHandle>& holder,
    WaveChannelView& view, const AudacityProject* pProject,
    const TrackPanelMouseState& state)
{
    auto waveChannel = view.FindWaveChannel();
    // For multichannel tracks, show adjustment handle only for the topmost
    // channel
    if (waveChannel->GetChannelIndex() != 0)
        return {};

    std::vector<UIHandlePtr> results;

    const auto rect = state.rect;

    auto py = state.state.m_y;

    if (py >= rect.GetTop() &&
        py <= (rect.GetTop() + static_cast<int>(rect.GetHeight() * 0.3)))
    {
        return HitAnywhere(holder,
            waveChannel->GetTrack().SharedPointer<WaveTrack>(),
            pProject, state);
    }
    return {};
}



HitTestPreview WaveClipAdjustBorderHandle::Preview(const TrackPanelMouseState& mouseState, AudacityProject* pProject)
{
   const bool unsafe = ProjectAudioIO::Get(*pProject).IsAudioActive();
   return mIsStretchMode
      ? HitPreviewStretch(pProject, unsafe, mIsLeftBorder)
      : HitPreviewTrim(pProject, unsafe, mIsLeftBorder);
}

UIHandle::Result WaveClipAdjustBorderHandle::Click
(const TrackPanelMouseEvent& event, AudacityProject* pProject)
{
   if (!ProjectAudioIO::Get(*pProject).IsAudioActive())
   {
      if (mAdjustPolicy->Init(event))
         return RefreshCode::RefreshNone;
   }
   return RefreshCode::Cancelled;
}

UIHandle::Result WaveClipAdjustBorderHandle::Drag
(const TrackPanelMouseEvent& event, AudacityProject* project)
{
   return mAdjustPolicy->Drag(event, *project);
}

UIHandle::Result WaveClipAdjustBorderHandle::Release
(const TrackPanelMouseEvent& event, AudacityProject* project,
    wxWindow* pParent)
{
   mAdjustPolicy->Finish(*project);
   return RefreshCode::RefreshAll;
}

UIHandle::Result WaveClipAdjustBorderHandle::Cancel(AudacityProject* pProject)
{
   mAdjustPolicy->Cancel();
   return RefreshCode::RefreshAll;
}

void WaveClipAdjustBorderHandle::Draw(TrackPanelDrawingContext& context, const wxRect& rect, unsigned iPass)
{
   mAdjustPolicy->Draw(context, rect, iPass);
}

wxRect WaveClipAdjustBorderHandle::DrawingArea(TrackPanelDrawingContext& context, const wxRect& rect,
   const wxRect& panelRect, unsigned iPass)
{
   return mAdjustPolicy->DrawingArea(context, rect, panelRect, iPass);
}
