/* 
 *
 * $Id: k3baudioeditorwidget.cpp 630444 2007-02-05 12:43:19Z trueg $
 * Copyright (C) 2004-2007 Sebastian Trueg <trueg@k3b.org>
 *
 * This file is part of the K3b project.
 * Copyright (C) 1998-2007 Sebastian Trueg <trueg@k3b.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * See the file "COPYING" for the exact licensing terms.
 */

#include "k3baudioeditorwidget.h"

#include <tqpainter.h>
#include <tqcolor.h>
#include <tqpixmap.h>
#include <tqcursor.h>
#include <tqapplication.h>
#include <tqdesktopwidget.h>
#include <tqtooltip.h>



class K3bAudioEditorWidget::Range
{
public:
  Range( int i,
	 const K3b::Msf& s,
	 const K3b::Msf& e,
	 bool sf,
	 bool ef,
	 const TQString& t,
	 const TQBrush& b )
    : id(i),
      start(s),
      end(e),
      startFixed(sf),
      endFixed(ef),
      brush(b),
      toolTip(t) {
  }

  int id;
  K3b::Msf start;
  K3b::Msf end;
  bool startFixed;
  bool endFixed;
  TQBrush brush;
  TQString toolTip;

  bool operator<( const K3bAudioEditorWidget::Range& r ) {
    return start < r.start;
  }
  bool operator>( const K3bAudioEditorWidget::Range& r ) {
    return start > r.start;
  }
  bool operator==( const K3bAudioEditorWidget::Range& r ) {
    return start == r.start;
  }
};


class K3bAudioEditorWidget::Marker
{
public:
  Marker( int i,
	  const K3b::Msf& msf,
	  bool f,
	  const TQColor& c, 
	  const TQString& t )
    : id(i),
      pos(msf),
      fixed(f),
      color(c),
      toolTip(t) {
  }

  int id;
  K3b::Msf pos;
  bool fixed;
  TQColor color;
  TQString toolTip;

  operator K3b::Msf& () { return pos; }
};


class K3bAudioEditorWidget::RangeList : public TQPtrList<K3bAudioEditorWidget::Range>
{
public:
  RangeList()
    : TQPtrList<Range>() {
  }

  RangeList( const RangeList& l )
    : TQPtrList<Range>( l ) {
  }

  int compareItems( TQPtrCollection::Item item1, TQPtrCollection::Item item2 ) {
    if( *static_cast<Range*>(item1) > *static_cast<Range*>(item2) )
      return 1;
    else if( *static_cast<Range*>(item1) < *static_cast<Range*>(item2) )
      return -1;
    else
      return 0;
  }
};

class K3bAudioEditorWidget::ToolTip : public TQToolTip
{
public:
  ToolTip( K3bAudioEditorWidget* w ) 
    : TQToolTip( w ),
      m_editorWidget( w ) {
  }

protected:
  void maybeTip( const TQPoint& p ) {
    TQRect r = m_editorWidget->contentsRect();
    Marker* m = m_editorWidget->findMarker( p );
    if( m ) {
      r.setLeft( p.x() - 1 );
      r.setRight( p.x() + 1 );
      tip( r, m->toolTip.isEmpty() ? m->pos.toString() : TQString("%1 (%2)").arg(m->toolTip).arg(m->pos.toString()) );
    }
    else {
      Range* range = m_editorWidget->findRange( p );
      if( range ) {
	r.setLeft( m_editorWidget->msfToPos( range->start ) );
	r.setRight( m_editorWidget->msfToPos( range->end ) );
	tip( r, 
	     range->toolTip.isEmpty()
	     ? TQString("%1 - %2").arg(range->start.toString()).arg(range->end.toString())
	     : TQString("%1 (%2 - %3)").arg(range->toolTip).arg(range->start.toString()).arg(range->end.toString()) );
      }
    }
  }

private:
  K3bAudioEditorWidget* m_editorWidget;
};


class K3bAudioEditorWidget::Private
{
public:
  Private()
    : allowOverlappingRanges(true),
      rangeSelectionEnabled(false),
      selectedRange(0),
      movedRange(0) {
  }

  TQBrush selectedRangeBrush;

