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

  Audacity: A Digital Audio Editor

  LabelTrack.cpp

  Dominic Mazzoni
  James Crook
  Jun Wan

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

\class LabelTrack
\brief A LabelTrack is a Track that holds labels (LabelStruct).

These are used to annotate a waveform.
Each label has a start time and an end time.
The text of the labels is editable and the
positions of the end points are draggable.

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

\class LabelStruct
\brief A LabelStruct holds information for ONE label in a LabelTrack

LabelStruct also has label specific functions, mostly functions
for drawing different aspects of the label and its text box.

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

#include "Audacity.h"
#include "LabelTrack.h"
#include "Experimental.h"
#include "TrackPanel.h"

#include <stdio.h>
#include <algorithm>
#include <limits.h>
#include <float.h>

#include <wx/bitmap.h>
#include <wx/brush.h>
#include <wx/clipbrd.h>
#include <wx/dataobj.h>
#include <wx/dc.h>
#include <wx/dcclient.h>
#include <wx/event.h>
#include <wx/intl.h>
#include <wx/log.h>
#include <wx/msgdlg.h>
#include <wx/pen.h>
#include <wx/string.h>
#include <wx/textfile.h>
#include <wx/tokenzr.h>
#include <wx/utils.h>

#include "AudioIO.h"
#include "DirManager.h"
#include "Internat.h"
#include "Prefs.h"
#include "RefreshCode.h"
#include "Theme.h"
#include "AllThemeResources.h"
#include "AColor.h"
#include "Project.h"
#include "TrackArtist.h"
#include "TrackPanel.h"
#include "UndoManager.h"
#include "commands/CommandManager.h"

#include "effects/TimeWarper.h"

enum
{
   OnCutSelectedTextID = 1,      // OSX doesn't like a 0 menu id
   OnCopySelectedTextID,
   OnPasteSelectedTextID,
   OnDeleteSelectedLabelID,
   OnEditSelectedLabelID,
};

wxFont LabelTrack::msFont;

// static member variables.
bool LabelTrack::mbGlyphsReady=false;

/// We have several variants of the icons (highlighting).
/// The icons are draggable, and you can drag one boundary
/// or all boundaries at the same timecode depending on whether you
/// click the centre (for all) or the arrow part (for one).
/// Currently we have twelve variants but we're only using six.
wxBitmap LabelTrack::mBoundaryGlyphs[ NUM_GLYPH_CONFIGS * NUM_GLYPH_HIGHLIGHTS ];
int LabelTrack::mIconHeight;
int LabelTrack::mIconWidth;
int LabelTrack::mTextHeight;

int LabelTrack::mFontHeight=-1;

LabelTrack::Holder TrackFactory::NewLabelTrack()
{
   return std::make_unique<LabelTrack>(mDirManager);
}

LabelTrack::LabelTrack(const std::shared_ptr<DirManager> &projDirManager):
   Track(projDirManager),
   mSelIndex(-1),
   mRestoreFocus(-1),
   mClipLen(0.0),
   miLastLabel(-1)
{
   SetDefaultName(_("Label Track"));
   SetName(GetDefaultName());

   // Label tracks are narrow
   // Default is to allow two rows so that NEW users get the
   // idea that labels can 'stack' when they would overlap.
   SetHeight(73);

   ResetFont();
   CreateCustomGlyphs();

   // reset flags
   ResetFlags();
}

LabelTrack::LabelTrack(const LabelTrack &orig) :
   Track(orig),
   mSelIndex(-1),
   mClipLen(0.0)
{
   for (auto &original: orig.mLabels) {
      LabelStruct l { original.selectedRegion, original.title };
      mLabels.push_back(l);
   }
   mSelIndex = orig.mSelIndex;

   // reset flags
   ResetFlags();
}

LabelTrack::~LabelTrack()
{
}

void LabelTrack::SetOffset(double dOffset)
{
   for (auto &labelStruct: mLabels)
      labelStruct.selectedRegion.move(dOffset);
}

void LabelTrack::Clear(double b, double e)
{
   // May DELETE labels, so use subscripts to iterate
   for (size_t i = 0; i < mLabels.size(); ++i) {
      auto &labelStruct = mLabels[i];
      LabelStruct::TimeRelations relation =
                        labelStruct.RegionRelation(b, e, this);
      if (relation == LabelStruct::BEFORE_LABEL)
         labelStruct.selectedRegion.move(- (e-b));
      else if (relation == LabelStruct::SURROUNDS_LABEL) {
         DeleteLabel( i );
         --i;
      }
      else if (relation == LabelStruct::ENDS_IN_LABEL)
         labelStruct.selectedRegion.setTimes(
            b,
            labelStruct.getT1() - (e - b));
      else if (relation == LabelStruct::BEGINS_IN_LABEL)
         labelStruct.selectedRegion.setT1(b);
      else if (relation == LabelStruct::WITHIN_LABEL)
         labelStruct.selectedRegion.moveT1( - (e-b));
   }
}

#if 0
//used when we want to use clear only on the labels
bool LabelTrack::SplitDelete(double b, double e)
{
   // May DELETE labels, so use subscripts to iterate
   for (size_t i = 0, len = mLabels.size(); i < len; ++i) {
      auto &labelStruct = mLabels[i];
      LabelStruct::TimeRelations relation =
                        labelStruct.RegionRelation(b, e, this);
      if (relation == LabelStruct::SURROUNDS_LABEL) {
         DeleteLabel(i);
         --i;
      }
      else if (relation == LabelStruct::WITHIN_LABEL)
         labelStruct.selectedRegion.moveT1( - (e-b));
      else if (relation == LabelStruct::ENDS_IN_LABEL)
         labelStruct.selectedRegion.setT0(e);
      else if (relation == LabelStruct::BEGINS_IN_LABEL)
         labelStruct.selectedRegion.setT1(b);
   }

   return true;
}
#endif

void LabelTrack::ShiftLabelsOnInsert(double length, double pt)
{
   for (auto &labelStruct: mLabels) {
      LabelStruct::TimeRelations relation =
                        labelStruct.RegionRelation(pt, pt, this);

      if (relation == LabelStruct::BEFORE_LABEL)
         labelStruct.selectedRegion.move(length);
      else if (relation == LabelStruct::WITHIN_LABEL)
         labelStruct.selectedRegion.moveT1(length);
   }
}

void LabelTrack::ChangeLabelsOnReverse(double b, double e)
{
   for (auto &labelStruct: mLabels) {
      if (labelStruct.RegionRelation(b, e, this) ==
                                    LabelStruct::SURROUNDS_LABEL)
      {
         double aux     = b + (e - labelStruct.getT1());
         labelStruct.selectedRegion.setTimes(
            aux,
            e - (labelStruct.getT0() - b));
      }
   }
   SortLabels();
}

void LabelTrack::ScaleLabels(double b, double e, double change)
{
   for (auto &labelStruct: mLabels) {
      labelStruct.selectedRegion.setTimes(
         AdjustTimeStampOnScale(labelStruct.getT0(), b, e, change),
         AdjustTimeStampOnScale(labelStruct.getT1(), b, e, change));
   }
}

double LabelTrack::AdjustTimeStampOnScale(double t, double b, double e, double change)
{
//t is the time stamp we'll be changing
//b and e are the selection start and end

   if (t < b){
      return t;
   }else if (t > e){
      double shift = (e-b)*change - (e-b);
      return t + shift;
   }else{
      double shift = (t-b)*change - (t-b);
      return t + shift;
   }
}

// Move the labels in the track according to the given TimeWarper.
// (If necessary this could be optimised by ignoring labels that occur before a
// specified time, as in most cases they don't need to move.)
void LabelTrack::WarpLabels(const TimeWarper &warper) {
   for (auto &labelStruct: mLabels) {
      labelStruct.selectedRegion.setTimes(
         warper.Warp(labelStruct.getT0()),
         warper.Warp(labelStruct.getT1()));
   }

   // This should not be needed, assuming the warper is nondecreasing, but
   // let's not assume too much.
   SortLabels();
}

void LabelTrack::ResetFlags()
{
   mInitialCursorPos = 1;
   mCurrentCursorPos = 1;
   mRightDragging = false;
   mDrawCursor = false;
}

void LabelTrack::RestoreFlags( const Flags& flags )
{
   mInitialCursorPos = flags.mInitialCursorPos;
   mCurrentCursorPos = flags.mCurrentCursorPos;
   mSelIndex = flags.mSelIndex;
   mRightDragging = flags.mRightDragging;
   mDrawCursor = flags.mDrawCursor;
}

wxFont LabelTrack::GetFont(const wxString &faceName, int size)
{
   wxFontEncoding encoding;
   if (faceName == wxT(""))
      encoding = wxFONTENCODING_DEFAULT;
   else
      encoding = wxFONTENCODING_SYSTEM;
   return wxFont(size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL,
                 wxFONTWEIGHT_NORMAL, false, faceName, encoding);
}

void LabelTrack::ResetFont()
{
   mFontHeight = -1;
   wxString facename = gPrefs->Read(wxT("/GUI/LabelFontFacename"), wxT(""));
   int size = gPrefs->Read(wxT("/GUI/LabelFontSize"), DefaultFontSize);
   msFont = GetFont(facename, size);
}

/// ComputeTextPosition is 'smart' about where to display
/// the label text.
///
/// The text must be displayed between its endpoints x and x1
/// We'd also like it centered between them, and we'd like it on
/// screen.  It isn't always possible to achieve all of this,
/// so we do the best we can.
///
/// This function has a number of tests and adjustments to the
/// text start position.  The tests later in the function will
/// take priority over the ones earlier, so because centering
/// is the first thing we do, it's the first thing we lose if
/// we can't do everything we want to.
void LabelTrack::ComputeTextPosition(const wxRect & r, int index) const
{
   auto &labelStruct = mLabels[index];

   // xExtra is extra space
   // between the text and the endpoints.
   const int xExtra=mIconWidth;
   int x     = labelStruct.x;  // left endpoint
   int x1    = labelStruct.x1; // right endpoint.
   int width = labelStruct.width;

   int xText; // This is where the text will end up.

   // Will the text all fit at this zoom?
   bool bTooWideForScreen = width > (r.width-2*xExtra);
// bool bSimpleCentering = !bTooWideForScreen;
   bool bSimpleCentering = false;

   //TODO (possibly):
   // Add configurable options as to when to use simple
   // and when complex centering.
   //
   // Simple centering does its best to keep the text
   // centered between the label limits.
   //
   // Complex centering positions the text proportionally
   // to how far we are through the label.
   //
   // If we add preferences for this, we want to be able to
   // choose separately whether:
   //   a) Wide text labels centered simple/complex.
   //   b) Other text labels centered simple/complex.
   //

   if( bSimpleCentering )
   {
      // Center text between the two end points.
      xText = (x+x1-width)/2;
   }
   else
   {
      // Calculate xText position to make text line
      // scroll sideways evenly as r moves right.

      // xText is a linear function of r.x.
      // These variables are used to compute that function.
      int rx0,rx1,xText0,xText1;

      // Since we will be using a linear function,
      // we should blend smoothly between left and right
      // aligned text as r, the 'viewport' moves.
      if( bTooWideForScreen )
      {
         rx0=x;           // when viewport at label start.
         xText0=x+xExtra; // text aligned left.
         rx1=x1-r.width;  // when viewport end at label end
         xText1=x1-(width+xExtra); // text aligned right.
      }
      else
      {
         // when label start + width + extra spacing at viewport end..
         rx0=x-r.width+width+2*xExtra;
         // ..text aligned left.
         xText0=x+xExtra;
         // when viewport start + width + extra spacing at label end..
         rx1=x1-(width+2*xExtra);
         // ..text aligned right.
         xText1=x1-(width+xExtra);
      }

      if( rx1 > rx0 ) // Avoid divide by zero case.
      {
         // Compute the blend between left and right aligned.

         // Don't use:
         //
         // xText = xText0 + ((xText1-xText0)*(r.x-rx0))/(rx1-rx0);
         //
         // The problem with the above is that it integer-oveflows at
         // high zoom.

         // Instead use:
         xText = xText0 + (int)((xText1-xText0)*(((float)(r.x-rx0))/(rx1-rx0)));
      }
      else
      {
         // Avoid divide by zero by reverting to
         // simple centering.
         //
         // We could also fall into this case if x and x1
         // are swapped, in which case we'll end up
         // left aligned anyway because code later on
         // will catch that.
         xText = (x+x1-width)/2;
      }
   }

   // Is the text now appearing partly outside r?
   bool bOffLeft = xText < r.x+xExtra;
   bool bOffRight = xText > r.x+r.width-width-xExtra;

   // IF any part of the text is offscreen
   // THEN we may bring it back.
   if( bOffLeft == bOffRight )
   {
      //IF both sides on screen, THEN nothing to do.
      //IF both sides off screen THEN don't do
      //anything about it.
      //(because if we did, you'd never get to read
      //all the text by scrolling).
   }
   else if( bOffLeft != bTooWideForScreen)
   {
      // IF we're off on the left, OR we're
      // too wide for the screen and off on the right
      // (only) THEN align left.
      xText = r.x+xExtra;
   }
   else
   {
      // We're off on the right, OR we're
      // too wide and off on the left (only)
      // SO align right.
      xText =r.x+r.width-width-xExtra;
   }

   // But if we've taken the text out from its endpoints
   // we must move it back so that it's between the endpoints.

   // We test the left end point last because the
   // text might not even fit between the endpoints (at this
   // zoom factor), and in that case we'd like to position
   // the text at the left end point.
   if( xText > (x1-width-xExtra))
      xText=(x1-width-xExtra);
   if( xText < x+xExtra )
      xText=x+xExtra;

   labelStruct.xText = xText;
}

