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

Audacity: A Digital Audio Editor

WaveChannelView.cpp

Paul Licameli split from TrackPanel.cpp

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

#include "WaveChannelView.h"

#include <unordered_set>

#include "CutlineHandle.h"

#include <numeric>
#include <wx/dc.h>
#include <wx/graphics.h>

#include "AColor.h"
#include "WaveClip.h"
#include "WaveTrack.h"

#include "../../../../../images/Cursors.h"
#include "AllThemeResources.h"

#include "../../../../commands/CommandContext.h"
#include "../../../../HitTestResult.h"
#include "ProjectHistory.h"
#include "../../../../RefreshCode.h"
#include "SyncLock.h"
#include "../../../../TrackArtist.h"
#include "../../../../TrackPanel.h"
#include "../../../../TrackPanelAx.h"
#include "../../../../TrackPanelDrawingContext.h"
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../../TrackPanelResizeHandle.h"
#include "ViewInfo.h"
#include "../../../../prefs/TracksPrefs.h"

#include "../../../ui/TimeShiftHandle.h"
#include "../../../ui/ButtonHandle.h"
#include "../../../../TrackInfo.h"

#include "../WaveTrackUtils.h"

#include "WaveTrackAffordanceControls.h"
#include "WaveTrackAffordanceHandle.h"
#include "WaveClipAdjustBorderHandle.h"
#include "WaveClipUtilities.h"

constexpr int kClipDetailedViewMinimumWidth{ 3 };

using WaveChannelSubViewPtrs = std::vector<std::shared_ptr<WaveChannelSubView>>;

namespace {
struct PlacementArray : ClientData::Cloneable<> {
   static PlacementArray &Get(Track &track);
   static const PlacementArray &Get(const Track &track);
   ~PlacementArray() = default;
   std::unique_ptr<Cloneable<>> Clone() const {
      return std::make_unique<PlacementArray>(*this); }
   WaveChannelSubViewPlacements mPlacements;
   bool mMultiView{ false };
};

static const ChannelGroup::Attachments::RegisteredFactory
key { [](auto &) { return std::make_unique<PlacementArray>(); } };

// Access for per-track effect list
PlacementArray &PlacementArray::Get(Track &track)
{
   return track.GetGroupData().Attachments
      ::Get<PlacementArray>(key);
}

const PlacementArray &PlacementArray::Get(const Track &track)
{
   return Get(const_cast<Track &>(track));
}
}

WaveChannelSubViewPlacements &WaveChannelView::DoGetPlacements()
{
   auto &waveTrack = *std::dynamic_pointer_cast<WaveTrack>(FindTrack());
   return PlacementArray::Get(waveTrack).mPlacements;
}

const WaveChannelSubViewPlacements &WaveChannelView::DoGetPlacements() const
{
   return const_cast<WaveChannelView&>(*this).DoGetPlacements();
}

bool &WaveChannelView::DoGetMultiView()
{
   auto &waveTrack = *std::dynamic_pointer_cast<WaveTrack>(FindTrack());
   return PlacementArray::Get(waveTrack).mMultiView;
}

bool WaveChannelView::DoGetMultiView() const
{
   return const_cast<WaveChannelView&>(*this).DoGetMultiView();
}

// Structure that collects and modifies information on sub-view positions
// Written with great generality, allowing any number of sub-views
struct SubViewAdjuster
{
   enum { HotZoneSize = 5 }; // so many pixels at top and bottom of each subview

   SubViewAdjuster(WaveChannelView &view)
      : mwView{
         std::static_pointer_cast<WaveChannelView>(view.shared_from_this()) }
   {
      mSubViews = view.GetAllSubViews();
      mOrigPlacements = mNewPlacements = view.SavePlacements();
      FindPermutation();
   }

   void FindPermutation()
   {
      // Find a certain sort of the sub-views
      auto size = mOrigPlacements.size();
      wxASSERT( mSubViews.size() == size );
      mPermutation.resize( size );
      const auto begin = mPermutation.begin(), end = mPermutation.end();
      std::iota( begin, end, 0 );
      static auto invisible = [](const WaveChannelSubViewPlacement &placement) {
         return placement.index < 0 || placement.fraction <= 0;
      };
      const auto comp = [this]( size_t ii, size_t jj ){
         auto &pi = mOrigPlacements[ii];
         bool iInvisible = invisible( pi );

         auto &pj = mOrigPlacements[jj];
         bool jInvisible = invisible( pj );

         // Sort the invisibles to the front, rest by index
         if ( iInvisible != jInvisible )
            return iInvisible;
         else if ( !iInvisible )
            return pi.index < pj.index;
         else
            // Minor sort among the invisible views by their type
            return mSubViews[ii]->SubViewType() < mSubViews[jj]->SubViewType();
      };
      std::sort( begin, end, comp );
      // Find the start of visible sub-views
      auto first = std::find_if( begin, end, [this](size_t ii){
         return !invisible( mOrigPlacements[ii] );
      } );
      mFirstSubView = first - begin;
   }

   size_t NVisible() const
   { return mPermutation.size() - mFirstSubView; }

   bool ModifyPermutation( bool top )
   {
      bool rotated = false;
      const auto pBegin = mPermutation.begin(), pEnd = mPermutation.end();
      auto pFirst = pBegin + mFirstSubView;
      if ( mFirstSubView > 0 ) {
         // In case of dragging the top edge of the topmost view, or the
         // bottom edge of the bottommost, decide which of the invisible
         // views can become visible, and reassign the sequence.
         // For definiteness, that choice depends on the subview type numbers;
         // see the sorting criteria above.
         --mFirstSubView;
         --pFirst;
         if ( top ) {
            // If you drag down the top, the greatest-numbered invisible
            // subview type will appear there.
            mNewPlacements[ *pFirst ].fraction = 0;
         }
         else {
            // If you drag up the bottom, let the least-numbered invisible
            // subview type appear there.
            mNewPlacements[ *pBegin ].fraction = 0;
            std::rotate( pBegin, pBegin + 1, pEnd );
            rotated = true;
         }
      }
      // Reassign index numbers to all sub-views and 0 fraction to invisibles
      for ( auto pIter = pBegin; pIter != pFirst; ++pIter ) {
         auto &placement = mNewPlacements[ *pIter ];
         placement.index = -1;
         placement.fraction = 0;
      }
      size_t index = 0;
      for ( auto pIter = pFirst; pIter != pEnd; ++pIter )
         mNewPlacements[ *pIter ].index = index++;
      return rotated;
   }

   size_t FindIndex(WaveChannelSubView &subView) const
   {
      const auto begin = mPermutation.begin(), end = mPermutation.end();
      auto iter = std::find_if( begin, end, [&](size_t ii){
         return mSubViews[ ii ].get() == &subView;
      } );
      return iter - begin;
   }

   std::pair< size_t, bool >
   HitTest(WaveChannelSubView &subView,
      wxCoord yy, wxCoord top, wxCoord height)
   {
      const auto index = FindIndex( subView );
      auto size = mPermutation.size();
      if ( index < (int)size ) {
         yy -= top;
         if ( yy >= 0 && yy < HotZoneSize && index > 0 )
            return { index, true }; // top hit
         if ( yy < height && yy >= height - HotZoneSize &&
            // Have not yet called ModifyPermutation; dragging bottom of
            // bottommost view allowed only if at least one view is invisible
            ( index < (int)size - 1 || mFirstSubView > 0 ) )
            return { index, false }; // bottom hit
      }
      return { size, false }; // not hit
   }