  bool allowOverlappingRanges;
  bool rangeSelectionEnabled;

  Range* selectedRange;
  Range* movedRange;
  K3b::Msf lastMovePosition;

  RangeList ranges;
};


K3bAudioEditorWidget::K3bAudioEditorWidget( TQWidget* parent, const char* name )
  : TQFrame( parent, name, TQt::WNoAutoErase ),
    m_maxMarkers(1),
    m_idCnt(1),
    m_mouseAt(true),
    m_draggedRange(0),
    m_draggedMarker(0)
{
  d = new Private;
  d->selectedRangeBrush = TQBrush(colorGroup().highlight());

  setSizePolicy( TQSizePolicy::Preferred, TQSizePolicy::Minimum );
  setFrameStyle( StyledPanel|Sunken );
  setMouseTracking(true);
  setCursor( TQt::PointingHandCursor );

  m_margin = 5;

  m_toolTip = new ToolTip( this );
}


K3bAudioEditorWidget::~K3bAudioEditorWidget()
{
  d->ranges.setAutoDelete(true);
  m_markers.setAutoDelete(true);
  d->ranges.clear();
  m_markers.clear();

  delete d;
}


TQSize K3bAudioEditorWidget::minimumSizeHint() const
{
  constPolish();
  // some fixed height minimum and enough space for a tickmark every minute
  // But never exceed 2/3 of the the screen width, otherwise it just looks ugly
  // FIXME: this is still bad for long sources and there might be 60 minutes sources!

  int maxWidth = TQApplication::desktop()->width()*2/3;
  int wantedWidth = 2*m_margin + 2*frameWidth() + (m_length.totalFrames()/75/60 + 1) * fontMetrics().width( "000" );
  return TQSize( TQMIN( maxWidth, wantedWidth ), 
		2*m_margin + 12 + 6 /*12 for the tickmarks and 6 for the markers */ + fontMetrics().height() + 2*frameWidth() );
}


TQSize K3bAudioEditorWidget::sizeHint() const
{
  return minimumSizeHint();
}


void K3bAudioEditorWidget::setLength( const K3b::Msf& length )
{
  m_length = length;
  // TODO: remove markers beyond length
  // TODO: shorten ranges if nesseccary
  update();
}


const K3b::Msf K3bAudioEditorWidget::length() const
{
  return m_length;
}


void K3bAudioEditorWidget::setSelectedRangeBrush( const TQBrush& b )
{
  d->selectedRangeBrush = b;
}


const TQBrush& K3bAudioEditorWidget::selectedRangeBrush() const
{
  return d->selectedRangeBrush;
}


void K3bAudioEditorWidget::setAllowOverlappingRanges( bool b )
{
  d->allowOverlappingRanges = b;
}


bool K3bAudioEditorWidget::allowOverlappingRanges() const
{
  return d->allowOverlappingRanges;
}


void K3bAudioEditorWidget::enableRangeSelection( bool b )
{
  d->rangeSelectionEnabled = b;
  repaint( false );
}


bool K3bAudioEditorWidget::rangeSelectedEnabled() const
{
  return d->selectedRange;
}


void K3bAudioEditorWidget::setSelectedRange( int id )
{
  setSelectedRange( getRange( id ) );
}


void K3bAudioEditorWidget::setSelectedRange( K3bAudioEditorWidget::Range* r )
{
  d->selectedRange = r;
  if( rangeSelectedEnabled() ) {
    repaint( false );
    emit selectedRangeChanged( d->selectedRange ? d->selectedRange->id : 0 );
  }
}


int K3bAudioEditorWidget::selectedRange() const
{
  if( d->selectedRange )
    return d->selectedRange->id;
  else
    return 0;
}


int K3bAudioEditorWidget::addRange( const K3b::Msf& start, const K3b::Msf& end, 
				    bool startFixed, bool endFixed,
				    const TQString& toolTip,
				    const TQBrush& brush )
{
  if( start > end || end > m_length-1 )
    return -1;

  Range* r = new Range( m_idCnt++, start, end, startFixed, endFixed, toolTip,
			brush.style() != TQBrush::NoBrush ? brush : TQBrush(colorGroup().background()) );
  d->ranges.inSort( r );

  // only update the changed range
  TQRect rect = contentsRect();
  rect.setLeft( msfToPos( start ) );
  rect.setRight( msfToPos( end ) );
  update( rect );

  return r->id;
}