/// ComputeLayout determines which row each label
/// should be placed on, and reserves space for it.
/// Function assumes that the labels are sorted.
void LabelTrack::ComputeLayout(const wxRect & r, const ZoomInfo &zoomInfo) const
{
   int xUsed[MAX_NUM_ROWS];

   int iRow;
   // Rows are the 'same' height as icons or as the text,
   // whichever is taller.
   const int yRowHeight = wxMax(mTextHeight,mIconHeight)+3;// pixels.
   // Extra space at end of rows.
   // We allow space for one half icon at the start and two
   // half icon widths for extra x for the text frame.
   // [we don't allow half a width space for the end icon since it is
   // allowed to be obscured by the text].
   const int xExtra= (3 * mIconWidth)/2;

   const int nRows = wxMin((r.height / yRowHeight) + 1, MAX_NUM_ROWS);
   // Initially none of the rows have been used.
   // So set a value that is less than any valid value.
   {
      // Bug 502: With dragging left of zeros, labels can be in 
      // negative space.  So set least possible value as starting point.
      const int xStart = INT_MIN;
      for (auto &x : xUsed)
         x = xStart;
   }
   int nRowsUsed=0;

   { int i = -1; for (auto &labelStruct : mLabels) { ++i;
      const int x = zoomInfo.TimeToPosition(labelStruct.getT0(), r.x);
      const int x1 = zoomInfo.TimeToPosition(labelStruct.getT1(), r.x);
      int y = r.y;

      labelStruct.x=x;
      labelStruct.x1=x1;
      labelStruct.y=-1;// -ve indicates nothing doing.
      iRow=0;
      // Our first preference is a row that ends where we start.
      // (This is to encourage merging of adjacent label boundaries).
      while( (iRow<nRowsUsed) && (xUsed[iRow] != x ))
         iRow++;
      // IF we didn't find one THEN
      // find any row that can take a span starting at x.
      if( iRow >= nRowsUsed )
      {
         iRow=0;
         while( (iRow<nRows) && (xUsed[iRow] > x ))
            iRow++;
      }
      // IF we found such a row THEN record a valid position.
      if( iRow<nRows )
      {
         // Possibly update the number of rows actually used.
         if( iRow >= nRowsUsed )
            nRowsUsed=iRow+1;
         // Record the position for this label
         y= r.y + iRow * yRowHeight +(yRowHeight/2)+1;
         labelStruct.y=y;
         // On this row we have used up to max of end marker and width.
         // Plus also allow space to show the start icon and
         // some space for the text frame.
         xUsed[iRow]=x+labelStruct.width+xExtra;
         if( xUsed[iRow] < x1 ) xUsed[iRow]=x1;
         ComputeTextPosition( r, i );
      }
   }}
}

LabelStruct::LabelStruct(const SelectedRegion &region,
                         const wxString& aTitle)
: selectedRegion(region)
, title(aTitle)
{
   updated = false;
   width = 0;
   x = 0;
   x1 = 0;
   xText = 0;
   y = 0;
}

LabelStruct::LabelStruct(const SelectedRegion &region,
                         double t0, double t1,
                         const wxString& aTitle)
: selectedRegion(region)
, title(aTitle)
{
   // Overwrite the times
   selectedRegion.setTimes(t0, t1);

   updated = false;
   width = 0;
   x = 0;
   x1 = 0;
   xText = 0;
   y = 0;
}

/// Draw vertical lines that go exactly through the position
/// of the start or end of a label.
///   @param  dc the device context
///   @param  r  the LabelTrack rectangle.
void LabelStruct::DrawLines(wxDC & dc, const wxRect & r) const
{
   // How far out from the centre line should the vertical lines
   // start, i.e. what is the y position of the icon?
   // We adjust this so that the line encroaches on the icon
   // slightly (there is white space in the design).
   const int yIconStart = y - (LabelTrack::mIconHeight /2)+1+(LabelTrack::mTextHeight+3)/2;
   const int yIconEnd   = yIconStart + LabelTrack::mIconHeight-2;

   // If y is positive then it is the center line for the
   // Label.
   if( y >= 0 )
   {
      if((x  >= r.x) && (x  <= (r.x+r.width)))
      {
         // Draw line above and below left dragging widget.
         AColor::Line(dc, x, r.y,  x, yIconStart - 1);
         AColor::Line(dc, x, yIconEnd, x, r.y + r.height);
      }
      if((x1 >= r.x) && (x1 <= (r.x+r.width)))
      {
         // Draw line above and below right dragging widget.
         AColor::Line(dc, x1, r.y,  x1, yIconStart - 1);
         AColor::Line(dc, x1, yIconEnd, x1, r.y + r.height);
      }
   }
   else
   {
      // Draw the line, even though the widget is off screen
      AColor::Line(dc, x, r.y,  x, r.y + r.height);
      AColor::Line(dc, x1, r.y,  x1, r.y + r.height);
   }
}

/// DrawGlyphs draws the wxIcons at the start and end of a label.
///   @param  dc the device context
///   @param  r  the LabelTrack rectangle.
void LabelStruct::DrawGlyphs
   (wxDC & dc, const wxRect & r, int GlyphLeft, int GlyphRight) const
{
   const int xHalfWidth=LabelTrack::mIconWidth/2;
   const int yStart=y-LabelTrack::mIconHeight/2+(LabelTrack::mTextHeight+3)/2;

   // If y == -1, nothing to draw
   if( y == -1 )
      return;

   if((x  >= r.x) && (x  <= (r.x+r.width)))
      dc.DrawBitmap(LabelTrack::GetGlyph(GlyphLeft), x-xHalfWidth,yStart, true);
   // The extra test commented out here would suppress right hand markers
   // when they overlap the left hand marker (e.g. zoomed out) or to the left.
   if((x1 >= r.x) && (x1 <= (r.x+r.width)) /*&& (x1>x+LabelTrack::mIconWidth)*/)
      dc.DrawBitmap(LabelTrack::GetGlyph(GlyphRight), x1-xHalfWidth,yStart, true);
}

/// Draw the text of the label and also draw
/// a long thin rectangle for its full extent
/// from x to x1 and a rectangular frame
/// behind the text itself.
///   @param  dc the device context
///   @param  r  the LabelTrack rectangle.
void LabelStruct::DrawText(wxDC & dc, const wxRect & r) const
{
   //If y is positive then it is the center line for the
   //text we are about to draw.
   //if it isn't, nothing to draw.

   if( y == -1 )
      return;

   // Draw frame for the text...
   // We draw it half an icon width left of the text itself.
   {
      const int xStart=wxMax(r.x,xText-LabelTrack::mIconWidth/2);
      const int xEnd=wxMin(r.x+r.width,xText+width+LabelTrack::mIconWidth/2);
      const int xWidth = xEnd-xStart;

      if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
      {
         // Now draw the text itself.
         dc.DrawText(title, xText, y-LabelTrack::mTextHeight/2);
      }
   }

}

void LabelStruct::DrawTextBox(wxDC & dc, const wxRect & r) const
{
   //If y is positive then it is the center line for the
   //text we are about to draw.
   const int yBarHeight=3;
   const int yFrameHeight = LabelTrack::mTextHeight+3;
   const int xBarShorten  = LabelTrack::mIconWidth+4;
   if( y == -1 )
      return;

   {
      const int xStart=wxMax(r.x,x+xBarShorten/2);
      const int xEnd=wxMin(r.x+r.width,x1-xBarShorten/2);
      const int xWidth = xEnd-xStart;

      if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
      {

         wxRect bar( xStart,y-yBarHeight/2+yFrameHeight/2,
            xWidth,yBarHeight);
         if( x1 > x+xBarShorten )
            dc.DrawRectangle(bar);
      }
   }

   // In drawing the bar and the frame, we compute the clipping
   // to the viewport ourselves.  Under Win98 the GDI does its
   // calculations in 16 bit arithmetic, and so gets it completely
   // wrong at higher zooms where the bar can easily be
   // more than 65536 pixels wide.

   // Draw bar for label extent...
   // We don't quite draw from x to x1 because we allow
   // half an icon width at each end.
   {
      const int xStart=wxMax(r.x,xText-LabelTrack::mIconWidth/2);
      const int xEnd=wxMin(r.x+r.width,xText+width+LabelTrack::mIconWidth/2);
      const int xWidth = xEnd-xStart;

      if( (xStart < (r.x+r.width)) && (xEnd > r.x) && (xWidth>0))
      {
          wxRect frame(
            xStart,y-yFrameHeight/2,
            xWidth,yFrameHeight );
         dc.DrawRectangle(frame);
      }
   }
}

/// Draws text-selected region within the label
void LabelStruct::DrawHighlight
   ( wxDC & dc, int xPos1, int xPos2, int charHeight) const
{
   wxPen curPen = dc.GetPen();
   curPen.SetColour(wxString(wxT("BLUE")));
   wxBrush curBrush = dc.GetBrush();
   curBrush.SetColour(wxString(wxT("BLUE")));
   if (xPos1 < xPos2)
      dc.DrawRectangle(xPos1-1, y-charHeight/2, xPos2-xPos1+1, charHeight);
   else
      dc.DrawRectangle(xPos2-1, y-charHeight/2, xPos1-xPos2+1, charHeight);
}

void LabelStruct::getXPos( wxDC & dc, int * xPos1, int cursorPos) const
{
   *xPos1 = xText;
   if( cursorPos > 0)
   {
      int partWidth;
      // Calculate the width of the substring and add it to Xpos
      dc.GetTextExtent(title.Left(cursorPos), &partWidth, NULL);
      *xPos1 += partWidth;
   }
}