   std::vector<wxCoord> ComputeHeights( wxCoord totalHeight )
   {
      // Compute integer-valued heights
      float total = 0;
      for (const auto index : mPermutation ) {
         const auto &placement = mOrigPlacements[ index ];
         total += std::max( 0.f, placement.fraction );
      }
      float partial = 0;
      wxCoord lastCoord = 0;
      std::vector<wxCoord> result;
      for (const auto index : mPermutation ) {
         const auto &placement = mOrigPlacements[ index ];
         auto fraction = std::max( 0.f, placement.fraction );
         wxCoord coord = ( (partial + fraction ) / total ) * totalHeight;
         auto height = coord - lastCoord;
         result.emplace_back( height );
         mNewPlacements[ index ].fraction = height;
         lastCoord = coord;
         partial += fraction;
      }
      return result;
   }

   void UpdateViews( bool rollback )
   {
      auto pView = mwView.lock();
      if ( pView ) {
         auto pTrack = static_cast< WaveTrack* >( pView->FindTrack().get() );
         WaveChannelView::Get(*pTrack).RestorePlacements(
            rollback ? mOrigPlacements : mNewPlacements);
      }
   }

   std::weak_ptr<WaveChannelView> mwView;
   WaveChannelSubViewPtrs mSubViews;
   WaveChannelSubViewPlacements mOrigPlacements, mNewPlacements;
   // Array mapping ordinal into the placement and subview arrays
   std::vector< size_t > mPermutation;
   // index into mPermutation
   size_t mFirstSubView{};
};

class SubViewAdjustHandle : public UIHandle
{
public:
   enum { MinHeight = SubViewAdjuster::HotZoneSize };

   static UIHandlePtr HitTest(std::weak_ptr<SubViewAdjustHandle> &holder,
      WaveChannelView &view,
      WaveChannelSubView &subView,
      const TrackPanelMouseState &state)
   {
      if ( !view.GetMultiView() )
         return {};

      SubViewAdjuster adjuster{ view };
      auto hit = adjuster.HitTest( subView,
         state.state.GetY(), state.rect.GetTop(), state.rect.GetHeight() );
      auto index = hit.first;

      if ( index < adjuster.mPermutation.size() ) {
         auto result = std::make_shared< SubViewAdjustHandle >(
            std::move( adjuster ), index, view.GetLastHeight(), hit.second
         );
         result = AssignUIHandlePtr( holder, result );
         return result;
      }
      else
         return {};
   }

   SubViewAdjustHandle(
      SubViewAdjuster &&adjuster, size_t subViewIndex,
         wxCoord viewHeight, bool top )
      : mAdjuster{ std::move( adjuster ) }
      , mMySubView{ subViewIndex }
      , mViewHeight{ viewHeight }
      , mTop{ top }
   {
      if ( mAdjuster.ModifyPermutation( top ) )
         --mMySubView;
   }

   Result Click(
      const TrackPanelMouseEvent &event, AudacityProject *pProject ) override
   {
      using namespace RefreshCode;
      const auto &permutation = mAdjuster.mPermutation;
      const auto size = permutation.size();
      if ( mMySubView >= size )
         return Cancelled;

      if (event.event.LeftDClick()) {
         for ( auto &placement : mAdjuster.mNewPlacements ) {
            if ( placement.index >= 0 )
               placement.fraction = 1.0f;
            else
               placement.fraction = 0.0f;
         }
         mAdjuster.UpdateViews( false );
         ProjectHistory::Get( *pProject ).ModifyState( false );

         // Do not start a drag
         return Cancelled | RefreshAll;
      }

      const auto &rect = event.rect;
      const auto height = rect.GetHeight();
      mOrigHeight = height;

      mOrigHeights = mAdjuster.ComputeHeights( mViewHeight );

      // Find the total height of the sub-views that may resize
      mTotalHeight = 0;
      auto index = ( mTop ? mAdjuster.mFirstSubView : mMySubView );
      const auto end = ( mTop ?  mMySubView + 1 : permutation.size() );
      for (; index != end; ++index)
         mTotalHeight += mOrigHeights[ index ];

      wxASSERT( height == mOrigHeights[ mMySubView ] );

      // Compute the maximum and minimum Y coordinates for drag effect
      if ( mTop ) {
         mOrigY = rect.GetTop();
         mYMax = rect.GetBottom();
         mYMin = mYMax - mTotalHeight + 1;
      }
      else {
         mOrigY = rect.GetBottom();
         mYMin = rect.GetTop();
         mYMax = mYMin + mTotalHeight - 1;
      }

      return RefreshNone;
   }

   Result Drag( const TrackPanelMouseEvent &event, AudacityProject * ) override
   {
      using namespace RefreshCode;
      auto pView = mAdjuster.mwView.lock();
      if ( !pView )
         return Cancelled;

      // Find new height for the dragged sub-view
      auto newY = std::max( mYMin, std::min( mYMax, event.event.GetY() ) );
      const auto delta = newY - mOrigY;
      wxCoord newHeight = mTop
         ? mOrigHeight - delta
         : mOrigHeight + delta
      ;
      wxASSERT( newHeight >= 0 && newHeight <= mTotalHeight );
      if ( newHeight < MinHeight )
         // Snap the dragged sub-view to nothing
         newHeight = 0;

      // Reassign height for the dragged sub-view
      auto &myPlacement =
         mAdjuster.mNewPlacements[ mAdjuster.mPermutation[ mMySubView ] ];
      myPlacement.fraction = newHeight;

      // Grow or shrink other sub-views
      auto excess = newHeight - mOrigHeight; // maybe negative
      const auto adjustHeight = [&](size_t ii) {
         if (excess == 0)
            return true;

         const auto oldFraction = mOrigHeights[ ii ];

         auto index = mAdjuster.mPermutation[ ii ];
         auto &placement = mAdjuster.mNewPlacements[ index ];
         auto &fraction = placement.fraction;

         if (excess > oldFraction) {
            excess -= oldFraction, fraction = 0;
            return false;
         }
         else {
            auto newFraction = oldFraction - excess;
            if ( newFraction < MinHeight ) {
               // This snaps very short sub-views to nothing
               myPlacement.fraction += newFraction;
               fraction = 0;
            }
            else
               fraction = newFraction;
            return true;
         }
      };
      if ( mTop ) {
         for ( size_t ii = mMySubView; ii > 0; ) {
            --ii;
            if ( adjustHeight( ii ) )
               break;
         }
      }
      else {
         for ( size_t ii = mMySubView + 1, size = mAdjuster.mPermutation.size();
            ii < size; ++ii
         ) {
            if ( adjustHeight( ii ) )
               break;
         }
      }

      // Save adjustment to the track and request a redraw
      mAdjuster.UpdateViews( false );
      return RefreshAll;
   }

   HitTestPreview Preview(
      const TrackPanelMouseState &state, AudacityProject * ) override
   {
      static auto resizeCursor =
         ::MakeCursor(wxCURSOR_ARROW, SubViewsCursorXpm, 16, 16);
      return {
         XO(
"Click and drag to adjust sizes of sub-views, double-click to split evenly"),
         &*resizeCursor
      };
   }

