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

Audacity: A Digital Audio Editor

PlayIndicatorOverlay.cpp

Paul Licameli split from TrackPanel.cpp

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


#include "PlayIndicatorOverlay.h"

#include "AColor.h"
#include "../../AdornedRulerPanel.h"
#include "AllThemeResources.h"
#include "AudioIO.h"
#include "LabelTrack.h"
#include "Project.h"
#include "ProjectAudioIO.h"
#include "../../ProjectAudioManager.h"
#include "../../ProjectWindow.h"
#include "Theme.h"
#include "Track.h"
#include "../../TrackPanel.h"
#include "ViewInfo.h"
#include "Scrubbing.h"
#include "ChannelView.h"

#include <wx/dc.h>

#include <algorithm>

namespace {
   template < class LOW, class MID, class HIGH >
   bool between_incexc(LOW l, MID m, HIGH h)
   {
      return (m >= l && m < h);
   }

   enum { IndicatorMediumWidth = 13 };
}

PlayIndicatorOverlayBase::PlayIndicatorOverlayBase(AudacityProject *project, bool isMaster)
: mProject(project)
, mIsMaster(isMaster)
{
}

PlayIndicatorOverlayBase::~PlayIndicatorOverlayBase()
{
}

unsigned PlayIndicatorOverlayBase::SequenceNumber() const
{
   return 10;
}

namespace {
// Returns the appropriate bitmap, and panel-relative coordinates for its
// upper left corner.
std::pair< wxPoint, wxBitmap > GetIndicatorBitmap( AudacityProject &project,
   wxCoord xx, bool playing)
{
   wxBitmap & bmp = theTheme.Bitmap(
      playing ? bmpPlayPointer : bmpRecordPointer
   );
   const int IndicatorHalfWidth = bmp.GetWidth() / 2;
   return {
      { xx - IndicatorHalfWidth - 1,
         AdornedRulerPanel::Get(project).GetInnerRect().y },
      bmp
   };
}
}

std::pair<wxRect, bool> PlayIndicatorOverlayBase::DoGetRectangle(wxSize size)
{
   wxCoord width = 1, xx = mLastIndicatorX;

   if ( !mIsMaster ) {
      auto gAudioIO = AudioIO::Get();
      bool rec = gAudioIO->IsCapturing();
      auto pair = GetIndicatorBitmap( *mProject, xx, !rec );
      xx = pair.first.x;
      width = pair.second.GetWidth();
   }

   // May be excessive height, but little matter
   wxRect rect( xx, 0, width, size.GetHeight());
   return {
      rect,
      (mLastIndicatorX != mNewIndicatorX
       || mLastIsCapturing != mNewIsCapturing)
   };
}


void PlayIndicatorOverlayBase::Draw(OverlayPanel &panel, wxDC &dc)
{
   // Set play/record color
   auto gAudioIO = AudioIO::Get();
   bool rec = gAudioIO->IsCapturing();
   AColor::IndicatorColor(&dc, !rec);

   if (mIsMaster
       && mLastIsCapturing != mNewIsCapturing) {
      // Detect transition to recording during punch and roll; make ruler
      // change its button color too
      auto &ruler = AdornedRulerPanel::Get( *mProject );
      ruler.UpdateButtonStates();
      ruler.Refresh();
   }
   mLastIsCapturing = mNewIsCapturing;

   mLastIndicatorX = mNewIndicatorX;
   if (!between_incexc(0, mLastIndicatorX, dc.GetSize().GetWidth()))
      return;

   if(auto tp = dynamic_cast<TrackPanel*>(&panel)) {
      wxASSERT(mIsMaster);

      AColor::Line(dc, mLastIndicatorX, 0, mLastIndicatorX, tp->GetSize().GetHeight());
   }
   else if(auto ruler = dynamic_cast<AdornedRulerPanel*>(&panel)) {
      wxASSERT(!mIsMaster);

      auto pair = GetIndicatorBitmap( *mProject, mLastIndicatorX, !rec );
      dc.DrawBitmap( pair.second, pair.first.x, pair.first.y );
   }
   else
      wxASSERT(false);
}

static const AudacityProject::AttachedObjects::RegisteredFactory sOverlayKey{
  []( AudacityProject &parent ){
     auto result = std::make_shared< PlayIndicatorOverlay >( &parent );
     TrackPanel::Get( parent ).AddOverlay( result );
     return result;
   }
};