bool LabelTrack::CalcCursorX(int * x) const
{
   if (mSelIndex >= 0) {
      wxMemoryDC dc;

      if (msFont.Ok()) {
         dc.SetFont(msFont);
      }

      mLabels[mSelIndex].getXPos(dc, x, mCurrentCursorPos);
      *x += LabelTrack::mIconWidth / 2;
      return true;
   }

   return false;
}

void LabelTrack::CalcHighlightXs(int *x1, int *x2) const
{
   wxMemoryDC dc;

   if (msFont.Ok()) {
      dc.SetFont(msFont);
   }

   int pos1 = mInitialCursorPos, pos2 = mCurrentCursorPos;
   if (pos1 > pos2)
      std::swap(pos1, pos2);

   const auto &labelStruct = mLabels[mSelIndex];

   // find the left X pos of highlighted area
   labelStruct.getXPos(dc, x1, pos1);
   // find the right X pos of highlighted area
   labelStruct.getXPos(dc, x2, pos2);
}

#include "tracks/labeltrack/ui/LabelGlyphHandle.h"
// TODO:  don't rely on the global ::GetActiveProject() to find this.
// Rather, give TrackPanelCell a drawing function and pass context into it.
namespace {
   LabelTrackHit *findHit()
   {
      // Fetch the highlighting state
      auto target = GetActiveProject()->GetTrackPanel()->Target();
      if (target) {
         auto handle = dynamic_cast<LabelGlyphHandle*>( target.get() );
         if (handle)
            return &handle->mHit;
      }
      return nullptr;
   }
}

#include "TrackPanelDrawingContext.h"
#include "tracks/labeltrack/ui/LabelTextHandle.h"

/// Draw calls other functions to draw the LabelTrack.
///   @param  dc the device context
///   @param  r  the LabelTrack rectangle.
void LabelTrack::Draw
(TrackPanelDrawingContext &context, const wxRect & r,
 const SelectedRegion &selectedRegion,
 const ZoomInfo &zoomInfo) const
{
   auto &dc = context.dc;
   auto pHit = findHit();

   if(msFont.Ok())
      dc.SetFont(msFont);

   if (mFontHeight == -1)
      calculateFontHeight(dc);

   TrackArtist::DrawBackgroundWithSelection(&dc, r, this,
         AColor::labelSelectedBrush, AColor::labelUnselectedBrush,
         selectedRegion, zoomInfo);

   wxCoord textWidth, textHeight;

   // Get the text widths.
   // TODO: Make more efficient by only re-computing when a
   // text label title changes.
   for (auto &labelStruct : mLabels) {
      dc.GetTextExtent(labelStruct.title, &textWidth, &textHeight);
      labelStruct.width = textWidth;
   }

   // TODO: And this only needs to be done once, but we
   // do need the dc to do it.
   // We need to set mTextHeight to something sensible,
   // guarding against the case where there are no
   // labels or all are empty strings, which for example
   // happens with a NEW label track.
   dc.GetTextExtent(wxT("Demo Text x^y"), &textWidth, &textHeight);
   mTextHeight = (int)textHeight;
   ComputeLayout( r, zoomInfo );
   dc.SetTextForeground(theTheme.Colour( clrLabelTrackText));
   dc.SetBackgroundMode(wxTRANSPARENT);
   dc.SetBrush(AColor::labelTextNormalBrush);
   dc.SetPen(AColor::labelSurroundPen);
   int GlyphLeft;
   int GlyphRight;
   // Now we draw the various items in this order,
   // so that the correct things overpaint each other.

   // Draw vertical lines that show where the end positions are.
   for (auto &labelStruct : mLabels)
      labelStruct.DrawLines( dc, r );

   // Draw the end glyphs.
   { int i = -1; for (auto &labelStruct : mLabels) { ++i;
      GlyphLeft=0;
      GlyphRight=1;
      if( pHit && i == pHit->mMouseOverLabelLeft )
         GlyphLeft = (pHit->mEdge & 4) ? 6:9;
      if( pHit && i == pHit->mMouseOverLabelRight )
         GlyphRight = (pHit->mEdge & 4) ? 7:4;
      labelStruct.DrawGlyphs( dc, r, GlyphLeft, GlyphRight );
   }}

   // Draw the label boxes.
   {
      bool highlightTrack = false;
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
      auto target = dynamic_cast<LabelTextHandle*>(context.target.get());
      highlightTrack = target && target->GetTrack().get() == this;
#endif
      int i = -1; for (auto &labelStruct : mLabels) { ++i;
         bool highlight = false;
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
         highlight = highlightTrack && target->GetLabelNum() == i;
#endif
         bool selected = mSelIndex == i;

         if( selected )
            dc.SetBrush( AColor::labelTextEditBrush );
         else if ( highlight )
            dc.SetBrush( AColor::uglyBrush );
         labelStruct.DrawTextBox( dc, r );

         if (highlight || selected)
            dc.SetBrush(AColor::labelTextNormalBrush);
      }
   }

   // Draw highlights
   if ((mInitialCursorPos != mCurrentCursorPos) && (mSelIndex >= 0 ))
   {
      int xpos1, xpos2;
      CalcHighlightXs(&xpos1, &xpos2);
      mLabels[mSelIndex].DrawHighlight(dc, xpos1, xpos2, mFontHeight);
   }

   // Draw the text and the label boxes.
   { int i = -1; for (auto &labelStruct : mLabels) { ++i;
      if( mSelIndex==i)
         dc.SetBrush(AColor::labelTextEditBrush);
      labelStruct.DrawText( dc, r );
      if( mSelIndex==i)
         dc.SetBrush(AColor::labelTextNormalBrush);
   }}

   // Draw the cursor, if there is one.
   if( mDrawCursor && mSelIndex >=0 )
   {
      const auto &labelStruct = mLabels[mSelIndex];
      int xPos = labelStruct.xText;

      if( mCurrentCursorPos > 0)
      {
         // Calculate the width of the substring and add it to Xpos
         int partWidth;
         dc.GetTextExtent(labelStruct.title.Left(mCurrentCursorPos), &partWidth, NULL);
         xPos += partWidth;
      }

      wxPen currentPen = dc.GetPen();
      const int CursorWidth=2;
      currentPen.SetWidth(CursorWidth);
      AColor::Line(dc,
                   xPos-1, labelStruct.y - mFontHeight/2 + 1,
                   xPos-1, labelStruct.y + mFontHeight/2 - 1);
      currentPen.SetWidth(1);
   }
}

/// uses GetTextExtent to find the character position
/// corresponding to the x pixel position.
int LabelTrack::FindCurrentCursorPosition(int xPos)
{
   int result = -1;
   wxMemoryDC dc;
   if(msFont.Ok())
      dc.SetFont(msFont);

   // A bool indicator to see if set the cursor position or not
   bool finished = false;
   int charIndex = 1;
   int partWidth;
   int oneWidth;
   double bound;
   wxString subString;
   const auto &labelStruct = mLabels[mSelIndex];
   const auto &title = labelStruct.title;
   const int length = title.length();
   while (!finished && (charIndex < length + 1))
   {
      subString = title.Left(charIndex);
      // Get the width of substring
      dc.GetTextExtent(subString, &partWidth, NULL);

      // Get the width of the last character
      dc.GetTextExtent(subString.Right(1), &oneWidth, NULL);
      bound = labelStruct.xText + partWidth - oneWidth * 0.5;

      if (xPos <= bound)
      {
         // Found
         result = charIndex - 1;
         finished = true;
      }
      else
      {
         // Advance
         charIndex++;
      }
   }
   if (!finished)
      // Cursor should be in the last position
      result = length;

   return result;
}

/// Set the cursor position according to x position of mouse
void LabelTrack::SetCurrentCursorPosition(int xPos)
{
   mCurrentCursorPos = FindCurrentCursorPosition(xPos);
}

void LabelTrack::calculateFontHeight(wxDC & dc) const
{
   int charDescent;
   int charLeading;

   // Calculate the width of the substring and add it to Xpos
   dc.GetTextExtent(wxT("(Test String)|[yp]"), NULL, &mFontHeight, &charDescent, &charLeading);

   // The cursor will have height charHeight.  We don't include the descender as
   // part of the height because for phonetic fonts this leads to cursors which are
   // too tall.  We don't include leading either - it is usually 0.
   // To make up for ignoring the descender height, we add one pixel above and below
   // using CursorExtraHeight so that the cursor is just a little taller than the
   // body of the characters.
   const int CursorExtraHeight=2;
   mFontHeight += CursorExtraHeight - (charLeading+charDescent);
}

bool LabelTrack::IsTextSelected()
{
   if (mSelIndex == -1)
      return false;
   if (mCurrentCursorPos == mInitialCursorPos)
      return false;
   return true;
}

/// Cut the selected text in the text box
///  @return true if text is selected in text box, false otherwise
bool LabelTrack::CutSelectedText()
{
   if (!IsTextSelected())
      return false;

   wxString left, right;
   auto &labelStruct = mLabels[mSelIndex];
   auto &text = labelStruct.title;

   int init = mInitialCursorPos;
   int cur = mCurrentCursorPos;
   if (init > cur)
      std::swap(init, cur);

   // data for cutting
   wxString data = text.Mid(init, cur - init);

   // get left-remaining text
   if (init > 0)
      left = text.Left(init);

   // get right-remaining text
   if (cur < (int)text.Length())
      right = text.Mid(cur);

   // set title to the combination of the two remainders
   text = left + right;

   // copy data onto clipboard
   if (wxTheClipboard->Open()) {
      // Clipboard owns the data you give it
      wxTheClipboard->SetData(safenew wxTextDataObject(data));
      wxTheClipboard->Close();
   }

   // set cursor positions
   mInitialCursorPos = mCurrentCursorPos = left.Length();
   return true;
}

/// Copy the selected text in the text box
///  @return true if text is selected in text box, false otherwise
bool LabelTrack::CopySelectedText()
{
   if (mSelIndex == -1)
      return false;

   const auto &labelStruct = mLabels[mSelIndex];

   int init = mInitialCursorPos;
   int cur = mCurrentCursorPos;
   if (init > cur)
      std::swap(init, cur);

   if (init == cur)
      return false;

   // data for copying
   wxString data = labelStruct.title.Mid(init, cur-init);

   // copy the data on clipboard
   if (wxTheClipboard->Open()) {
      // Clipboard owns the data you give it
      wxTheClipboard->SetData(safenew wxTextDataObject(data));
      wxTheClipboard->Close();
   }

   return true;
}

// PRL:  should this set other fields of the label selection?
/// Paste the text on the clipboard to text box
///  @return true if mouse is clicked in text box, false otherwise
bool LabelTrack::PasteSelectedText(double sel0, double sel1)
{
   if (mSelIndex == -1)
      AddLabel(SelectedRegion(sel0, sel1), wxT(""));

   wxString text, left, right;

   // if text data is available
   if (IsTextClipSupported()) {
      if (wxTheClipboard->Open()) {
         wxTextDataObject data;
         wxTheClipboard->GetData(data);
         wxTheClipboard->Close();
         text = data.GetText();
      }

      // Convert control characters to blanks
      for (int i = 0; i < (int)text.Length(); i++) {
         if (wxIscntrl(text[i])) {
            text[i] = wxT(' ');
         }
      }
   }

   auto &labelStruct = mLabels[mSelIndex];
   auto &title = labelStruct.title;
   int cur = mCurrentCursorPos, init = mInitialCursorPos;
   if (init > cur)
      std::swap(init, cur);
   left = title.Left(init);
   if (cur < (int)title.Length())
      right = title.Mid(cur);

   title = left + text + right;
   mInitialCursorPos =  mCurrentCursorPos = left.Length() + text.Length();
   return true;
}