   Result Release(
      const TrackPanelMouseEvent &event, AudacityProject *pProject,
      wxWindow *pParent) override
   {
      ProjectHistory::Get( *pProject ).ModifyState( false );
      return RefreshCode::RefreshNone;
   }

   Result Cancel( AudacityProject * ) override
   {
      mAdjuster.UpdateViews( true );
      return RefreshCode::RefreshAll;
   }

private:

   SubViewAdjuster mAdjuster;
   std::vector<wxCoord> mOrigHeights;

   // An index into mAdjuster.mPermutation
   size_t mMySubView{};

   wxCoord mYMin{}, mYMax{};
   wxCoord mViewHeight{}; // Total height of all sub-views
   wxCoord mTotalHeight{}; // Total height of adjusting sub-views only
   wxCoord mOrigHeight{};
   wxCoord mOrigY{};

   // Whether we drag the top or the bottom of the sub-view
   bool mTop{};
};

class SubViewRearrangeHandle : public UIHandle
{
public:
   // Make it somewhat wider than the close button
   enum { HotZoneWidth = 3 * kTrackInfoBtnSize / 2 };

   static UIHandlePtr HitTest(std::weak_ptr<SubViewRearrangeHandle> &holder,
      WaveChannelView &view, WaveChannelSubView &subView,
      const TrackPanelMouseState &state)
   {
      if ( !view.GetMultiView() )
         return {};

      SubViewAdjuster adjuster{ view };
      if ( adjuster.NVisible() < 2 )
         return {};

      auto relX = state.state.GetX() - state.rect.GetLeft();
      if ( relX >= HotZoneWidth )
         return {};

      auto index = adjuster.FindIndex( subView );

      // Hit on the rearrange cursor only in the top and bottom thirds of
      // sub-view height, leaving the rest free to hit the selection cursor
      // first.
      // And also exclude the top third of the topmost sub-view and bottom
      // third of bottommost.
      auto relY = state.state.GetY() - state.rect.GetTop();
      auto height = state.rect.GetHeight();
      bool hit =
         ( ( 3 * relY < height ) && index > 0 ) // top hit
      ||
         ( ( 3 * relY > 2 * height ) &&
           index < adjuster.mPermutation.size() - 1 ) // bottom
      ;
      if ( ! hit )
         return {};

      auto result = std::make_shared< SubViewRearrangeHandle >(
         std::move( adjuster ),
         index, view.GetLastHeight()
      );
      result = AssignUIHandlePtr( holder, result );
      return result;
   }

   SubViewRearrangeHandle(
      SubViewAdjuster &&adjuster, size_t subViewIndex,
         wxCoord viewHeight )
      : mAdjuster{ std::move( adjuster ) }
      , mMySubView{ subViewIndex }
      , mViewHeight{ viewHeight }
   {
   }

   Result Click(
      const TrackPanelMouseEvent &event, AudacityProject *pProject ) override
   {
      using namespace RefreshCode;
      const auto &permutation = mAdjuster.mPermutation;
      const auto size = permutation.size();
      if ( mMySubView >= size )
         return Cancelled;

      mHeights = mAdjuster.ComputeHeights( mViewHeight );

      // Find y coordinate of first sub-view
      wxCoord heightAbove = 0;
      for (auto index = mAdjuster.mFirstSubView;
         index != mMySubView; ++index)
         heightAbove += mHeights[ index ];
      mTopY = event.rect.GetTop() - heightAbove;

      return RefreshNone;
   }

   bool Clicked() const { return !mHeights.empty(); }

   enum DragChoice_t{ Upward, Downward, Neutral };

   DragChoice_t DragChoice( const TrackPanelMouseEvent &event ) const
   {
      // Disregard x coordinate -- so the mouse need not be in any sub-view,
      // just in the correct range of y coordinates
      auto yy = event.event.GetY();
      auto coord = mTopY;
      size_t ii = mAdjuster.mFirstSubView;
      if ( yy < mTopY )
         return ( mMySubView == ii ) ? Neutral : Upward;

      for ( auto nn = mHeights.size(); ii < nn; ++ii ) {
         const auto height = mHeights[ ii ];
         coord += height;
         if ( yy < coord )
            break;
      }

      if ( ii < mMySubView ) {
         if ( yy < coord - mHeights[ ii ] + mHeights[ mMySubView ] )
            return Upward;
      }

      if ( ii > mMySubView ) {
         if( mMySubView < mHeights.size() - 1 &&
            yy >= coord - mHeights[ mMySubView ] )
         return Downward;
      }

      return Neutral;
   }

   Result Drag( const TrackPanelMouseEvent &event, AudacityProject * ) override
   {
      using namespace RefreshCode;
      auto pView = mAdjuster.mwView.lock();
      if ( !pView )
         return Cancelled;

      switch( DragChoice( event ) ) {
         case Upward:
         {
            std::swap( mHeights[ mMySubView ], mHeights[ mMySubView - 1 ] );
            std::swap(
               mAdjuster.mNewPlacements[ mMySubView ].index,
               mAdjuster.mNewPlacements[ mMySubView - 1 ].index
            );
            --mMySubView;
            break;
         }
         case Downward:
         {
            std::swap( mHeights[ mMySubView ], mHeights[ mMySubView + 1 ] );
            std::swap(
               mAdjuster.mNewPlacements[ mMySubView ].index,
               mAdjuster.mNewPlacements[ mMySubView + 1 ].index
            );
            ++mMySubView;
            break;
         }
         default:
            return RefreshNone;
      }

      // Save adjustment to the track and request a redraw
      mAdjuster.UpdateViews( false );
      return RefreshAll;
   }

   HitTestPreview Preview(
      const TrackPanelMouseState &state, AudacityProject * ) override
   {
      static auto hoverCursor =
         ::MakeCursor(wxCURSOR_HAND, RearrangeCursorXpm, 16, 16);
      static auto clickedCursor =
         ::MakeCursor(wxCURSOR_HAND, RearrangingCursorXpm, 16, 16);
      return {
         XO("Click and drag to rearrange sub-views"),
         Clicked() ? &*clickedCursor : &*hoverCursor,
         XO("Rearrange sub-views")
      };
   }

   Result Release(
      const TrackPanelMouseEvent &event, AudacityProject *pProject,
      wxWindow *pParent) override
   {
      ProjectHistory::Get( *pProject ).ModifyState( false );
      return RefreshCode::RefreshNone;
   }

   Result Cancel( AudacityProject * ) override
   {
      mAdjuster.UpdateViews( true );
      return RefreshCode::RefreshAll;
   }

private:

   SubViewAdjuster mAdjuster;
   std::vector<wxCoord> mHeights;
   wxCoord mTopY;

   // An index into mAdjuster.mPermutation
   size_t mMySubView{};

   wxCoord mViewHeight{}; // Total height of all sub-views
};

class SubViewCloseHandle : public ButtonHandle
{
   static wxRect GetButtonRect( const wxRect &rect )
   {
      return {
         rect.GetLeft(),
         rect.GetTop(),
         kTrackInfoBtnSize,
         kTrackInfoBtnSize
      };
   }

public:
   static UIHandlePtr HitTest(std::weak_ptr<SubViewCloseHandle> &holder,
      WaveChannelView &view, WaveChannelSubView &subView,
      const TrackPanelMouseState &state)
   {
      SubViewAdjuster adjuster{ view };
      if ( adjuster.NVisible() < 2 )
         return {};

      const auto rect = GetButtonRect( state.rect );
      if ( !rect.Contains( state.state.GetPosition() ) )
         return {};
      auto index = adjuster.FindIndex( subView );
      auto result = std::make_shared<SubViewCloseHandle>(
         std::move( adjuster ), index, view.FindTrack(), rect );
      result = AssignUIHandlePtr( holder, result );
      return result;
   }