int K3bAudioEditorWidget::findRange( int pos ) const
{
  Range* r = findRange( TQPoint( pos, 0 ) );
  if( r )
    return r->id;
  else
    return 0;
}


int K3bAudioEditorWidget::findRangeEdge( int pos, bool* end ) const
{
  Range* r = findRangeEdge( TQPoint( pos, 0 ), end );
  if( r )
    return r->id;
  else
    return 0;
}


bool K3bAudioEditorWidget::modifyRange( int identifier, const K3b::Msf& start, const K3b::Msf& end )
{
  Range* range = getRange( identifier );
  if( range ) {
    if( start > end )
      return false;

    if( end > m_length )
      return false;

    range->start = start;
    range->end = end;

    if( !d->allowOverlappingRanges )
      fixupOverlappingRanges( range );
    
    repaint( false );

    return true;
  }
  else
    return false;
}


bool K3bAudioEditorWidget::removeRange( int identifier )
{
  if( Range* range = getRange( identifier ) ) {
    d->ranges.removeRef( range );

    emit rangeRemoved( identifier );

    // repaint only the part of the range
    TQRect rect = contentsRect();
    rect.setLeft( msfToPos( range->start ) );
    rect.setRight( msfToPos( range->end ) );

    if( d->selectedRange == range )
      setSelectedRange( 0 );

    delete range;

    update( rect );

    return true;
  }
  else
    return false;
}


K3b::Msf K3bAudioEditorWidget::rangeStart( int identifier ) const
{
  if( Range* range = getRange( identifier ) )
    return range->start;
  else
    return 0;
}


K3b::Msf K3bAudioEditorWidget::rangeEnd( int identifier ) const
{
  if( Range* range = getRange( identifier ) )
    return range->end;
  else
    return 0;
}


TQValueList<int> K3bAudioEditorWidget::allRanges() const
{
  TQValueList<int> l;
  d->ranges.sort();
  for( TQPtrListIterator<Range> it( d->ranges ); *it; ++it )
    l.append( (*it)->id );
  return l;
}


void K3bAudioEditorWidget::setMaxNumberOfMarkers( int i )
{
  m_maxMarkers = i;

  // remove last markers
  while( m_markers.count() > TQMAX( 1, m_maxMarkers ) ) {
    removeMarker( m_markers.getLast()->id );
  }
}


int K3bAudioEditorWidget::addMarker( const K3b::Msf& pos, bool fixed, const TQString& toolTip, const TQColor& color )
{
  if( pos < m_length ) {
    Marker* m = new Marker( m_idCnt++, pos, fixed, color.isValid() ? color : colorGroup().foreground(), toolTip );
    m_markers.inSort( m );
    return m->id;
  }
  else
    return -1;
}


bool K3bAudioEditorWidget::removeMarker( int identifier )
{
  if( Marker* m = getMarker( identifier ) ) {
    m_markers.removeRef( m );

    emit markerRemoved( identifier );

    // TODO: in case a marker is bigger than one pixel this needs to be changed
    TQRect rect = contentsRect();
    rect.setLeft( msfToPos( m->pos ) );
    rect.setRight( msfToPos( m->pos ) );
    delete m;

    update( rect );

    return true;
  }
  else
    return false;
}


bool K3bAudioEditorWidget::moveMarker( int identifier, const K3b::Msf& pos )
{
  if( pos < m_length )
    if( Marker* m = getMarker( identifier ) ) {
      TQRect rect = contentsRect();
      rect.setLeft( TQMIN( msfToPos( pos ), msfToPos( m->pos ) ) );
      rect.setRight( TQMAX( msfToPos( pos ), msfToPos( m->pos ) ) );

      m->pos = pos;

      // TODO: in case a marker is bigger than one pixel this needs to be changed
      update( rect );

      return true;
    }

  return false;
}