/// @return true if the text data is available in the clipboard, false otherwise
bool LabelTrack::IsTextClipSupported()
{
   return wxTheClipboard->IsSupported(wxDF_TEXT);
}


double LabelTrack::GetOffset() const
{
   return mOffset;
}

double LabelTrack::GetStartTime() const
{
   if (mLabels.empty())
      return 0.0;
   else
      return mLabels[0].getT0();
}

double LabelTrack::GetEndTime() const
{
   //we need to scan through all the labels, because the last
   //label might not have the right-most end (if there is overlap).
   if (mLabels.empty())
      return 0.0;

   double end = 0.0;
   for (auto &labelStruct: mLabels) {
      const double t1 = labelStruct.getT1();
      if(t1 > end)
         end = t1;
   }
   return end;
}

Track::Holder LabelTrack::Duplicate() const
{
   return std::make_unique<LabelTrack>( *this );
}

void LabelTrack::SetSelected(bool s)
{
   Track::SetSelected(s);
   if (!s)
      Unselect();
}

/// TODO: Investigate what happens with large
/// numbers of labels, might need a binary search
/// rather than a linear one.
void LabelTrack::OverGlyph(LabelTrackHit &hit, int x, int y) const
{
   //Determine the NEW selection.
   int result=0;
   const int d1=10; //distance in pixels, used for have we hit drag handle.
   const int d2=5;  //distance in pixels, used for have we hit drag handle center.

   //If not over a label, reset it
   hit.mMouseOverLabelLeft  = -1;
   hit.mMouseOverLabelRight = -1;
   hit.mEdge = 0;
   { int i = -1; for (auto &labelStruct : mLabels) { ++i;
      //over left or right selection bound
      //Check right bound first, since it is drawn after left bound,
      //so give it precedence for matching/highlighting.
      if( abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
               abs(labelStruct.x1 - d2 -x) < d1)
      {
         hit.mMouseOverLabelRight = i;
         if(abs(labelStruct.x1 - x) < d2 )
         {
            result |= 4;
            // If left and right co-incident at this resolution, then we drag both.
            // We could be a little less stringent about co-incidence here if we liked.
            if( abs(labelStruct.x1-labelStruct.x) < 1.0 )
            {
               result |=1;
               hit.mMouseOverLabelLeft = i;
            }
         }
         result |= 2;
      }
      // Use else-if here rather than else to avoid detecting left and right
      // of the same label.
      else if(   abs(labelStruct.y - (y - (LabelTrack::mTextHeight+3)/2)) < d1 &&
            abs(labelStruct.x + d2 - x) < d1 )
      {
         hit.mMouseOverLabelLeft = i;
         if(abs(labelStruct.x - x) < d2 )
            result |= 4;
         result |= 1;
      }

      // give text box better priority for selecting
      if(OverTextBox(&labelStruct, x, y))
      {
         result = 0;
      }

   }}
   hit.mEdge = result;
}

int LabelTrack::OverATextBox(int xx, int yy) const
{
   for (int nn = (int)mLabels.size(); nn--;) {
      const auto &labelStruct = mLabels[nn];
      if (OverTextBox(&labelStruct, xx, yy))
         return nn;
   }

   return -1;
}

// return true if the mouse is over text box, false otherwise
bool LabelTrack::OverTextBox(const LabelStruct *pLabel, int x, int y) const
{
   if( (pLabel->xText-(mIconWidth/2) < x) &&
            (x<pLabel->xText+pLabel->width+(mIconWidth/2)) &&
            (abs(pLabel->y-y)<mIconHeight/2))
   {
      return true;
   }
   return false;
}

// Adjust label's left or right boundary, depending which is requested.
// Return true iff the label flipped.
bool LabelStruct::AdjustEdge( int iEdge, double fNewTime)
{
   updated = true;
   if( iEdge < 0 )
      return selectedRegion.setT0(fNewTime);
   else
      return selectedRegion.setT1(fNewTime);
}

// We're moving the label.  Adjust both left and right edge.
void LabelStruct::MoveLabel( int iEdge, double fNewTime)
{
   double fTimeSpan = getDuration();

   if( iEdge < 0 )
   {
      selectedRegion.setTimes(fNewTime, fNewTime+fTimeSpan);
   }
   else
   {
      selectedRegion.setTimes(fNewTime-fTimeSpan, fNewTime);
   }
   updated = true;
}

LabelStruct LabelStruct::Import(wxTextFile &file, int &index)
{
   SelectedRegion sr;
   wxString title;
   static const wxString continuation{ wxT("\\") };

   wxString firstLine = file.GetLine(index++);

   {
      // Assume tab is an impossible character within the exported text
      // of the label, so can be only a delimiter.  But other white space may
      // be part of the label text.
      wxStringTokenizer toker { firstLine, wxT("\t") };

      //get the timepoint of the left edge of the label.
      auto token = toker.GetNextToken();

      double t0;
      if (!Internat::CompatibleToDouble(token, &t0))
         throw BadFormatException{};

      token = toker.GetNextToken();

      double t1;
      if (!Internat::CompatibleToDouble(token, &t1))
         //s1 is not a number.
         t1 = t0;  //This is a one-sided label; t1 == t0.
      else
         token = toker.GetNextToken();

      sr.setTimes( t0, t1 );
      
      title = token;
   }

   // Newer selection fields are written on additional lines beginning with
   // '\' which is an impossible numerical character that older versions of
   // audacity will ignore.  Test for the presence of such a line and then
   // parse it if we can.

   // There may also be additional continuation lines from future formats that
   // we ignore.

   // Advance index over all continuation lines first, before we might throw
   // any exceptions.
   int index2 = index;
   while (index < file.GetLineCount() &&
          file.GetLine(index).StartsWith(continuation))
      ++index;

   if (index2 < index) {
      wxStringTokenizer toker { file.GetLine(index2++), wxT("\t") };
      auto token = toker.GetNextToken();
      if (token != continuation)
         throw BadFormatException{};

      token = toker.GetNextToken();
      double f0;
      if (!Internat::CompatibleToDouble(token, &f0))
         throw BadFormatException{};

      token = toker.GetNextToken();
      double f1;
      if (!Internat::CompatibleToDouble(token, &f1))
         throw BadFormatException{};

      sr.setFrequencies(f0, f1);
   }

   return LabelStruct{ sr, title };
}

void LabelStruct::Export(wxTextFile &file) const
{
   file.AddLine(wxString::Format(wxT("%s\t%s\t%s"),
      Internat::ToString(getT0(), FLT_DIG).c_str(),
      Internat::ToString(getT1(), FLT_DIG).c_str(),
      title.c_str()
   ));

   // Do we need more lines?
   auto f0 = selectedRegion.f0();
   auto f1 = selectedRegion.f1();
   if (f0 == SelectedRegion::UndefinedFrequency &&
       f1 == SelectedRegion::UndefinedFrequency)
      return;

   // Write a \ character at the start of a second line,
   // so that earlier versions of Audacity ignore it.
   file.AddLine(wxString::Format(wxT("\\\t%s\t%s"),
      Internat::ToString(f0, FLT_DIG).c_str(),
      Internat::ToString(f1, FLT_DIG).c_str()
   ));

   // Additional lines in future formats should also start with '\'.
}

auto LabelStruct::RegionRelation(
      double reg_t0, double reg_t1, const LabelTrack * WXUNUSED(parent)) const
-> TimeRelations
{
   bool retainLabels = false;

   wxASSERT(reg_t0 <= reg_t1);
   gPrefs->Read(wxT("/GUI/RetainLabels"), &retainLabels);

   if(retainLabels) {

      // Desired behavior for edge cases: The length of the selection is smaller
      // than the length of the label if the selection is within the label or
      // matching exactly a (region) label.

      if (reg_t0 < getT0() && reg_t1 > getT1())
         return SURROUNDS_LABEL;
      else if (reg_t1 < getT0())
         return BEFORE_LABEL;
      else if (reg_t0 > getT1())
         return AFTER_LABEL;

      else if (reg_t0 >= getT0() && reg_t0 <= getT1() &&
               reg_t1 >= getT0() && reg_t1 <= getT1())
         return WITHIN_LABEL;

      else if (reg_t0 >= getT0() && reg_t0 <= getT1())
         return BEGINS_IN_LABEL;
      else
         return ENDS_IN_LABEL;

   } else {

      // AWD: Desired behavior for edge cases: point labels bordered by the
      // selection are included within it. Region labels are included in the
      // selection to the extent that the selection covers them; specifically,
      // they're not included at all if the selection borders them, and they're
      // fully included if the selection covers them fully, even if it just
      // borders their endpoints. This is just one of many possible schemes.

      // The first test catches bordered point-labels and selected-through
      // region-labels; move it to third and selection edges become inclusive
      // WRT point-labels.
      if (reg_t0 <= getT0() && reg_t1 >= getT1())
         return SURROUNDS_LABEL;
      else if (reg_t1 <= getT0())
         return BEFORE_LABEL;
      else if (reg_t0 >= getT1())
         return AFTER_LABEL;

      // At this point, all point labels should have returned.

      else if (reg_t0 > getT0() && reg_t0 < getT1() &&
               reg_t1 > getT0() && reg_t1 < getT1())
         return WITHIN_LABEL;

      // Knowing that none of the other relations match simplifies remaining
      // tests
      else if (reg_t0 > getT0() && reg_t0 < getT1())
         return BEGINS_IN_LABEL;
      else
         return ENDS_IN_LABEL;

   }
}

/// If the index is for a real label, adjust its left or right boundary.
/// @iLabel - index of label, -1 for none.
/// @iEdge - which edge is requested to move, -1 for left +1 for right.
/// @bAllowSwapping - if we can switch which edge is being dragged.
/// fNewTime - the NEW time for this edge of the label.
void LabelTrack::MayAdjustLabel
( LabelTrackHit &hit, int iLabel, int iEdge, bool bAllowSwapping, double fNewTime)
{
   if( iLabel < 0 )
      return;
   LabelStruct &labelStruct = mLabels[ iLabel ];

   // Adjust the requested edge.
   bool flipped = labelStruct.AdjustEdge( iEdge, fNewTime );
   // If the edges did not swap, then we are done.
   if( ! flipped )
      return;

   // If swapping's not allowed we must also move the edge
   // we didn't move.  Then we're done.
   if( !bAllowSwapping )
   {
      labelStruct.AdjustEdge( -iEdge, fNewTime );
      return;
   }

   // Swap our record of what we are dragging.
   std::swap( hit.mMouseOverLabelLeft, hit.mMouseOverLabelRight );
}

// If the index is for a real label, adjust its left and right boundary.
void LabelTrack::MayMoveLabel( int iLabel, int iEdge, double fNewTime)
{
   if( iLabel < 0 )
      return;
   mLabels[ iLabel ].MoveLabel( iEdge, fNewTime );
}

// Constrain function, as in processing/arduino.
// returned value will be between min and max (inclusive).
static int Constrain( int value, int min, int max )
{
   wxASSERT( min <= max );
   int result=value;
   if( result < min )
      result=min;
   if( result > max )
      result=max;
   return result;
}