   SubViewCloseHandle(
      SubViewAdjuster &&adjuster, size_t index,
      const std::shared_ptr<Track> &pTrack, const wxRect &rect )
      : ButtonHandle{ pTrack, rect }
      , mAdjuster{ std::move( adjuster ) }
      , mMySubView{ index }
   {
   }

   Result CommitChanges(
      const wxMouseEvent &event, AudacityProject *pProject, wxWindow *pParent)
      override
   {
      ProjectHistory::Get( *pProject ).ModifyState( false );
      auto &myPlacement =
         mAdjuster.mNewPlacements[ mAdjuster.mPermutation[ mMySubView ] ];
      myPlacement.fraction = 0;
      mAdjuster.UpdateViews( false );
      return RefreshCode::RefreshAll;
   }

   TranslatableString Tip(
      const wxMouseState &state, AudacityProject &project) const override
   {
      return XO("Close sub-view");
   }

   // TrackPanelDrawable implementation
   void Draw(
      TrackPanelDrawingContext &context, const wxRect &rect, unsigned iPass )
      override
   {
      if ( iPass == TrackArtist::PassMargins ) { // after PassTracks
          TrackInfo::DrawCloseButton(
             context, GetButtonRect(rect), GetTrack().get(), this );
      }
   }

private:
   SubViewAdjuster mAdjuster;
   size_t mMySubView{};
};

std::pair<
   bool, // if true, hit-testing is finished
   std::vector<UIHandlePtr>
> WaveChannelSubView::DoDetailedHitTest(
   const TrackPanelMouseState &state,
   const AudacityProject *pProject, int currentTool, bool bMultiTool,
   const std::shared_ptr<WaveTrack> &wt)
{
   auto results = WaveChannelView::DoDetailedHitTest(
      state, pProject, currentTool, bMultiTool, wt, *this);
   if ( results.first )
      return results;

   auto pWaveChannelView = mwWaveChannelView.lock();
   if ( pWaveChannelView && !state.state.HasModifiers() ) {
      if ( auto pHandle = SubViewCloseHandle::HitTest(
         mCloseHandle,
         *pWaveChannelView, *this, state ) )
         results.second.push_back( pHandle );

      auto channels = TrackList::Channels(wt.get());
      if(channels.size() > 1) {
         // Only one cell is tested and we need to know
         // which one and it's relative location to the border.
         auto subviews = pWaveChannelView->GetSubViews();
         auto currentSubview = std::find_if(subviews.begin(), subviews.end(),
            [self = shared_from_this()](const auto& p){
               return self == p.second;
         });
         if (currentSubview != subviews.end())
         {
            auto currentSubviewIndex = std::distance(subviews.begin(), currentSubview);

            const auto py = state.state.GetY();
            const auto topBorderHit = std::abs(py - state.rect.GetTop())
               <= WaveChannelView::kChannelSeparatorThickness / 2;
            const auto bottomBorderHit = std::abs(py - state.rect.GetBottom())
               <= WaveChannelView::kChannelSeparatorThickness / 2;

            auto currentChannel = channels.find(wt.get());
            auto currentChannelIndex = std::distance(channels.begin(), currentChannel);

            if (//for not-last-view check the bottom border hit
               ((currentChannelIndex != channels.size() - 1)
                  && (currentSubviewIndex == static_cast<int>(subviews.size()) - 1)
                  && bottomBorderHit)
               ||
               //or for not-first-view check the top border hit
               ((currentChannelIndex != 0) && currentSubviewIndex == 0 && topBorderHit))
            {
               //depending on which border hit test succeeded on we
               //need to choose a proper target for resizing
               auto it = bottomBorderHit ? currentChannel : currentChannel.advance(-1);
               auto result = std::make_shared<TrackPanelResizeHandle>(
                  (*it)->GetChannel(0), py);
               result = AssignUIHandlePtr(mResizeHandle, result);
               results.second.push_back(result);
            }
         }
      }

      if ( auto pHandle = SubViewAdjustHandle::HitTest(
         mAdjustHandle,
         *pWaveChannelView, *this, state ) )
         results.second.push_back( pHandle );
      if ( auto pHandle = SubViewRearrangeHandle::HitTest(
         mRearrangeHandle,
         *pWaveChannelView, *this, state ) )
         results.second.push_back( pHandle );
   }
   if( pWaveChannelView )
   {
      if (auto pHandle = WaveClipAdjustBorderHandle::HitTest(
          mClipBorderAdjustHandle,
          *pWaveChannelView, pProject, state))
          results.second.push_back(pHandle);
   }
   if (auto result = CutlineHandle::HitTest(
      mCutlineHandle, state.state, state.rect,
      pProject, wt ))
      // This overriding test applies in all tools
      results.second.push_back(result);

   return results;
}


void WaveChannelSubView::DrawBoldBoundaries(
   TrackPanelDrawingContext &context, const WaveTrack &track,
   const wxRect &rect)
{
   auto &dc = context.dc;
   const auto artist = TrackArtist::Get( context );

   const auto &zoomInfo = *artist->pZoomInfo;

#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
   auto target2 = dynamic_cast<CutlineHandle*>(context.target.get());
#endif
   for (const auto loc : FindWaveTrackLocations(track)) {
      bool highlightLoc = false;
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
      highlightLoc =
         target2 && target2->GetTrack().get() == &track &&
         target2->GetLocation() == loc;
#endif
      const int xx = zoomInfo.TimeToPosition(loc.pos);
      if (xx >= 0 && xx < rect.width) {
         dc.SetPen( highlightLoc ? AColor::uglyPen : *wxGREY_PEN );
         AColor::Line(dc, (int) (rect.x + xx - 1), rect.y, (int) (rect.x + xx - 1), rect.y + rect.height);
         dc.SetPen( highlightLoc ? AColor::uglyPen : *wxRED_PEN );
         AColor::Line(dc, (int) (rect.x + xx), rect.y, (int) (rect.x + xx), rect.y + rect.height);
         dc.SetPen( highlightLoc ? AColor::uglyPen : *wxGREY_PEN );
         AColor::Line(dc, (int) (rect.x + xx + 1), rect.y, (int) (rect.x + xx + 1), rect.y + rect.height);
      }
   }
}

std::weak_ptr<WaveChannelView> WaveChannelSubView::GetWaveChannelView() const
{
   return mwWaveChannelView;
}

auto WaveChannelSubView::GetMenuItems(
   const wxRect &rect, const wxPoint *pPosition, AudacityProject *pProject)
      -> std::vector<MenuItem>
{
   auto pTrack = static_cast<WaveTrack*>( FindTrack().get() );
   if(pTrack != nullptr && pPosition != nullptr)
   {
      const auto &viewInfo = ViewInfo::Get(*pProject);
      const auto t = viewInfo.PositionToTime(pPosition->x, rect.x);
      if((pTrack->IsSelected() &&
         t > viewInfo.selectedRegion.t0() && t < viewInfo.selectedRegion.t1() &&
         !pTrack->GetClipsIntersecting(viewInfo.selectedRegion.t0(), viewInfo.selectedRegion.t1()).empty())
         ||
         pTrack->GetClipAtTime(t))
      {
         return GetWaveClipMenuItems();
      }
   }
   return {
         { L"Paste", XO("Paste")  },
         {},
         { L"TrackMute", XO("Mute/Unmute Track") },
      };
}