void K3bAudioEditorWidget::drawContents( TQPainter* p )
{
  // double buffering
  TQPixmap pix( contentsRect().size() );
  pix.fill( colorGroup().base() );

  TQPainter pixP;
  pixP.begin( &pix, this );

  TQRect drawRect( contentsRect() );
  drawRect.setLeft( drawRect.left() + m_margin );
  drawRect.setRight( drawRect.right() - m_margin );

  // from minimumSizeHint()
//   int neededHeight = fontMetrics().height() + 12 + 6;

//   drawRect.setTop( drawRect.top() + (drawRect.height() - neededHeight)/2 );
//   drawRect.setHeight( neededHeight );

  drawRect.setTop( drawRect.top() + m_margin );
  drawRect.setBottom( drawRect.bottom() - m_margin );

  drawAll( &pixP, drawRect );

  pixP.end();

  TQRect rect = p->clipRegion().boundingRect();
  TQRect pixRect = rect;
  pixRect.moveBy( -1*frameWidth(), -1*frameWidth() );
  bitBlt( this, rect.topLeft(), &pix, pixRect );
}


void K3bAudioEditorWidget::drawAll( TQPainter* p, const TQRect& drawRect )
{
  // we simply draw the ranges one after the other.
  for( TQPtrListIterator<Range> it( d->ranges ); *it; ++it )
    drawRange( p, drawRect, *it );

  // Hack to make sure the currently selected range is always on top
  if( d->selectedRange )
    drawRange( p, drawRect, d->selectedRange );
  
  for( TQPtrListIterator<Marker> it( m_markers ); *it; ++it )
    drawMarker( p, drawRect, *it );


  // left vline
  p->drawLine( drawRect.left(), drawRect.top(), 
	       drawRect.left(), drawRect.bottom() );

  // timeline
  p->drawLine( drawRect.left(), drawRect.bottom(), 
	       drawRect.right(), drawRect.bottom() );

  // right vline
  p->drawLine( drawRect.right(), drawRect.top(), 
	       drawRect.right(), drawRect.bottom() );

  // draw minute markers every minute
  int minute = 1;
  int minuteStep = 1;
  int markerVPos = drawRect.bottom();
  int maxMarkerWidth = fontMetrics().width( TQString::number(m_length.minutes()) );
  int minNeededSpace = maxMarkerWidth + 1;
  int x = 0;
  while( minute*60*75 < m_length ) {
    int newX = msfToPos( minute*60*75 );

    // only draw the mark if we have anough space
    if( newX - x >= minNeededSpace ) {
      p->drawLine( newX, markerVPos, newX, markerVPos-5 );
      TQRect txtRect( newX-(maxMarkerWidth/2), 
		     markerVPos - 6 - fontMetrics().height(), 
		     maxMarkerWidth, 
		     fontMetrics().height() );
      p->drawText( txtRect, TQt::AlignCenter, TQString::number(minute) );

      // FIXME: draw second markers if we have enough space

      x = newX;
    }
    else {
      minute -= minuteStep;
      if( minuteStep == 1 )
	minuteStep = 5;
      else
	minuteStep *= 2;
    }

    minute += minuteStep;
  }
}


void K3bAudioEditorWidget::drawRange( TQPainter* p, const TQRect& drawRect, K3bAudioEditorWidget::Range* r )
{
  p->save();

  int start = msfToPos( r->start );
  int end = msfToPos( r->end );

  if( rangeSelectedEnabled() && r == d->selectedRange )
    p->fillRect( start, drawRect.top() + 6 , end-start+1, drawRect.height() - 6, selectedRangeBrush() );
  else
    p->fillRect( start, drawRect.top() + 6 , end-start+1, drawRect.height() - 6, r->brush );

  p->drawRect( start, drawRect.top() + 6 , end-start+1, drawRect.height() - 6 );

  p->restore();
}


void K3bAudioEditorWidget::drawMarker( TQPainter* p, const TQRect& drawRect, K3bAudioEditorWidget::Marker* m )
{
  p->save();

  p->setPen( m->color );
  p->setBrush( m->color );

  int x = msfToPos( m->pos );
  p->drawLine( x, drawRect.bottom(), x, drawRect.top() );

  TQPointArray points( 3 );
  points.setPoint( 0, x, drawRect.top() + 6 );
  points.setPoint( 1, x-3, drawRect.top() );
  points.setPoint( 2, x+3, drawRect.top() );
  p->drawPolygon( points );

  p->restore();
}