bool LabelTrack::HandleGlyphDragRelease
(LabelTrackHit &hit, const wxMouseEvent & evt,
 wxRect & r, const ZoomInfo &zoomInfo,
 SelectedRegion *newSel)
{
   if(evt.LeftUp())
   {
      bool lupd = false, rupd = false;
      if( hit.mMouseOverLabelLeft >= 0 ) {
         auto &labelStruct = mLabels[ hit.mMouseOverLabelLeft ];
         lupd = labelStruct.updated;
         labelStruct.updated = false;
      }
      if( hit.mMouseOverLabelRight >= 0 ) {
         auto &labelStruct = mLabels[ hit.mMouseOverLabelRight ];
         rupd = labelStruct.updated;
         labelStruct.updated = false;
      }

      hit.mIsAdjustingLabel = false;
      hit.mMouseOverLabelLeft  = -1;
      hit.mMouseOverLabelRight = -1;
      return lupd || rupd;
   }

   if(evt.Dragging())
   {
      //If we are currently adjusting a label,
      //just reset its value and redraw.
      // LL:  Constrain to inside track rectangle for now.  Should be changed
      //      to allow scrolling while dragging labels
      int x = Constrain( evt.m_x + mxMouseDisplacement - r.x, 0, r.width);

      // If exactly one edge is selected we allow swapping
      bool bAllowSwapping =
         ( hit.mMouseOverLabelLeft >=0 ) !=
         ( hit.mMouseOverLabelRight >= 0);
      // If we're on the 'dot' and nowe're moving,
      // Though shift-down inverts that.
      // and if both edges the same, then we're always moving the label.
      bool bLabelMoving = hit.mbIsMoving;
      bLabelMoving ^= evt.ShiftDown();
      bLabelMoving |= ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight );
      double fNewX = zoomInfo.PositionToTime(x, 0);
      if( bLabelMoving )
      {
         MayMoveLabel( hit.mMouseOverLabelLeft,  -1, fNewX );
         MayMoveLabel( hit.mMouseOverLabelRight, +1, fNewX );
      }
      else
      {
         MayAdjustLabel( hit, hit.mMouseOverLabelLeft,  -1, bAllowSwapping, fNewX );
         MayAdjustLabel( hit, hit.mMouseOverLabelRight, +1, bAllowSwapping, fNewX );
      }

      if( mSelIndex >=0 )
      {
         //Set the selection region to be equal to
         //the NEW size of the label.
         *newSel = mLabels[mSelIndex].selectedRegion;
      }
      SortLabels( &hit );
   }

   return false;
}

void LabelTrack::HandleTextDragRelease(const wxMouseEvent & evt)
{
   if(evt.LeftUp())
   {
#if 0
      // AWD: Due to wxWidgets bug #7491 (fix not ported to 2.8 branch) we
      // should never write the primary selection. We can enable this block
      // when we move to the 3.0 branch (or if a fixed 2.8 version is released
      // and we can do a runtime version check)
#if defined (__WXGTK__) && defined (HAVE_GTK)
      // On GTK, if we just dragged out a text selection, set the primary
      // selection
      if (mInitialCursorPos != mCurrentCursorPos) {
         wxTheClipboard->UsePrimarySelection(true);
         CopySelectedText();
         wxTheClipboard->UsePrimarySelection(false);
      }
#endif
#endif

      return;
   }

   if(evt.Dragging())
   {
      if (!mRightDragging)
         // Update drag end
         SetCurrentCursorPosition(evt.m_x);

      return;
   }

   if (evt.RightUp()) {
      if ((mSelIndex != -1) && OverTextBox(GetLabel(mSelIndex), evt.m_x, evt.m_y)) {
         // popup menu for editing
         ShowContextMenu();
      }
   }

   return;
}

void LabelTrack::HandleGlyphClick
(LabelTrackHit &hit, const wxMouseEvent & evt,
 const wxRect & r, const ZoomInfo &zoomInfo,
 SelectedRegion *newSel)
{
   if (evt.ButtonDown())
   {
      //OverGlyph sets mMouseOverLabel to be the chosen label.
      OverGlyph(hit, evt.m_x, evt.m_y);
      hit.mIsAdjustingLabel = evt.Button(wxMOUSE_BTN_LEFT) &&
         ( hit.mEdge & 3 ) != 0;

      if (hit.mIsAdjustingLabel)
      {
         float t = 0.0;
         // We move if we hit the centre, we adjust one edge if we hit a chevron.
         // This is if we are moving just one edge.
         hit.mbIsMoving = (hit.mEdge & 4)!=0;
         // When we start dragging the label(s) we don't want them to jump.
         // so we calculate the displacement of the mouse from the drag center
         // and use that in subsequent dragging calculations.  The mouse stays
         // at the same relative displacement throughout dragging.

         // However, if two label's edges are being dragged
         // then the displacement is relative to the initial average
         // position of them, and in that case there can be a jump of at most
         // a few pixels to bring the two label boundaries to exactly the same
         // position when we start dragging.

         // Dragging of three label edges at the same time is not supported (yet).
         if( ( hit.mMouseOverLabelRight >= 0 ) &&
             ( hit.mMouseOverLabelLeft >= 0 )
           )
         {
            t = (mLabels[ hit.mMouseOverLabelRight ].getT1() +
                 mLabels[ hit.mMouseOverLabelLeft ].getT0()) / 2.0f;
            // If we're moving two edges, then it's a move (label size preserved)
            // if both edges are the same label, and it's an adjust (label sizes change)
            // if we're on a boundary between two different labels.
            hit.mbIsMoving =
               ( hit.mMouseOverLabelLeft == hit.mMouseOverLabelRight );
         }
         else if( hit.mMouseOverLabelRight >=0)
         {
            t = mLabels[ hit.mMouseOverLabelRight ].getT1();
         }
         else if( hit.mMouseOverLabelLeft >=0)
         {
            t = mLabels[ hit.mMouseOverLabelLeft ].getT0();
         }
         mxMouseDisplacement = zoomInfo.TimeToPosition(t, r.x) - evt.m_x;
      }
   }
}

void LabelTrack::HandleTextClick(const wxMouseEvent & evt,
   const wxRect & r, const ZoomInfo &zoomInfo,
   SelectedRegion *newSel)
{
   if (evt.ButtonDown())
   {

      mSelIndex = OverATextBox(evt.m_x, evt.m_y);
      if (mSelIndex != -1) {
         auto &labelStruct = mLabels[mSelIndex];
         *newSel = labelStruct.selectedRegion;

         if (evt.LeftDown()) {
            // Find the NEW drag end
            auto position = FindCurrentCursorPosition(evt.m_x);

            // Anchor shift-drag at the farther end of the previous highlight
            // that is farther from the click, on Mac, for consistency with
            // its text editors, but on the others, re-use the previous
            // anchor.
            if (evt.ShiftDown()) {
#ifdef __WXMAC__
               // Set the drag anchor at the end of the previous selection
               // that is farther from the NEW drag end
               if (abs(position - mCurrentCursorPos) >
                   abs(position - mInitialCursorPos))
                  mInitialCursorPos = mCurrentCursorPos;
#else
               // mInitialCursorPos remains as before
#endif
            }
            else
               mInitialCursorPos = position;

            mCurrentCursorPos = position;
            
            mDrawCursor = true;
            mRightDragging = false;
         }
         else
            // Actually this might be right or middle down
            mRightDragging = true;

         // reset the highlight indicator
         wxRect highlightedRect;
         {
            int xpos1, xpos2;
            CalcHighlightXs(&xpos1, &xpos2);

            wxASSERT(mFontHeight >= 0); // should have been set up while drawing
            // the rectangle of highlighted area
            highlightedRect = {
               xpos1, labelStruct.y - mFontHeight / 2,
               (int)(xpos2 - xpos1 + 0.5), mFontHeight
            };
         }

         // Middle click on GTK: paste from primary selection
#if defined(__WXGTK__) && (HAVE_GTK)
         if (evt.MiddleDown()) {
            // Check for a click outside of the selected label's text box; in this
            // case PasteSelectedText() will start a NEW label at the click
            // location
            if (!OverTextBox(&labelStruct, evt.m_x, evt.m_y))
               mSelIndex = -1;
            double t = zoomInfo.PositionToTime(evt.m_x, r.x);
            *newSel = SelectedRegion(t, t);
         }
#endif
      }
#if defined(__WXGTK__) && (HAVE_GTK)
      if (evt.MiddleDown()) {
         // Paste text, making a NEW label if none is selected.
         wxTheClipboard->UsePrimarySelection(true);
         PasteSelectedText(newSel->t0(), newSel->t1());
         wxTheClipboard->UsePrimarySelection(false);
      }
#endif
   }
}

// Check for keys that we will process
bool LabelTrack::DoCaptureKey(wxKeyEvent & event)
{
   // Check for modifiers and only allow shift
   int mods = event.GetModifiers();
   if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
      return false;
   }

   // Always capture the navigation keys, if we have any labels
   auto code = event.GetKeyCode();
   if ((code == WXK_TAB || code == WXK_NUMPAD_TAB) &&
       !mLabels.empty())
      return true;

   if (mSelIndex >= 0) {
      if (IsGoodLabelEditKey(event)) {
         return true;
      }
   }
   else {
      bool typeToCreateLabel;
      gPrefs->Read(wxT("/GUI/TypeToCreateLabel"), &typeToCreateLabel, true);
      if (IsGoodLabelFirstKey(event) && typeToCreateLabel) {
         AudacityProject * pProj = GetActiveProject();

         // If we're playing, don't capture if the selection is the same as the
         // playback region (this helps prevent label track creation from
         // stealing unmodified kbd. shortcuts)
         if (pProj->GetAudioIOToken() > 0 &&
               gAudioIO->IsStreamActive(pProj->GetAudioIOToken()))
         {
            double t0, t1;
            pProj->GetPlayRegion(&t0, &t1);
            if (pProj->mViewInfo.selectedRegion.t0() == t0 &&
                pProj->mViewInfo.selectedRegion.t1() == t1) {
               return false;
            }
         }

         // If there's a label there already don't capture
         if( GetLabelIndex(pProj->mViewInfo.selectedRegion.t0(),
                           pProj->mViewInfo.selectedRegion.t1()) != wxNOT_FOUND ) {
            return false;
         }

         return true;
      }
   }

   return false;
}

unsigned LabelTrack::CaptureKey(wxKeyEvent & event, ViewInfo &, wxWindow *)
{
   event.Skip(!DoCaptureKey(event));
   return RefreshCode::RefreshNone;
}

unsigned LabelTrack::KeyDown(wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *pParent)
{
   double bkpSel0 = viewInfo.selectedRegion.t0(),
      bkpSel1 = viewInfo.selectedRegion.t1();

   AudacityProject *const pProj = GetActiveProject();

   // Pass keystroke to labeltrack's handler and add to history if any
   // updates were done
   if (OnKeyDown(viewInfo.selectedRegion, event)) {
      pProj->PushState(_("Modified Label"),
         _("Label Edit"),
         UndoPush::CONSOLIDATE);
   }

   // Make sure caret is in view
   int x;
   if (CalcCursorX(&x)) {
      pProj->GetTrackPanel()->ScrollIntoView(x);
   }

   // If selection modified, refresh
   // Otherwise, refresh track display if the keystroke was handled
   if (bkpSel0 != viewInfo.selectedRegion.t0() ||
      bkpSel1 != viewInfo.selectedRegion.t1())
      return RefreshCode::RefreshAll;
   else if (!event.GetSkipped())
      return RefreshCode::RefreshCell;

   return RefreshCode::RefreshNone;
}

unsigned LabelTrack::Char(wxKeyEvent & event, ViewInfo &viewInfo, wxWindow *)
{
   double bkpSel0 = viewInfo.selectedRegion.t0(),
      bkpSel1 = viewInfo.selectedRegion.t1();
   // Pass keystroke to labeltrack's handler and add to history if any
   // updates were done

   AudacityProject *const pProj = GetActiveProject();

   if (OnChar(viewInfo.selectedRegion, event))
      pProj->PushState(_("Modified Label"),
      _("Label Edit"),
      UndoPush::CONSOLIDATE);

   // If selection modified, refresh
   // Otherwise, refresh track display if the keystroke was handled
   if (bkpSel0 != viewInfo.selectedRegion.t0() ||
      bkpSel1 != viewInfo.selectedRegion.t1())
      return RefreshCode::RefreshAll;
   else if (!event.GetSkipped())
      return RefreshCode::RefreshCell;

   return RefreshCode::RefreshNone;
}