WaveChannelView &WaveChannelView::Get(WaveChannel &channel)
{
   return static_cast<WaveChannelView&>(ChannelView::Get(channel));
}

const WaveChannelView &WaveChannelView::Get(const WaveChannel &channel)
{
   return Get(const_cast<WaveChannel&>(channel));
}

WaveChannelView *WaveChannelView::Find(WaveChannel *pChannel)
{
   return static_cast<WaveChannelView*>(ChannelView::Find(pChannel));
}

const WaveChannelView *WaveChannelView::Find(const WaveChannel *pChannel)
{
   return Find(const_cast<WaveChannel*>(pChannel));
}

WaveChannelView::WaveChannelView(
   const std::shared_ptr<Track> &pTrack, size_t channel
)  : CommonChannelView{ pTrack, channel }
{
}

WaveChannelSubView::WaveChannelSubView(WaveChannelView &waveChannelView)
   : CommonChannelView{
      waveChannelView.FindTrack(), waveChannelView.GetChannelIndex() }
{
   mwWaveChannelView = std::static_pointer_cast<WaveChannelView>(
      waveChannelView.shared_from_this() );
}

void WaveChannelSubView::CopyToSubView(WaveChannelSubView *destSubView) const {

}

WaveChannelView::~WaveChannelView()
{
}

void WaveChannelView::CopyTo(Track &track) const
{
   ChannelView::CopyTo(track);
   auto &other = ChannelView::Get(*track.GetChannel(0));
   if (const auto pOther = dynamic_cast<WaveChannelView*>(&other)) {
      // only these fields are important to preserve in undo/redo history
      pOther->RestorePlacements( SavePlacements() );
      pOther->DoGetMultiView() = DoGetMultiView();

      auto srcSubViewsPtrs =
         const_cast<WaveChannelView*>(this)->GetAllSubViews();
      auto destSubViewsPtrs =
         const_cast<WaveChannelView*>(pOther)->GetAllSubViews();
      wxASSERT(srcSubViewsPtrs.size() == destSubViewsPtrs.size());

      for(auto i = 0; i != srcSubViewsPtrs.size(); i++){
         srcSubViewsPtrs[i]->CopyToSubView(destSubViewsPtrs[i].get());
      }
   }
}

std::vector<UIHandlePtr> WaveChannelView::DetailedHitTest(
   const TrackPanelMouseState &st,
   const AudacityProject *pProject, int currentTool, bool bMultiTool)
{
   // should not come here any more, delegation to sub-view instead
   wxASSERT( false );
   return {};
}

std::pair< bool, std::vector<UIHandlePtr> >
WaveChannelView::DoDetailedHitTest(
   const TrackPanelMouseState &st,
   const AudacityProject *pProject, int currentTool, bool bMultiTool,
   const std::shared_ptr<WaveTrack> &pTrack,
   CommonChannelView &view)
{
   // common hit-testing for different sub-view types, to help implement their
   // DetailedHitTest()

   // This is the only override of Track::DetailedHitTest that still
   // depends on the state of the Tools toolbar.
   // If that toolbar were eliminated, this could simplify to a sequence of
   // hit test routines describable by a table.

   std::vector<UIHandlePtr> results;

   const auto& viewInfo = ViewInfo::Get(*pProject);

   for (auto& clip : pTrack->GetClips())
   {
      if (!WaveChannelView::ClipDetailsVisible(*clip, viewInfo, st.rect)
         && HitTest(*clip, viewInfo, st.rect, st.state.GetPosition()))
      {
         auto &waveChannelView = WaveChannelView::Get(*pTrack);
         results.push_back(
            AssignUIHandlePtr(
               waveChannelView.mAffordanceHandle,
               std::make_shared<WaveTrackAffordanceHandle>(pTrack, clip)
            )
         );
      }
   }

   if (bMultiTool && st.state.CmdDown()) {
      // Ctrl modifier key in multi-tool overrides everything else
      // (But this does not do the time shift constrained to the vertical only,
      //  which is what happens when you hold Ctrl in the Time Shift tool mode)
      auto result = TimeShiftHandle::HitAnywhere(
         view.mTimeShiftHandle, pTrack, false);
      if (result)
         results.push_back(result);
      return { true, results };
   }

   return { false, results };
}

auto WaveChannelView::GetDisplays() const
   -> std::vector<WaveChannelSubView::Type>
{
   BuildSubViews();

   // Collect the display types of visible views and sort them by position
   using Pair = std::pair<int, WaveChannelSubView::Type>;
   std::vector< Pair > pairs;
   size_t ii = 0;
   const auto &placements = DoGetPlacements();
   WaveChannelSubViews::ForEach([&](const WaveChannelSubView &subView) {
      auto &placement = placements[ii];
      if (placement.fraction > 0)
         pairs.emplace_back(placement.index, subView.SubViewType());
      ++ii;
   } );
   std::sort( pairs.begin(), pairs.end() );
   std::vector<WaveChannelSubView::Type> results;
   for (const auto &pair : pairs)
      results.push_back(pair.second);
   return results;
}

void WaveChannelView::SetDisplay(Display display, bool exclusive)
{
   BuildSubViews();
   DoSetDisplay( display, exclusive );
}

bool WaveChannelView::ToggleSubView(Display display)
{
   size_t ii = 0;
   size_t found = 0;
   if (WaveChannelSubViews::FindIf([&](const WaveChannelSubView &subView) {
      if (subView.SubViewType().id == display) {
         found = ii;
         return true;
      }
      ++ii;
      return false;
   })) {
      auto &placements = DoGetPlacements();
      auto &foundPlacement = placements[found];
      if ( foundPlacement.fraction > 0.0 ) {
         // Toggle off

         if (GetDisplays().size() < 2)
            // refuse to do it
            return false;

         auto index = foundPlacement.index;
         foundPlacement = { -1, 0.0 };
         if (index >= 0) {
            for ( auto &placement : placements ) {
               if ( placement.index > index )
                  --placement.index;
            }
         }

         return true;
      }
      else {
         // Toggle on
         float total = 0;
         int greatest = -1;
         unsigned nn = 0;
         for ( const auto &placement : placements ) {
            if ( placement.fraction > 0.0 && placement.index >= 0 ) {
               total += placement.fraction;
               greatest = std::max( greatest, placement.index );
               ++nn;
            }
         }
         // Turn on the sub-view, putting it lowest, and with average of the
         // heights of the other sub-views
         foundPlacement = { greatest + 1, total / nn };

         return true;
      }
   }
   else
      // unknown sub-view
      return false;
}