void K3bAudioEditorWidget::fixupOverlappingRanges( Range* r )
{
  // copy the list to avoid problems with the iterator
  RangeList ranges( d->ranges );
  for( TQPtrListIterator<Range> it( ranges ); *it; ++it ) {
    Range* range = *it;
    if( range != r ) {
      // remove the range if it is covered completely
      if( range->start >= r->start &&
	  range->end <= r->end ) {
	d->ranges.removeRef( range );
	emit rangeRemoved( range->id );
	if( d->selectedRange == range )
	  setSelectedRange( 0 );
	delete range;
      }
      // split the range if it contains r completely
      else if( r->start >= range->start &&
	       r->end <= range->end ) {
	// create a new range that spans the part after r
	addRange( r->end+1, range->end, 
		  range->startFixed, range->endFixed,
		  range->toolTip,
		  range->brush );

	// modify the old range to only span the part before r
	range->end = r->start-1;
	emit rangeChanged( range->id, range->start, range->end );
      }
      else if( range->start >= r->start && range->start <= r->end ) {
	range->start = r->end+1;
	emit rangeChanged( range->id, range->start, range->end );
      }
      else if( range->end >= r->start && range->end <= r->end ) {
	range->end = r->start-1;
	emit rangeChanged( range->id, range->start, range->end );
      }
    }
  }
}


void K3bAudioEditorWidget::mousePressEvent( TQMouseEvent* e )
{
  m_draggedRange = 0;
  m_draggedMarker = 0;
  bool end;
  Range* r = findRangeEdge( e->pos(), &end );

  if (r) {
    m_draggedRange = r;
    m_draggingRangeEnd = end;
    setSelectedRange( r );
  }
  else {
    r = findRange( e->pos() );
    d->movedRange = r;
    d->lastMovePosition = posToMsf( e->pos().x() );
    setSelectedRange( r );
    m_draggedMarker = findMarker( e->pos() );
  }

  TQFrame::mousePressEvent(e);
}


void K3bAudioEditorWidget::mouseReleaseEvent( TQMouseEvent* e )
{
  if( !d->allowOverlappingRanges ) {
    //
    // modify and even delete ranges that we touched
    //
    if( m_draggedRange ) {
      fixupOverlappingRanges( m_draggedRange );
      repaint( false );
    }
    else if( d->movedRange ) {
      fixupOverlappingRanges( d->movedRange );
      repaint( false );
    }
  }

  m_draggedRange = 0;
  m_draggedMarker = 0;
  d->movedRange = 0;

  TQFrame::mouseReleaseEvent(e);
}


void K3bAudioEditorWidget::mouseDoubleClickEvent( TQMouseEvent* e )
{
  TQFrame::mouseDoubleClickEvent(e);
}


void K3bAudioEditorWidget::mouseMoveEvent( TQMouseEvent* e )
{
  if( m_mouseAt )
    emit mouseAt( posToMsf( e->pos().x() ) );
  
  if( e->state() & TQt::LeftButton ) {
    if( m_draggedRange ) {
      // determine the position the range's end was dragged to and its other end
      K3b::Msf msfPos = TQMAX( 0, TQMIN( posToMsf( e->pos().x() ), m_length-1 ) );
      K3b::Msf otherEnd = ( m_draggingRangeEnd ? m_draggedRange->start : m_draggedRange->end );

      // move it to the new pos
      if( m_draggingRangeEnd )
	m_draggedRange->end = msfPos;
      else
	m_draggedRange->start = msfPos;

      // if we pass the other end switch them
      if( m_draggedRange->start > m_draggedRange->end ) {
	K3b::Msf buf = m_draggedRange->start;
	m_draggedRange->start = m_draggedRange->end;
	m_draggedRange->end = buf;
	m_draggingRangeEnd = !m_draggingRangeEnd;
      }

      emit rangeChanged( m_draggedRange->id, m_draggedRange->start, m_draggedRange->end );

      repaint( false );
    }
    else if( m_draggedMarker ) {
      m_draggedMarker->pos = posToMsf( e->pos().x() );
      emit markerMoved( m_draggedMarker->id, m_draggedMarker->pos );

      repaint( false );
    }
    else if( d->movedRange ) {
      int diff = posToMsf( e->pos().x() ).lba() - d->lastMovePosition.lba();
      if( d->movedRange->end + diff >= m_length )
	diff = m_length.lba() - d->movedRange->end.lba() - 1;
      else if( d->movedRange->start - diff < 0 )
	diff = -1 * d->movedRange->start.lba();
      d->movedRange->start += diff;
      d->movedRange->end += diff;

//       if( !d->allowOverlappingRanges )
// 	fixupOverlappingRanges( d->movedRange );

      d->lastMovePosition = posToMsf( e->pos().x() );

      emit rangeChanged( d->movedRange->id, d->movedRange->start, d->movedRange->end );

      repaint( false );
    }
  }
  else if( findRangeEdge( e->pos() ) || findMarker( e->pos() ) )
    setCursor( TQt::SizeHorCursor );
  else
    setCursor( TQt::PointingHandCursor );

  TQFrame::mouseMoveEvent(e);
}