/// KeyEvent is called for every keypress when over the label track.
bool LabelTrack::OnKeyDown(SelectedRegion &newSel, wxKeyEvent & event)
{
   // Only track true changes to the label
   bool updated = false;

   // Cache the keycode
   int keyCode = event.GetKeyCode();
   const int mods = event.GetModifiers();

   // Check for modifiers and only allow shift
   if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
      event.Skip();
      return updated;
   }

   // All editing keys are only active if we're currently editing a label
   if (mSelIndex >= 0) {
      auto &labelStruct = mLabels[mSelIndex];
      auto &title = labelStruct.title;
      switch (keyCode) {

      case WXK_BACK:
         {
            int len = title.Length();

            //IF the label is not blank THEN get rid of a letter or letters according to cursor position
            if (len > 0)
            {
               // IF there are some highlighted letters, THEN DELETE them
               if (mInitialCursorPos != mCurrentCursorPos)
                  RemoveSelectedText();
               else
               {
                  // DELETE one letter
                  if (mCurrentCursorPos > 0) {
                     title.Remove(mCurrentCursorPos-1, 1);
                     mCurrentCursorPos--;
                  }
               }
            }
            else
            {
               // ELSE no text in text box, so DELETE whole label.
               DeleteLabel( mSelIndex );
            }
            mInitialCursorPos = mCurrentCursorPos;
            updated = true;
         }
         break;

      case WXK_DELETE:
      case WXK_NUMPAD_DELETE:
         {
            int len = title.Length();

            //If the label is not blank get rid of a letter according to cursor position
            if (len > 0)
            {
               // if there are some highlighted letters, DELETE them
               if (mInitialCursorPos != mCurrentCursorPos)
                  RemoveSelectedText();
               else
               {
                  // DELETE one letter
                  if (mCurrentCursorPos < len) {
                     title.Remove(mCurrentCursorPos, 1);
                  }
               }
            }
            else
            {
               // DELETE whole label if no text in text box
               DeleteLabel( mSelIndex );
            }
            mInitialCursorPos = mCurrentCursorPos;
            updated = true;
         }
         break;

      case WXK_HOME:
      case WXK_NUMPAD_HOME:
         // Move cursor to beginning of label
         mCurrentCursorPos = 0;
         if (mods == wxMOD_SHIFT)
            ;
         else
            mInitialCursorPos = mCurrentCursorPos;
         break;

      case WXK_END:
      case WXK_NUMPAD_END:
         // Move cursor to end of label
         mCurrentCursorPos = (int)title.length();
         if (mods == wxMOD_SHIFT)
            ;
         else
            mInitialCursorPos = mCurrentCursorPos;
         break;

      case WXK_LEFT:
      case WXK_NUMPAD_LEFT:
         // Moving cursor left
         if (mCurrentCursorPos > 0) {
            mCurrentCursorPos--;
            if (mods == wxMOD_SHIFT)
               ;
            else
               mInitialCursorPos = mCurrentCursorPos =
                  std::min(mInitialCursorPos, mCurrentCursorPos);
         }
         break;

      case WXK_RIGHT:
      case WXK_NUMPAD_RIGHT:
         // Moving cursor right
         if (mCurrentCursorPos < (int)title.length()) {
            mCurrentCursorPos++;
            if (mods == wxMOD_SHIFT)
               ;
            else
               mInitialCursorPos = mCurrentCursorPos =
                  std::max(mInitialCursorPos, mCurrentCursorPos);
         }
         break;

      case WXK_RETURN:
      case WXK_NUMPAD_ENTER:

      case WXK_ESCAPE:
         if (mRestoreFocus >= 0) {
            TrackListIterator iter(GetActiveProject()->GetTracks());
            Track *track = iter.First();
            while (track && mRestoreFocus--)
               track = iter.Next();
            if (track)
               GetActiveProject()->GetTrackPanel()->SetFocusedTrack(track);
            mRestoreFocus = -1;
         }
         mSelIndex = -1;
         break;

      case WXK_TAB:
      case WXK_NUMPAD_TAB:
         if (event.ShiftDown()) {
               mSelIndex--;
         } else {
               mSelIndex++;
         }

         mSelIndex = (mSelIndex + (int)mLabels.size()) % (int)mLabels.size();    // wrap round if necessary
         {
            LabelStruct &newLabel = mLabels[mSelIndex];
            mCurrentCursorPos = newLabel.title.Length();
            mInitialCursorPos = mCurrentCursorPos;
            //Set the selection region to be equal to the selection bounds of the tabbed-to label.
            newSel = newLabel.selectedRegion;
         }
         break;

      case '\x10':   // OSX
      case WXK_MENU:
      case WXK_WINDOWS_MENU:
         ShowContextMenu();
         break;

      default:
         if (!IsGoodLabelEditKey(event)) {
            event.Skip();
         }
         break;
      }
   }
   else
   {
      switch (keyCode) {

      case WXK_TAB:
      case WXK_NUMPAD_TAB:
         if (!mLabels.empty()) {
            int len = (int) mLabels.size();
            if (event.ShiftDown()) {
               mSelIndex = len - 1;
               if (newSel.t0() > mLabels[0].getT0()) {
                  while (mSelIndex >= 0 &&
                         mLabels[mSelIndex].getT0() > newSel.t0()) {
                     mSelIndex--;
                  }
               }
            } else {
               mSelIndex = 0;
               if (newSel.t0() < mLabels[len - 1].getT0()) {
                  while (mSelIndex < len &&
                         mLabels[mSelIndex].getT0() < newSel.t0()) {
                     mSelIndex++;
                  }
               }
            }

            if (mSelIndex >= 0 && mSelIndex < len) {
               const auto &labelStruct = mLabels[mSelIndex];
               mCurrentCursorPos = labelStruct.title.Length();
               mInitialCursorPos = mCurrentCursorPos;
               //Set the selection region to be equal to the selection bounds of the tabbed-to label.
               newSel = labelStruct.selectedRegion;
            }
            else {
               mSelIndex = -1;
            }
         }
         break;

      default:
         if (!IsGoodLabelFirstKey(event)) {
            event.Skip();
         }
         break;
      }
   }

   // Make sure the caret is visible
   mDrawCursor = true;

   return updated;
}

/// OnChar is called for incoming characters -- that's any keypress not handled
/// by OnKeyDown.
bool LabelTrack::OnChar(SelectedRegion &WXUNUSED(newSel), wxKeyEvent & event)
{
   // Check for modifiers and only allow shift.
   //
   // We still need to check this or we will eat the top level menu accelerators
   // on Windows if our capture or key down handlers skipped the event.
   const int mods = event.GetModifiers();
   if (mods != wxMOD_NONE && mods != wxMOD_SHIFT) {
      event.Skip();
      return false;
   }

   // Only track true changes to the label
   bool updated = false;

   // Cache the character
   wxChar charCode = event.GetUnicodeKey();

   // Skip if it's not a valid unicode character or a control character
   if (charCode == 0 || wxIscntrl(charCode)) {
      event.Skip();
      return false;
   }
   
   // If we've reached this point and aren't currently editing, add NEW label
   if (mSelIndex < 0) {
      // Don't create a NEW label for a space
      if (wxIsspace(charCode)) {
         event.Skip();
         return false;
      }
      SetSelected(true);
      AudacityProject *p = GetActiveProject();
      AddLabel(p->mViewInfo.selectedRegion);
      p->PushState(_("Added label"), _("Label"));
   }

   //
   // Now we are definitely in a label; append the incoming character
   //

   auto &labelStruct = mLabels[mSelIndex];
   auto &title = labelStruct.title;

   // Test if cursor is in the end of string or not
   if (mInitialCursorPos != mCurrentCursorPos)
      RemoveSelectedText();

   if (mCurrentCursorPos < (int)title.length()) {
      // Get substring on the righthand side of cursor
      wxString rightPart = title.Mid(mCurrentCursorPos);
      // Set title to substring on the lefthand side of cursor
      title = title.Left(mCurrentCursorPos);
      //append charcode
      title += charCode;
      //append the right part substring
      title += rightPart;
   }
   else
      //append charCode
      title += charCode;

   //moving cursor position forward
   mInitialCursorPos = ++mCurrentCursorPos;
   updated = true;

   // Make sure the caret is visible
   mDrawCursor = true;

   return updated;
}

void LabelTrack::ShowContextMenu()
{
   wxWindow *parent = wxWindow::FindFocus();

   {
      wxMenu menu;
      menu.Bind(wxEVT_MENU, &LabelTrack::OnContextMenu, this);

      menu.Append(OnCutSelectedTextID, _("Cu&t"));
      menu.Append(OnCopySelectedTextID, _("&Copy"));
      menu.Append(OnPasteSelectedTextID, _("&Paste"));
      menu.Append(OnDeleteSelectedLabelID, _("&Delete Label"));
      menu.Append(OnEditSelectedLabelID, _("&Edit..."));

      menu.Enable(OnCutSelectedTextID, IsTextSelected());
      menu.Enable(OnCopySelectedTextID, IsTextSelected());
      menu.Enable(OnPasteSelectedTextID, IsTextClipSupported());
      menu.Enable(OnDeleteSelectedLabelID, true);
      menu.Enable(OnEditSelectedLabelID, true);

      wxASSERT(mSelIndex >= 0);
      const LabelStruct *ls = GetLabel(mSelIndex);

      wxClientDC dc(parent);

      if (msFont.Ok())
      {
         dc.SetFont(msFont);
      }

      int x = 0;
      bool success = CalcCursorX(&x);
      wxASSERT(success);

      parent->PopupMenu(&menu, x, ls->y + (mIconHeight / 2) - 1);
   }
}

void LabelTrack::OnContextMenu(wxCommandEvent & evt)
{
   AudacityProject *p = GetActiveProject();

   switch (evt.GetId())
   {
   /// Cut selected text if cut menu item is selected
   case OnCutSelectedTextID:
      if (CutSelectedText())
      {
         p->PushState(_("Modified Label"),
                      _("Label Edit"),
                      UndoPush::CONSOLIDATE);
      }
      break;

   /// Copy selected text if copy menu item is selected
   case OnCopySelectedTextID:
      CopySelectedText();
      break;

   /// paste selected text if paste menu item is selected
   case OnPasteSelectedTextID:
      if (PasteSelectedText(p->GetSel0(), p->GetSel1()))
      {
         p->PushState(_("Modified Label"),
                      _("Label Edit"),
                      UndoPush::CONSOLIDATE);
      }
      break;

   /// DELETE selected label
   case OnDeleteSelectedLabelID: {
      int ndx = GetLabelIndex(p->GetSel0(), p->GetSel1());
      if (ndx != -1)
      {
         DeleteLabel(ndx);
         p->PushState(_("Deleted Label"),
                      _("Label Edit"),
                      UndoPush::CONSOLIDATE);
      }
   }
      break;

   case OnEditSelectedLabelID: {
      int ndx = GetLabelIndex(p->GetSel0(), p->GetSel1());
      if (ndx != -1)
         p->DoEditLabels(this, ndx);
   }
      break;
   }
}

void LabelTrack::RemoveSelectedText()
{
   wxString left, right;

   int init = mInitialCursorPos;
   int cur = mCurrentCursorPos;
   if (init > cur)
      std::swap(init, cur);

   auto &labelStruct = mLabels[mSelIndex];
   auto &title = labelStruct.title;

   if (init > 0)
      left = title.Left(init);

   if (cur < (int)title.Length())
      right = title.Mid(cur);

   title = left + right;
   mInitialCursorPos = mCurrentCursorPos = left.Length();
}