// If exclusive, make the chosen view take up all the height.  Else,
// partition equally, putting the specified view on top.
// Be sure the sequence in which the other views appear is determinate.
void WaveChannelView::DoSetDisplay(Display display, bool exclusive)
{
   // Some generality here anticipating more than two views.
   // The order of sub-views in the array is not specified, so make it definite
   // by sorting by the view type constants.
   size_t ii = 0;
   std::vector<std::pair<WaveChannelViewConstants::Display, size_t>> pairs;
   WaveChannelSubViews::ForEach([&pairs, &ii](WaveChannelSubView &subView){
      pairs.push_back({ subView.SubViewType().id, ii++ });
   });
   std::sort( pairs.begin(), pairs.end() );

   int jj = 1;
   auto &placements = DoGetPlacements();
   for ( const auto &pair : pairs ) {
      auto &placement = placements[ pair.second ];
      if (pair.first == display) {
         // 0 for first view
         placement = { 0, 1.0 };
      }
      else if( exclusive )
         // -1 for not displayed
         placement = { -1, 0.0 };
      else
         // positions other than the first.
         // (Note that the fractions in the placement don't need to be
         // denominated to 1.  Just make them all equal to get an equal
         // partitioning of the sub-views.)
         placement = { jj++, 1.0 };
   }
}

namespace {
   template<typename Iter, typename Comp>
   const WaveClip* NextClipLooped(ViewInfo& viewInfo, Iter begin, Iter end, Comp comp)
   {
      auto it = WaveTrackUtils::SelectedClip(viewInfo, begin, end);
      if (it == end)
         it = std::find_if(begin, end, comp);
      else
         it = std::next(it);

      if (it == end)
         return *begin;
      return *it;
   }
}

bool WaveChannelView::SelectNextClip(
   ViewInfo& viewInfo, AudacityProject* project, bool forward)
{
   //Iterates through clips in a looped manner
   auto waveTrack = std::dynamic_pointer_cast<WaveTrack>(FindTrack());
   if (!waveTrack)
      return false;
   auto clips = waveTrack->SortedClipArray();
   if (clips.empty())
      return false;

   const WaveClip* clip{ };
   if (forward)
   {
      clip = NextClipLooped(viewInfo, clips.begin(), clips.end(), [&](const WaveClip* other) {
         return other->GetPlayStartTime() >= viewInfo.selectedRegion.t1();
      });
   }
   else
   {
      clip = NextClipLooped(viewInfo, clips.rbegin(), clips.rend(), [&](const WaveClip* other) {
         return other->GetPlayStartTime() <= viewInfo.selectedRegion.t0();
      });
   }

   viewInfo.selectedRegion.setTimes(clip->GetPlayStartTime(), clip->GetPlayEndTime());
   ProjectHistory::Get(*project).ModifyState(false);

   // create and send message to screen reader
   auto it = std::find(clips.begin(), clips.end(), clip);
   auto index = std::distance(clips.begin(), it);

   auto message = XP(
   /* i18n-hint:
       string is the name of a clip
       first number is the position of that clip in a sequence of clips,
       second number counts the clips */
       "%s, %d of %d clip",
       "%s, %d of %d clips",
       2
   )(
      clip->GetName(),
      static_cast<int>(index + 1),
      static_cast<int>(clips.size())
  );

   TrackFocus::Get(*project).MessageForScreenReader(message);
   return true;
}

auto WaveChannelView::GetSubViews(const wxRect &rect) -> Refinement
{
   return GetSubViews(&rect);
}

auto WaveChannelView::GetSubViews(const wxRect* rect) -> Refinement
{
   BuildSubViews();

   // Collect the visible views in the right sequence
   struct Item {
      int index; float fraction; std::shared_ptr<ChannelView> pView;
   };
   std::vector< Item > items;
   size_t ii = 0;
   float total = 0;
   const auto &placements = DoGetPlacements();
   WaveChannelSubViews::ForEach([&](WaveChannelSubView& subView) {
      auto& placement = placements[ii];
      auto index = placement.index;
      auto fraction = placement.fraction;
      if (index >= 0 && fraction > 0.0)
         total += fraction,
         items.push_back({ index, fraction, subView.shared_from_this() });
      ++ii;
   });
   std::sort(items.begin(), items.end(), [](const Item& a, const Item& b) {
      return a.index < b.index;
   });

   // Remove views we don't need
   auto begin = items.begin(), end = items.end(),
        newEnd = std::remove_if(begin, end,
            [](const Item& item) { return !item.pView; });
   items.erase(newEnd, end);

   Refinement results;

   if (rect != nullptr)
   {
      // Assign coordinates, redenominating to the total height,
      // storing integer values
      results.reserve(items.size());
      const auto top = rect->GetTop();
      const auto height = rect->GetHeight();
      float partial = 0;
      wxCoord lastCoord = 0;
      for (const auto& item : items) {
         wxCoord newCoord = top + (partial / total) * height;
         results.emplace_back(newCoord, item.pView);
         partial += item.fraction;
      }

      // Cache for the use of sub-view dragging
      mLastHeight = height;
   }
   else
   {
      std::transform(items.begin(), items.end(), std::back_inserter(results), [](const auto& item) {
         return std::make_pair(0, item.pView);
      });
   }

   return results;
}

/*
 Note that the WaveChannelView isn't in the TrackPanel subdivision, but it is
 set sometimes as the focused cell, and therefore the following functions can
 be visited.  To visit their overrides in the sub-views and affordances,
 which are never focused, we must forward to them.  To do that properly, if
 any cell declines to handle the event by setting it as skipped, it must be
 set again to not-skipped before attempting the next call-through.
 */
unsigned WaveChannelView::CaptureKey(
   wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent,
   AudacityProject* project)
{
   unsigned result{ RefreshCode::RefreshNone };
   auto pTrack = static_cast<WaveTrack*>(FindTrack().get());
   for (auto pChannel : pTrack->Channels()) {
      event.Skip(false);
      auto &waveChannelView = WaveChannelView::Get(*pChannel);
      // Give sub-views first chance to handle the event
      for (auto &subView : waveChannelView.GetSubViews()) {
         // Event defaults in skipped state which must be turned off explicitly
         wxASSERT(!event.GetSkipped());
         result |= subView.second->CaptureKey(event, viewInfo, pParent, project);
         if (!event.GetSkipped()) {
            // sub view wants it
            mKeyEventDelegate = subView.second;
            return result;
         }
         else
            event.Skip(false);
      }

      if (auto affordance = waveChannelView.GetAffordanceControls()) {
         result |= affordance->CaptureKey(event, viewInfo, pParent, project);
         if (!event.GetSkipped()) {
            mKeyEventDelegate = affordance;
            return result;
         }
      }

      event.Skip(false);
   }
   switch (event.GetKeyCode())
   {
   case WXK_TAB:
      break;
   default:
      result |= CommonChannelView::CaptureKey(
         event, viewInfo, pParent, project);
      break;
   };
   if (!event.GetSkipped()) {
      mKeyEventDelegate = shared_from_this();
   }

   return result;
}

unsigned WaveChannelView::KeyDown(
   wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent,
   AudacityProject* project)
{
   unsigned result{ RefreshCode::RefreshNone };
   if (auto delegate = mKeyEventDelegate.lock()) {
      if (auto pWaveChannelView =
         dynamic_cast<WaveChannelView*>(delegate.get()))
      {
         if (event.GetKeyCode() == WXK_TAB)
         {
            SelectNextClip(viewInfo, project, event.GetModifiers() != wxMOD_SHIFT);
            result |= RefreshCode::RefreshCell;
         }
         else
            result |= pWaveChannelView->CommonChannelView::KeyDown(
               event, viewInfo, pParent, project);
      }
      else
         result |= delegate->KeyDown(event, viewInfo, pParent, project);
   }
   else
      event.Skip();

   return result;
}