PlayIndicatorOverlay::PlayIndicatorOverlay(AudacityProject *project)
: PlayIndicatorOverlayBase(project, true)
{
   mSubscription = ProjectWindow::Get( *mProject )
      .GetPlaybackScroller().Subscribe( *this, &PlayIndicatorOverlay::OnTimer );
}

void PlayIndicatorOverlay::OnTimer(Observer::Message)
{
   // Ensure that there is an overlay attached to the ruler
   if (!mPartner) {
      auto &ruler = AdornedRulerPanel::Get( *mProject );
      mPartner = std::make_shared<PlayIndicatorOverlayBase>(mProject, false);
      ruler.AddOverlay( mPartner );
   }

   const auto &viewInfo = ViewInfo::Get( *mProject );
   auto width = viewInfo.GetTracksUsableWidth();

   if (!ProjectAudioIO::Get( *mProject ).IsAudioActive()) {
      mNewIndicatorX = -1;
      mNewIsCapturing = false;
      const auto &scrubber = Scrubber::Get( *mProject );
      if (scrubber.HasMark()) {
         auto position = scrubber.GetScrubStartPosition();
         const auto offset = viewInfo.GetLeftOffset();
         if(position >= viewInfo.GetLeftOffset() &&
            position < offset + width)
            mNewIndicatorX = position;
      }
   }
   else {
      auto &viewport = Viewport::Get(*mProject);
      auto &window = ProjectWindow::Get( *mProject );
      auto &scroller = window.GetPlaybackScroller();
      // Calculate the horizontal position of the indicator
      const double playPos = scroller.GetRecentStreamTime();

      using Mode = ProjectWindow::PlaybackScroller::Mode;
      const Mode mode = scroller.GetMode();
      const bool pinned = ( mode == Mode::Pinned || mode == Mode::Right );

      // Use a small tolerance to avoid flicker of play head pinned all the way
      // left or right
      const auto tolerance = pinned
         ? 1.5 * std::chrono::duration<double>{kTimerInterval}.count()
         : 0;
      bool onScreen = playPos >= 0.0 &&
         between_incexc(viewInfo.hpos - tolerance,
         playPos,
         viewInfo.GetScreenEndTime() + tolerance);

      auto gAudioIO = AudioIO::Get();
      const auto &scrubber = Scrubber::Get( *mProject );

      // BG: Scroll screen if option is set
      if( viewInfo.bUpdateTrackIndicator &&
          playPos >= 0 && !onScreen ) {
         // msmeyer: But only if not playing looped or in one-second mode
         // PRL: and not scrolling with play/record head fixed
         auto mode = ProjectAudioManager::Get( *mProject ).GetLastPlayMode();
         if (!pinned &&
             mode != PlayMode::oneSecondPlay &&
             !gAudioIO->IsPaused() &&
             // Bug 2656 allow scrolling when paused in
             // scrubbing/play-at-speed.
             // ONLY do this additional test if scrubbing/play-at-speed
             // is active.
             (!scrubber.IsScrubbing() || !scrubber.IsPaused())
            )
         {
            auto newPos = playPos;
            if (playPos < viewInfo.hpos) {
               // This is possible when scrubbing backwards.
               // We want to page leftward by (at least) a whole screen, not
               // just a little bit equal to the scrubbing poll interval
               // duration.
               newPos = viewInfo.OffsetTimeByPixels( newPos, -width );
               newPos = std::max(newPos, viewport.ScrollingLowerBoundTime());
            }
            viewport.SetHorizontalThumb(newPos);
            // Might yet be off screen, check it
            onScreen = playPos >= 0.0 &&
            between_incexc(viewInfo.hpos,
                           playPos,
                           viewInfo.GetScreenEndTime());
         }
      }

      // Always update scrollbars even if not scrolling the window. This is
      // important when NEW audio is recorded, because this can change the
      // length of the project and therefore the appearance of the scrollbar.
      viewport.UpdateScrollbarsForTracks();

      if (onScreen)
         mNewIndicatorX =
            viewInfo.TimeToPosition(playPos, viewInfo.GetLeftOffset());
      else
         mNewIndicatorX = -1;

      mNewIsCapturing = gAudioIO->IsCapturing();
   }

   if(mPartner)
      mPartner->Update(mNewIndicatorX);
}