void LabelTrack::Unselect()
{
   mSelIndex = -1;
}

bool LabelTrack::IsSelected() const
{
   return (mSelIndex >= 0 && mSelIndex < (int)mLabels.size());
}

/// Export labels including label start and end-times.
void LabelTrack::Export(wxTextFile & f) const
{
   // PRL: to do: export other selection fields
   for (auto &labelStruct: mLabels)
      labelStruct.Export(f);
}

/// Import labels, handling files with or without end-times.
void LabelTrack::Import(wxTextFile & in)
{
   int lines = in.GetLineCount();

   mLabels.clear();
   mLabels.reserve(lines);

   //Currently, we expect a tag file to have two values and a label
   //on each line. If the second token is not a number, we treat
   //it as a single-value label.
   bool error = false;
   for (int index = 0; index < lines;) {
      try {
         // Let LabelStruct::Import advance index
         LabelStruct l { LabelStruct::Import(in, index) };
         mLabels.push_back(l);
      }
      catch(const LabelStruct::BadFormatException&) { error = true; }
   }
   if (error)
      ::wxMessageBox( _("One or more saved labels could not be read.") );
   SortLabels();
}

bool LabelTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs)
{
   if (!wxStrcmp(tag, wxT("label"))) {

      SelectedRegion selectedRegion;
      wxString title;

      // loop through attrs, which is a null-terminated list of
      // attribute-value pairs
      while(*attrs) {
         const wxChar *attr = *attrs++;
         const wxChar *value = *attrs++;

         if (!value)
            break;

         const wxString strValue = value;
         if (!XMLValueChecker::IsGoodString(strValue))
         {
            return false;
         }

         if (selectedRegion.HandleXMLAttribute(attr, value, wxT("t"), wxT("t1")))
            ;
         else if (!wxStrcmp(attr, wxT("title")))
            title = strValue;

      } // while

      // Handle files created by Audacity 1.1.   Labels in Audacity 1.1
      // did not have separate start- and end-times.
      // PRL: this superfluous now, given class SelectedRegion's internal
      // consistency guarantees
      //if (selectedRegion.t1() < 0)
      //   selectedRegion.collapseToT0();

      LabelStruct l { selectedRegion, title };
      mLabels.push_back(l);

      return true;
   }
   else if (!wxStrcmp(tag, wxT("labeltrack"))) {
      long nValue = -1;
      while (*attrs) {
         const wxChar *attr = *attrs++;
         const wxChar *value = *attrs++;

         if (!value)
            return true;

         const wxString strValue = value;
         if (!wxStrcmp(attr, wxT("name")) && XMLValueChecker::IsGoodString(strValue))
            mName = strValue;
         else if (!wxStrcmp(attr, wxT("numlabels")) &&
                     XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
         {
            if (nValue < 0)
            {
               wxLogWarning(wxT("Project shows negative number of labels: %d"), nValue);
               return false;
            }
            mLabels.clear();
            mLabels.reserve(nValue);
         }
         else if (!wxStrcmp(attr, wxT("height")) &&
                  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
            SetHeight(nValue);
         else if (!wxStrcmp(attr, wxT("minimized")) &&
                  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
            SetMinimized(nValue != 0);
         else if (!wxStrcmp(attr, wxT("isSelected")) &&
                  XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue))
            this->SetSelected(nValue != 0);
      }

      return true;
   }

   return false;
}

XMLTagHandler *LabelTrack::HandleXMLChild(const wxChar *tag)
{
   if (!wxStrcmp(tag, wxT("label")))
      return this;
   else
      return NULL;
}

void LabelTrack::WriteXML(XMLWriter &xmlFile) const
// may throw
{
   int len = mLabels.size();

   xmlFile.StartTag(wxT("labeltrack"));
   xmlFile.WriteAttr(wxT("name"), mName);
   xmlFile.WriteAttr(wxT("numlabels"), len);
   xmlFile.WriteAttr(wxT("height"), this->GetActualHeight());
   xmlFile.WriteAttr(wxT("minimized"), this->GetMinimized());
   xmlFile.WriteAttr(wxT("isSelected"), this->GetSelected());

   for (auto &labelStruct: mLabels) {
      xmlFile.StartTag(wxT("label"));
      labelStruct.getSelectedRegion()
         .WriteXMLAttributes(xmlFile, wxT("t"), wxT("t1"));
      // PRL: to do: write other selection fields
      xmlFile.WriteAttr(wxT("title"), labelStruct.title);
      xmlFile.EndTag(wxT("label"));
   }

   xmlFile.EndTag(wxT("labeltrack"));
}

#if LEGACY_PROJECT_FILE_SUPPORT
bool LabelTrack::Load(wxTextFile * in, DirManager * dirManager)
{
   if (in->GetNextLine() != wxT("NumMLabels"))
      return false;

   unsigned long len;
   if (!(in->GetNextLine().ToULong(&len)))
      return false;

   mLabels.clear();
   mLabels.reserve(len);

   for (int i = 0; i < len; i++) {
      double t0;
      if (!Internat::CompatibleToDouble(in->GetNextLine(), &t0))
         return false;
      // Legacy file format does not include label end-times.
      // PRL: nothing NEW to do, legacy file support
      mLabels.push_back(LabelStruct {
         SelectedRegion{ t0, t0 }, in->GetNextLine()
      });
   }

   if (in->GetNextLine() != wxT("MLabelsEnd"))
      return false;
   SortLabels();
   return true;
}

bool LabelTrack::Save(wxTextFile * out, bool overwrite)
{
   out->AddLine(wxT("NumMLabels"));
   int len = mLabels.size();
   out->AddLine(wxString::Format(wxT("%d"), len));

   for (auto pLabel : mLabels) {
      const auto &labelStruct = *pLabel;
      out->AddLine(wxString::Format(wxT("%lf"), labelStruct.selectedRegion.mT0));
      out->AddLine(labelStruct.title);
   }
   out->AddLine(wxT("MLabelsEnd"));

   return true;
}
#endif

Track::Holder LabelTrack::Cut(double t0, double t1)
{
   auto tmp = Copy(t0, t1);

   Clear(t0, t1);

   return tmp;
}

#if 0
Track::Holder LabelTrack::SplitCut(double t0, double t1)
{
   // SplitCut() == Copy() + SplitDelete()

   Track::Holder tmp = Copy(t0, t1);

   if (!SplitDelete(t0, t1))
      return {};

   return tmp;
}
#endif

Track::Holder LabelTrack::Copy(double t0, double t1, bool) const
{
   auto tmp = std::make_unique<LabelTrack>(GetDirManager());
   const auto lt = static_cast<LabelTrack*>(tmp.get());

   for (auto &labelStruct: mLabels) {
      LabelStruct::TimeRelations relation =
                        labelStruct.RegionRelation(t0, t1, this);
      if (relation == LabelStruct::SURROUNDS_LABEL) {
         LabelStruct l {
            labelStruct.selectedRegion,
            labelStruct.getT0() - t0,
            labelStruct.getT1() - t0,
            labelStruct.title
         };
         lt->mLabels.push_back(l);
      }
      else if (relation == LabelStruct::WITHIN_LABEL) {
         LabelStruct l {
            labelStruct.selectedRegion,
            0,
            t1-t0,
            labelStruct.title
         };
         lt->mLabels.push_back(l);
      }
      else if (relation == LabelStruct::BEGINS_IN_LABEL) {
         LabelStruct l {
            labelStruct.selectedRegion,
            0,
            labelStruct.getT1() - t0,
            labelStruct.title
         };
         lt->mLabels.push_back(l);
      }
      else if (relation == LabelStruct::ENDS_IN_LABEL) {
         LabelStruct l {
            labelStruct.selectedRegion,
            labelStruct.getT0() - t0,
            t1 - t0,
            labelStruct.title
         };
         lt->mLabels.push_back(l);
      }
   }
   lt->mClipLen = (t1 - t0);

   // This std::move is needed to "upcast" the pointer type
   return std::move(tmp);
}


bool LabelTrack::PasteOver(double t, const Track * src)
{
   if (src->GetKind() != Track::Label)
      // THROW_INCONSISTENCY_EXCEPTION; // ?
      return false;

   int len = mLabels.size();
   int pos = 0;

   while (pos < len && mLabels[pos].getT0() < t)
      pos++;

   auto sl = static_cast<const LabelTrack *>(src);
   for (auto &labelStruct: sl->mLabels) {
      LabelStruct l {
         labelStruct.selectedRegion,
         labelStruct.getT0() + t,
         labelStruct.getT1() + t,
         labelStruct.title
      };
      mLabels.insert(mLabels.begin() + pos++, l);
   }

   return true;
}

void LabelTrack::Paste(double t, const Track *src)
{
   if (src->GetKind() != Track::Label)
      // THROW_INCONSISTENCY_EXCEPTION; // ?
      return;

   LabelTrack *lt = (LabelTrack *)src;

   double shiftAmt = lt->mClipLen > 0.0 ? lt->mClipLen : lt->GetEndTime();

   ShiftLabelsOnInsert(shiftAmt, t);
   PasteOver(t, src);
}

// This repeats the labels in a time interval a specified number of times.
bool LabelTrack::Repeat(double t0, double t1, int n)
{
   // Sanity-check the arguments
   if (n < 0 || t1 < t0)
      return false;

   double tLen = t1 - t0;

   // Insert space for the repetitions
   ShiftLabelsOnInsert(tLen * n, t1);

   // mLabels may resize as we iterate, so use subscripting
   for (unsigned int i = 0; i < mLabels.size(); ++i)
   {
      LabelStruct::TimeRelations relation =
                        mLabels[i].RegionRelation(t0, t1, this);
      if (relation == LabelStruct::SURROUNDS_LABEL)
      {
         // Label is completely inside the selection; duplicate it in each
         // repeat interval
         unsigned int pos = i; // running label insertion position in mLabels

         for (int j = 1; j <= n; j++)
         {
            const LabelStruct &label = mLabels[i];
            LabelStruct l {
               label.selectedRegion,
               label.getT0() + j * tLen,
               label.getT1() + j * tLen,
               label.title
            };

            // Figure out where to insert
            while (pos < mLabels.size() &&
                   mLabels[pos].getT0() < l.getT0())
               pos++;
            mLabels.insert(mLabels.begin() + pos, l);
         }
      }
      else if (relation == LabelStruct::BEGINS_IN_LABEL)
      {
         // Label ends inside the selection; ShiftLabelsOnInsert() hasn't touched
         // it, and we need to extend it through to the last repeat interval
         mLabels[i].selectedRegion.moveT1(n * tLen);
      }

      // Other cases have already been handled by ShiftLabelsOnInsert()
   }

   return true;
}

void LabelTrack::Silence(double t0, double t1)
{
   int len = mLabels.size();

   // mLabels may resize as we iterate, so use subscripting
   for (int i = 0; i < len; ++i) {
      LabelStruct::TimeRelations relation =
                        mLabels[i].RegionRelation(t0, t1, this);
      if (relation == LabelStruct::WITHIN_LABEL)
      {
         // Split label around the selection
         const LabelStruct &label = mLabels[i];
         LabelStruct l {
            label.selectedRegion,
            t1,
            label.getT1(),
            label.title
         };

         mLabels[i].selectedRegion.setT1(t0);

         // This might not be the right place to insert, but we sort at the end
         ++i;
         mLabels.insert(mLabels.begin() + i, l);
      }
      else if (relation == LabelStruct::ENDS_IN_LABEL)
      {
         // Beginning of label to selection end
         mLabels[i].selectedRegion.setT0(t1);
      }
      else if (relation == LabelStruct::BEGINS_IN_LABEL)
      {
         // End of label to selection beginning
         mLabels[i].selectedRegion.setT1(t0);
      }
      else if (relation == LabelStruct::SURROUNDS_LABEL)
      {
         DeleteLabel( i );
         len--;
         i--;
      }
   }

   SortLabels();
}

void LabelTrack::InsertSilence(double t, double len)
{
   for (auto &labelStruct: mLabels) {
      double t0 = labelStruct.getT0();
      double t1 = labelStruct.getT1();
      if (t0 >= t)
         t0 += len;

      if (t1 >= t)
         t1 += len;
      labelStruct.selectedRegion.setTimes(t0, t1);
   }
}

int LabelTrack::GetNumLabels() const
{
   return mLabels.size();
}

const LabelStruct *LabelTrack::GetLabel(int index) const
{
   return &mLabels[index];
}

int LabelTrack::GetLabelIndex(double t, double t1)
{
   //We'd have liked to have times in terms of samples,
   //because then we're doing an intrger comparison.
   //Never mind.  Instead we look for near enough.
   //This level of (in)accuracy is only a problem if we
   //deal with sounds in the MHz range.
   const double delta = 1.0e-7;
   { int i = -1; for (auto &labelStruct : mLabels) { ++i;
      if( fabs( labelStruct.getT0() - t ) > delta )
         continue;
      if( fabs( labelStruct.getT1() - t1 ) > delta )
         continue;
      return i;
   }}

   return wxNOT_FOUND;
}

int LabelTrack::AddLabel(const SelectedRegion &selectedRegion,
                         const wxString &title, int restoreFocus)
{
   LabelStruct l { selectedRegion, title };
   mInitialCursorPos = mCurrentCursorPos = title.length();

   int len = mLabels.size();
   int pos = 0;

   while (pos < len && mLabels[pos].getT0() < selectedRegion.t0())
      pos++;

   mLabels.insert(mLabels.begin() + pos, l);

   mSelIndex = pos;

   // Make sure the caret is visible
   //
   // LLL: The cursor will not be drawn when the first label
   //      is added since mDrawCursor will be false.  Presumably,
   //      if the user adds a label, then a cursor should be drawn
   //      to indicate that typing is expected.
   //
   //      If the label is added during actions like import, then the
   //      mDrawCursor flag will be reset once the action is complete.
   mDrawCursor = true;

   mRestoreFocus = restoreFocus;

   return pos;
}

void LabelTrack::DeleteLabel(int index)
{
   wxASSERT((index < (int)mLabels.size()));
   mLabels.erase(mLabels.begin() + index);
   // IF we've deleted the selected label
   // THEN set no label selected.
   if( mSelIndex== index )
   {
      mSelIndex = -1;
      mCurrentCursorPos = 1;
   }
   // IF we removed a label before the selected label
   // THEN the NEW selected label number is one less.
   else if( index < mSelIndex )
   {
      mSelIndex--;
   }
}

wxBitmap & LabelTrack::GetGlyph( int i)
{
   return theTheme.Bitmap( i + bmpLabelGlyph0);
}

// This one XPM spec is used to generate a number of
// different wxIcons.
/* XPM */
static const char *const GlyphXpmRegionSpec[] = {
/* columns rows colors chars-per-pixel */
"15 23 7 1",
/* Default colors, with first color transparent */
". c none",
"2 c black",
"3 c black",
"4 c black",
"5 c #BEBEF0",
"6 c #BEBEF0",
"7 c #BEBEF0",
/* pixels */
"...............",
"...............",
"...............",
"....333.444....",
"...3553.4774...",
"...3553.4774...",
"..35553.47774..",
"..35522222774..",
".3552666662774.",
".3526666666274.",
"355266666662774",
"355266666662774",
"355266666662774",
".3526666666274.",
".3552666662774.",
"..35522222774..",
"..35553.47774..",
"...3553.4774...",
"...3553.4774...",
"....333.444....",
"...............",
"...............",
"..............."
};

/// CreateCustomGlyphs() creates the mBoundaryGlyph array.
/// It's a bit like painting by numbers!
///
/// Schematically the glyphs we want will 'look like':
///   <O,  O>   and   <O>
/// for a left boundary to a label, a right boundary and both.
/// we're creating all three glyphs using the one Xpm Spec.
///
/// When we hover over a glyph we highlight the
/// inside of either the '<', the 'O' or the '>' or none,
/// giving 3 x 4 = 12 combinations.
///
/// Two of those combinations aren't used, but
/// treating them specially would make other code more
/// complicated.
void LabelTrack::CreateCustomGlyphs()
{
   int iConfig;
   int iHighlight;
   int index;
   const int nSpecRows =
      sizeof( GlyphXpmRegionSpec )/sizeof( GlyphXpmRegionSpec[0]);
   const char *XmpBmp[nSpecRows];

   // The glyphs are declared static wxIcon; so we only need
   // to create them once, no matter how many LabelTracks.
   if( mbGlyphsReady )
      return;

   // We're about to tweak the basic color spec to get 12 variations.
   for( iConfig=0;iConfig<NUM_GLYPH_CONFIGS;iConfig++)
   {
      for( iHighlight=0;iHighlight<NUM_GLYPH_HIGHLIGHTS;iHighlight++)
      {
         index = iConfig + NUM_GLYPH_CONFIGS * iHighlight;
         // Copy the basic spec...
         memcpy( XmpBmp, GlyphXpmRegionSpec, sizeof( GlyphXpmRegionSpec ));
         // The higlighted region (if any) is white...
         if( iHighlight==1 ) XmpBmp[5]="5 c #FFFFFF";
         if( iHighlight==2 ) XmpBmp[6]="6 c #FFFFFF";
         if( iHighlight==3 ) XmpBmp[7]="7 c #FFFFFF";
         // For left or right arrow the other side of the glyph
         // is the transparent color.
         if( iConfig==0) { XmpBmp[3]="3 c none"; XmpBmp[5]="5 c none"; }
         if( iConfig==1) { XmpBmp[4]="4 c none"; XmpBmp[7]="7 c none"; }
         // Create the icon from the tweaked spec.
         mBoundaryGlyphs[index] = wxBitmap(XmpBmp);
         // Create the mask
         // SetMask takes ownership
         mBoundaryGlyphs[index].SetMask(safenew wxMask(mBoundaryGlyphs[index], wxColour(192, 192, 192)));
      }
   }

   mIconWidth  = mBoundaryGlyphs[0].GetWidth();
   mIconHeight = mBoundaryGlyphs[0].GetHeight();
   mTextHeight = mIconHeight; // until proved otherwise...
   // The icon should have an odd width so that the
   // line goes exactly down the middle.
   wxASSERT( (mIconWidth %2)==1);

   mbGlyphsReady=true;
}

/// Returns true for keys we capture to start a label.
bool LabelTrack::IsGoodLabelFirstKey(const wxKeyEvent & evt)
{
   int keyCode = evt.GetKeyCode();
   return (keyCode < WXK_START
                  && keyCode != WXK_SPACE && keyCode != WXK_DELETE && keyCode != WXK_RETURN) ||
          (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
          (keyCode >= WXK_NUMPAD_EQUAL && keyCode <= WXK_NUMPAD_DIVIDE) ||
#if defined(__WXMAC__)
          (keyCode > WXK_RAW_CONTROL) ||
#endif
          (keyCode > WXK_WINDOWS_MENU);
}

/// This returns true for keys we capture for label editing.
bool LabelTrack::IsGoodLabelEditKey(const wxKeyEvent & evt)
{
   int keyCode = evt.GetKeyCode();

   // Accept everything outside of WXK_START through WXK_COMMAND, plus the keys
   // within that range that are usually printable, plus the ones we use for
   // keyboard navigation.
   return keyCode < WXK_START ||
          (keyCode >= WXK_END && keyCode < WXK_UP) ||
          (keyCode == WXK_RIGHT) ||
          (keyCode >= WXK_NUMPAD0 && keyCode <= WXK_DIVIDE) ||
          (keyCode >= WXK_NUMPAD_SPACE && keyCode <= WXK_NUMPAD_ENTER) ||
          (keyCode >= WXK_NUMPAD_HOME && keyCode <= WXK_NUMPAD_END) ||
          (keyCode >= WXK_NUMPAD_DELETE && keyCode <= WXK_NUMPAD_DIVIDE) ||
#if defined(__WXMAC__)
          (keyCode > WXK_RAW_CONTROL) ||
#endif
          (keyCode > WXK_WINDOWS_MENU);
}

/// Sorts the labels in order of their starting times.
/// This function is called often (whilst dragging a label)
/// We expect them to be very nearly in order, so insertion
/// sort (with a linear search) is a reasonable choice.
void LabelTrack::SortLabels( LabelTrackHit *pHit )
{
   const auto begin = mLabels.begin();
   const auto nn = (int)mLabels.size();
   int i = 1;
   while (true)
   {
      // Find the next disorder
      while (i < nn && mLabels[i - 1].getT0() <= mLabels[i].getT0())
         ++i;
      if (i >= nn)
         break;

      // Where must element i sink to?  At most i - 1, maybe less
      int j = i - 2;
      while( (j >= 0) && (mLabels[j].getT0() > mLabels[i].getT0()) )
         --j;
      ++j;

      // Now fix the disorder
      std::rotate(
         begin + j,
         begin + i,
         begin + i + 1
      );

      // Various indices need to be updated with the moved items...
      auto update = [=](int &index) {
         if( index <= i ) {
            if( index == i )
               index = j;
            else if( index >= j)
               ++index;
         }
      };
      if ( pHit ) {
         update( pHit->mMouseOverLabelLeft );
         update( pHit->mMouseOverLabelRight );
      }
      update(mSelIndex);
   }
}

wxString LabelTrack::GetTextOfLabels(double t0, double t1) const
{
   bool firstLabel = true;
   wxString retVal;

   for (auto &labelStruct: mLabels) {
      if (labelStruct.getT0() >= t0 &&
          labelStruct.getT1() <= t1)
      {
         if (!firstLabel)
            retVal += '\t';
         firstLabel = false;
         retVal += labelStruct.title;
      }
   }

   return retVal;
}

int LabelTrack::FindNextLabel(const SelectedRegion& currentRegion)
{
   int i = -1;

   if (!mLabels.empty()) {
      int len = (int) mLabels.size();
      if (miLastLabel >= 0 && miLastLabel + 1 < len
         && currentRegion.t0() == mLabels[miLastLabel].getT0()
         && currentRegion.t0() == mLabels[miLastLabel + 1].getT0() ) {
         i = miLastLabel + 1;
      }
      else {
         i = 0;
         if (currentRegion.t0() < mLabels[len - 1].getT0()) {
            while (i < len &&
                  mLabels[i].getT0() <= currentRegion.t0()) {
               i++;
            }
         }
      }
   }

   miLastLabel = i;
   return i;
}

 int LabelTrack::FindPrevLabel(const SelectedRegion& currentRegion)
{
   int i = -1;

   if (!mLabels.empty()) {
      int len = (int) mLabels.size();
      if (miLastLabel > 0 && miLastLabel < len
         && currentRegion.t0() == mLabels[miLastLabel].getT0()
         && currentRegion.t0() == mLabels[miLastLabel - 1].getT0() ) {
         i = miLastLabel - 1;
      }
      else {
         i = len - 1;
         if (currentRegion.t0() > mLabels[0].getT0()) {
            while (i >=0  &&
                  mLabels[i].getT0() >= currentRegion.t0()) {
               i--;
            }
         }
      }
   }

   miLastLabel = i;
   return i;
}