unsigned WaveChannelView::Char(
   wxKeyEvent& event, ViewInfo& viewInfo, wxWindow* pParent,
   AudacityProject* project)
{
   unsigned result{ RefreshCode::RefreshNone };
   if (auto delegate = mKeyEventDelegate.lock()) {
      if (auto pWaveChannelView =
         dynamic_cast<WaveChannelView*>(delegate.get()))
         result |= pWaveChannelView->CommonChannelView::Char(
            event, viewInfo, pParent, project);
      else
         result |= delegate->Char(event, viewInfo, pParent, project);
   }
   else
      event.Skip();

   return result;
}

unsigned WaveChannelView::LoseFocus(AudacityProject *project)
{
   unsigned result = RefreshCode::RefreshNone;
   if (auto delegate = mKeyEventDelegate.lock()) {
      if (auto pWaveChannelView =
         dynamic_cast<WaveChannelView*>(delegate.get()))
         result = pWaveChannelView->CommonChannelView::LoseFocus(project);
      else
         result = delegate->LoseFocus(project);
      mKeyEventDelegate.reset();
   }
   return result;
}

namespace {
using PMF = bool (WaveTrackAffordanceControls::*)(AudacityProject &);
bool AnyAffordance(AudacityProject& project, WaveChannelView &view, PMF pmf)
{
   const auto pLeader = static_cast<WaveTrack*>(
      *TrackList::Channels(view.FindTrack().get()).begin());
   auto& channelView = ChannelView::Get(*pLeader);
   if (const auto affordance =
      std::dynamic_pointer_cast<WaveTrackAffordanceControls>(
         channelView.GetAffordanceControls()).get()
      ; affordance && (affordance->*pmf)(project)
   )
      return true;
   return false;
}
}

bool WaveChannelView::CutSelectedText(AudacityProject& project)
{
   return
      AnyAffordance(project, *this, &WaveTrackAffordanceControls::OnTextCut);
}

bool WaveChannelView::CopySelectedText(AudacityProject& project)
{
   return
      AnyAffordance(project, *this, &WaveTrackAffordanceControls::OnTextCopy);
}

bool WaveChannelView::ClipDetailsVisible(const ClipTimes& clip,
   const ZoomInfo& zoomInfo, const wxRect& viewRect)
{
   //Do not fold clips to line at sample zoom level, as
   //it may become impossible to 'unfold' it when clip is trimmed
   //to a single sample
   bool showSamples{ false };
   auto clipRect = ClipParameters::GetClipRect(clip, zoomInfo, viewRect, &showSamples);
   return showSamples || clipRect.width >= kClipDetailedViewMinimumWidth;
}

wxRect WaveChannelView::ClipHitTestArea(const WaveClip& clip,
   const ZoomInfo& zoomInfo, const wxRect& viewRect)
{
   bool showSamples{ false };
   auto clipRect = ClipParameters::GetClipRect(clip, zoomInfo, viewRect, &showSamples);
   if (showSamples || clipRect.width >= kClipDetailedViewMinimumWidth)
      return clipRect;

   return clipRect.Inflate(2, 0);
}

bool WaveChannelView::HitTest(const WaveClip& clip,
   const ZoomInfo& viewInfo, const wxRect& viewRect, const wxPoint& pos)
{
   return ClipHitTestArea(clip, viewInfo, viewRect).Contains(pos);
}

bool WaveChannelView::PasteText(AudacityProject& project)
{
   return
      AnyAffordance(project, *this, &WaveTrackAffordanceControls::OnTextPaste);
}

bool WaveChannelView::SelectAllText(AudacityProject& project)
{
   return
      AnyAffordance(project, *this, &WaveTrackAffordanceControls::OnTextSelect);
}

std::vector<std::shared_ptr<WaveChannelSubView>>
WaveChannelView::GetAllSubViews()
{
   BuildSubViews();

   std::vector<std::shared_ptr<WaveChannelSubView>> results;
   WaveChannelSubViews::ForEach([&](WaveChannelSubView &subView) {
      results.push_back(std::static_pointer_cast<WaveChannelSubView>(
         subView.shared_from_this()));
   });
   return results;
}

std::shared_ptr<CommonTrackCell> WaveChannelView::GetAffordanceControls()
{
   auto track = FindTrack();
   if (track->IsLeader())
      return DoGetAffordance(track);
   return {};
}

void WaveChannelView::DoSetMinimized(bool minimized)
{
   BuildSubViews();

   // May come here.  Invoke also on sub-views.
   ChannelView::DoSetMinimized(minimized);
   WaveChannelSubViews::ForEach([minimized](WaveChannelSubView &subView) {
      subView.DoSetMinimized(minimized);
   });
}

std::shared_ptr<CommonTrackCell>
WaveChannelView::DoGetAffordance(const std::shared_ptr<Track>& track)
{
    if (mpAffordanceCellControl == nullptr)
        mpAffordanceCellControl = std::make_shared<WaveTrackAffordanceControls>(track);
    return mpAffordanceCellControl;
}

using DoGetWaveChannelView = DoGetView::Override<WaveTrack>;
DEFINE_ATTACHED_VIRTUAL_OVERRIDE(DoGetWaveChannelView) {
   return [](WaveTrack &track, size_t iChannel) {
      assert(iChannel < track.NChannels());
      return std::make_shared<WaveChannelView>(track.SharedPointer(), iChannel);
   };
}

std::shared_ptr<ChannelVRulerControls> WaveChannelView::DoGetVRulerControls()
{
   // This should never be called because of delegation to the spectrum or
   // waveform sub-view
   wxASSERT( false );
   return {};
}

namespace
{
// Returns an offset in seconds to be applied to the right clip
// boundary so that it does not overlap the last sample
double CalculateAdjustmentForZoomLevel(double avgPixPerSecond, bool showSamples)
{
   constexpr double pixelsOffset { 2 }; // The desired offset in pixels
   if (showSamples)
      // adjustment so that the last circular point doesn't appear
      // to be hanging off the end
      return pixelsOffset /
             avgPixPerSecond; // pixels / ( pixels / second ) = seconds
   return .0;
}

double GetBlankSpaceBeforePlayEndTime(const ClipTimes &clip)
{
   return 0.99 * clip.GetStretchRatio() / clip.GetRate();
}

double GetPixelsPerSecond(const wxRect& viewRect, const ZoomInfo& zoomInfo)
{
   const auto h = zoomInfo.PositionToTime(0, 0, true);
   const auto trackRectT1 = zoomInfo.PositionToTime(viewRect.width, 0, true);
   return viewRect.width / (trackRectT1 - h);
}

bool ShowIndividualSamples(
   int sampleRate, double stretchRatio, double pixelsPerSecond)
{
   const auto secondsPerSample = stretchRatio / sampleRate;
   const auto pixelsPerSample = pixelsPerSecond * secondsPerSample;
   return pixelsPerSample > 0.5;
}
}