K3bAudioEditorWidget::Range* K3bAudioEditorWidget::getRange( int i ) const
{
  for( TQPtrListIterator<Range> it( d->ranges ); *it; ++it )
    if( (*it)->id == i )
      return *it;

  return 0;
}


K3bAudioEditorWidget::Range* K3bAudioEditorWidget::findRange( const TQPoint& p ) const
{
  // TODO: binary search; maybe store start and end positions in sorted lists for quick searching
  // this might be a stupid approach but we do not have many ranges anyway
  for( TQPtrListIterator<Range> it( d->ranges ); *it; ++it ) {
    Range* range = *it;
    int start = msfToPos( range->start );
    int end = msfToPos( range->end );

    if( p.x() >= start && p.x() <= end ) {
      return range;
    }
  }
  return 0;
}


K3bAudioEditorWidget::Range* K3bAudioEditorWidget::findRangeEdge( const TQPoint& p, bool* isEnd ) const
{
  // TODO: binary search
  // this might be a stupid approach but we do not have many ranges anyway
  for( TQPtrListIterator<Range> it( d->ranges ); *it; ++it ) {
    Range* range = *it;
    int start = msfToPos( range->start );
    int end = msfToPos( range->end );

    //
    // In case two ranges meet at one point moving the mouse cursor deeper into one
    // range allows for grabbing that end
    //

    if( p.x() - 3 <= start && p.x() >= start && !range->startFixed ) {
      if( isEnd )
	*isEnd = false;
      return range;
    }
    else if( p.x() <= end && p.x() + 3 >= end && !range->endFixed ) {
      if( isEnd )
	*isEnd = true;
      return range;
    }
  }
  return 0;
}


K3bAudioEditorWidget::Marker* K3bAudioEditorWidget::getMarker( int i ) const
{
  for( TQPtrListIterator<Marker> it( m_markers ); *it; ++it )
    if( (*it)->id == i )
      return *it;

  return 0;
}


K3bAudioEditorWidget::Marker* K3bAudioEditorWidget::findMarker( const TQPoint& p ) const
{
  // TODO: binary search
  for( TQPtrListIterator<Marker> it( m_markers ); *it; ++it ) {
    Marker* marker = *it;
    int start = msfToPos( marker->pos );

    if( p.x() - 1 <= start && p.x() + 1 >= start && !marker->fixed )
      return marker;
  }

  return 0;
}


// p is in widget coordinates
K3b::Msf K3bAudioEditorWidget::posToMsf( int p ) const
{
  int w = contentsRect().width() - 2*m_margin;
  int x = TQMIN( p-frameWidth()-m_margin, w );
  return ( (int)((double)(m_length.lba()-1) / (double)w * (double)x) );
}


// returns widget coordinates
int K3bAudioEditorWidget::msfToPos( const K3b::Msf& msf ) const
{
  int w = contentsRect().width() - 2*m_margin;
  int pos = (int)((double)w / (double)(m_length.lba()-1) * (double)msf.lba());
  return frameWidth() + m_margin + TQMIN( pos, w-1 );
}


#include "k3baudioeditorwidget.moc"