ClipParameters::ClipParameters(
   const ClipTimes &clip, const wxRect& rect, const ZoomInfo& zoomInfo
)  : trackRectT0 { zoomInfo.PositionToTime(0, 0, true) }
   , averagePixelsPerSecond { GetPixelsPerSecond(rect, zoomInfo) }
   , showIndividualSamples { ShowIndividualSamples(
      clip.GetRate(), clip.GetStretchRatio(), averagePixelsPerSecond) }
{
   const auto trackRectT1 = zoomInfo.PositionToTime(rect.width, 0, true);
   const auto stretchRatio = clip.GetStretchRatio();
   const auto playStartTime = clip.GetPlayStartTime();

   const double clipLength = clip.GetPlayEndTime() - clip.GetPlayStartTime();

   // Hidden duration because too far left.
   const auto tpre = trackRectT0 - playStartTime;
   const auto tpost = trackRectT1 - playStartTime;

   const auto blank = GetBlankSpaceBeforePlayEndTime(clip);

   // Calculate actual selection bounds so that t0 > 0 and t1 < the
   // end of the track
   t0 = std::max(tpre, .0);
   t1 = std::min(tpost, clipLength - blank) +
        CalculateAdjustmentForZoomLevel(
           averagePixelsPerSecond, showIndividualSamples);

   // Make sure t1 (the right bound) is greater than 0
   if (t1 < 0.0) {
      t1 = 0.0;
   }

   // Make sure t1 is greater than t0
   if (t0 > t1) {
      t0 = t1;
   }

   // The variable "hiddenMid" will be the rectangle containing the
   // actual waveform, as opposed to any blank area before
   // or after the track, as it would appear without the fisheye.
   hiddenMid = rect;

   // If the left edge of the track is to the right of the left
   // edge of the display, then there's some unused area to the
   // left of the track.  Reduce the "hiddenMid"
   hiddenLeftOffset = 0;
   if (tpre < 0) {
      // Fix Bug #1296 caused by premature conversion to (int).
      wxInt64 time64 = zoomInfo.TimeToPosition(playStartTime, 0, true);
      if( time64 < 0 )
         time64 = 0;
      hiddenLeftOffset = (time64 < rect.width) ? (int)time64 : rect.width;

      hiddenMid.x += hiddenLeftOffset;
      hiddenMid.width -= hiddenLeftOffset;
   }

   // If the right edge of the track is to the left of the right
   // edge of the display, then there's some unused area to the right
   // of the track.  Reduce the "hiddenMid" rect by the
   // size of the blank area.
   if (tpost > t1) {
      wxInt64 time64 = zoomInfo.TimeToPosition(playStartTime + t1, 0, true);
      if( time64 < 0 )
         time64 = 0;
      const int hiddenRightOffset = (time64 < rect.width) ? (int)time64 : rect.width;

      hiddenMid.width = std::max(0, hiddenRightOffset - hiddenLeftOffset);
   }
   // The variable "mid" will be the rectangle containing the
   // actual waveform, as distorted by the fisheye,
   // as opposed to any blank area before or after the track.
   mid = rect;

   // If the left edge of the track is to the right of the left
   // edge of the display, then there's some unused area to the
   // left of the track.  Reduce the "mid"
   leftOffset = 0;
   if (tpre < 0) {
      wxInt64 time64 = zoomInfo.TimeToPosition(playStartTime, 0, false);
      if( time64 < 0 )
         time64 = 0;
      leftOffset = (time64 < rect.width) ? (int)time64 : rect.width;

      mid.x += leftOffset;
      mid.width -= leftOffset;
   }

   // If the right edge of the track is to the left of the right
   // edge of the display, then there's some unused area to the right
   // of the track.  Reduce the "mid" rect by the
   // size of the blank area.
   if (tpost > t1) {
      wxInt64 time64 = zoomInfo.TimeToPosition(playStartTime + t1, 0, false);
      if( time64 < 0 )
         time64 = 0;
      const int distortedRightOffset = (time64 < rect.width) ? (int)time64 : rect.width;

      mid.width = std::max(0, distortedRightOffset - leftOffset);
   }
}

wxRect ClipParameters::GetClipRect(const ClipTimes& clip,
   const ZoomInfo& zoomInfo, const wxRect& viewRect, bool* outShowSamples)
{
   const auto pixelsPerSecond = GetPixelsPerSecond(viewRect, zoomInfo);
   const auto showIndividualSamples = ShowIndividualSamples(
      clip.GetRate(), clip.GetStretchRatio(), pixelsPerSecond);
   const auto clipEndingAdjustment =
      CalculateAdjustmentForZoomLevel(pixelsPerSecond, showIndividualSamples);
   if (outShowSamples != nullptr)
      *outShowSamples = showIndividualSamples;
   constexpr auto edgeLeft =
      static_cast<ZoomInfo::int64>(std::numeric_limits<int>::min());
   constexpr auto edgeRight =
      static_cast<ZoomInfo::int64>(std::numeric_limits<int>::max());
   const auto left = std::clamp(
      zoomInfo.TimeToPosition(clip.GetPlayStartTime(), viewRect.x, true),
      edgeLeft, edgeRight);
   const auto right = std::clamp(
      zoomInfo.TimeToPosition(
         clip.GetPlayEndTime() - GetBlankSpaceBeforePlayEndTime(clip) +
            clipEndingAdjustment,
         viewRect.x, true),
      edgeLeft, edgeRight);
   if (right >= left)
   {
      // after clamping we can expect that left and right
      // are small enough to be put into int
      return wxRect(
         static_cast<int>(left), viewRect.y,
         std::max(1, static_cast<int>(right - left)), viewRect.height);
    }
    return wxRect();
}

void WaveChannelView::Reparent(const std::shared_ptr<Track> &parent)
{
   // BuildSubViews(); // not really needed
   CommonChannelView::Reparent(parent);
   WaveChannelSubViews::ForEach([&parent](WaveChannelSubView &subView){
      subView.Reparent(parent);
   });
   if (mpAffordanceCellControl)
      mpAffordanceCellControl->Reparent(parent);
}

std::weak_ptr<WaveClip> WaveChannelView::GetSelectedClip()
{
   if (auto affordance = std::dynamic_pointer_cast<WaveTrackAffordanceControls>(GetAffordanceControls()))
   {
      assert(GetChannelIndex() == 0);
      if(auto interval = *affordance->GetSelectedInterval())
      {
         return interval->GetClip(0);
      }
   }
   return {};
}

void WaveChannelView::BuildSubViews() const
{
   if (WaveChannelSubViews::size() == 0) {
      // On-demand steps that can't happen in the constructor
      auto pThis = const_cast<WaveChannelView*>(this);
      pThis->BuildAll();
      bool minimized = GetMinimized();
      pThis->WaveChannelSubViews::ForEach([&](WaveChannelSubView &subView){
         subView.DoSetMinimized(minimized);
      });

      auto &placements = pThis->DoGetPlacements();
      if (placements.empty()) {
         placements.resize(WaveChannelSubViews::size());

         auto pTrack = pThis->FindTrack();
         auto display = TracksPrefs::ViewModeChoice();
         bool multi = (display == WaveChannelViewConstants::MultiView);
         if (multi) {
            pThis->SetMultiView( true );
            display = WaveChannelSubViewType::Default();
         }

         pThis->DoSetDisplay( display, !multi );
      }
   }
}

void WaveChannelView::Draw(
   TrackPanelDrawingContext &context,
   const wxRect &rect, unsigned iPass)
{
   // Should not come here, drawing is now delegated to sub-views
   wxASSERT( false );

   CommonChannelView::Draw(context, rect, iPass);
}

using GetWaveTrackSyncLockPolicy =
   GetSyncLockPolicy::Override< const WaveTrack >;
DEFINE_ATTACHED_VIRTUAL_OVERRIDE(GetWaveTrackSyncLockPolicy) {
   return [](auto &) { return SyncLockPolicy::Grouped; };
}
