//=============================================================================
//  MuseScore
//  Music Composition & Notation
//
//  Copyright (C) 2002-2018 Werner Schweer
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License version 2
//  as published by the Free Software Foundation and appearing in
//  the file LICENCE.GPL
//=============================================================================

#include "lyrics.h"

#include "chord.h"
#include "score.h"
#include "sym.h"
#include "system.h"
#include "xml.h"
#include "staff.h"
#include "segment.h"
#include "undo.h"
#include "textedit.h"

namespace Ms {

//---------------------------------------------------------
//   searchNextLyrics
//---------------------------------------------------------

static Lyrics* searchNextLyrics(Segment* s, int staffIdx, int verse, Placement p)
      {
      Lyrics* l = 0;
      while ((s = s->next1(SegmentType::ChordRest))) {
            int strack = staffIdx * VOICES;
            int etrack = strack + VOICES;
            // search through all tracks of current staff looking for a lyric in specified verse
            for (int track = strack; track < etrack; ++track) {
                  ChordRest* cr = toChordRest(s->element(track));
                  if (cr) {
                        // cr with lyrics found, but does it have a syllable in specified verse?
                        l = cr->lyrics(verse, p);
                        if (l)
                              break;
                        }
                  }
            if (l)
                  break;
            }
      return l;
      }

//---------------------------------------------------------
//   LyricsLine
//---------------------------------------------------------

LyricsLine::LyricsLine(Score* s)
  : SLine(s, ElementFlag::NOT_SELECTABLE)
      {
      setGenerated(true);           // no need to save it, as it can be re-generated
      setDiagonal(false);
      setLineWidth(Lyrics::LYRICS_DASH_DEFAULT_LINE_THICKNESS * spatium());
      setAnchor(Spanner::Anchor::SEGMENT);
      _nextLyrics = 0;
      }

LyricsLine::LyricsLine(const LyricsLine& g)
   : SLine(g)
      {
      _nextLyrics = 0;
      }

//---------------------------------------------------------
//   layout
//---------------------------------------------------------

void LyricsLine::layout()
      {
      bool tempMelismaTicks = (lyrics()->ticks() == Lyrics::TEMP_MELISMA_TICKS);
      if (lyrics()->ticks()) {              // melisma
            setLineWidth(score()->styleP(Sid::lyricsLineThickness));
            // if lyrics has a temporary one-chord melisma, set to 0 ticks (just its own chord)
            if (tempMelismaTicks)
                  lyrics()->setTicks(0);

            // Lyrics::_ticks points to the beginning of the last spanned segment,
            // but the line shall include it:
            // include the duration of this last segment in the melisma duration
            Segment* lyricsSegment = lyrics()->segment();
            int lyricsStartTick    = lyricsSegment->tick();
            int lyricsEndTick      = lyrics()->endTick();
            int lyricsTrack        = lyrics()->track();

            // find segment with tick >= endTick
            Segment* s = lyricsSegment;
            while (s && s->tick() < lyricsEndTick)
                  s = s->nextCR(lyricsTrack, true);
            if (!s) {
                  // user probably deleted measures at end of score, leaving this melisma too long
                  // set s to last segment and reset lyricsEndTick to trigger FIXUP code below
                  s = score()->lastSegment();
                  lyricsEndTick = -1;
                  }
            Element* se = s->element(lyricsTrack);
            // everything is OK if we have reached a chord at right tick on right track
            if (s->tick() == lyricsEndTick && se && se->type() == ElementType::CHORD) {
                  // advance to next CR, or last segment if no next CR
                  s = s->nextCR(lyricsTrack, true);
                  if (!s)
                        s = score()->lastSegment();
                  }
            else {
                  // FIXUP - lyrics tick count not valid
                  // this happens if edits to score have removed the original end segment
                  // so let's fix it here
                  // s is already pointing to segment past endTick (or to last segment)
                  // we should shorten the lyrics tick count to make this work
                  Segment* ns = s;
                  Segment* ps = s->prev1(SegmentType::ChordRest);
                  while (ps && ps != lyricsSegment) {
                        Element* pe = ps->element(lyricsTrack);
                        // we're looking for an actual chord on this track
                        if (pe && pe->type() == ElementType::CHORD)
                              break;
                        s = ps;
                        ps = ps->prev1(SegmentType::ChordRest);
                        }
                  if (!ps || ps == lyricsSegment) {
                        // no valid previous CR, so try to lengthen melisma instead
                        ps = ns;
                        s = ps->nextCR(lyricsTrack, true);
                        Element* e = s ? s->element(lyricsTrack) : nullptr;
                        // check to make sure we have a chord
                        if (!e || e->type() != ElementType::CHORD) {
                              // nothing to do but set ticks to 0
                              // this will result in melisma being deleted later
                              lyrics()->undoChangeProperty(Pid::LYRIC_TICKS, 0);
                              setTicks(0);
                              return;
                              }
                        }
                  lyrics()->undoChangeProperty(Pid::LYRIC_TICKS, ps->tick() - lyricsStartTick);
                  }
            setTicks(s->tick() - lyricsStartTick);
            }
      else {                                    // dash(es)
#if defined(USE_FONT_DASH_TICKNESS)
            setLineWidth(Spatium(lyrics()->dashThickness() / spatium()));
#endif
            _nextLyrics = searchNextLyrics(lyrics()->segment(), staffIdx(), lyrics()->no(), lyrics()->placement());
            setTick2(_nextLyrics ? _nextLyrics->segment()->tick() : tick());
            }
      if (ticks()) {                // only do layout if some time span
            // do layout with non-0 duration
            if (tempMelismaTicks)
                  lyrics()->setTicks(Lyrics::TEMP_MELISMA_TICKS);
            SLine::layout();
            // if temp melisma and there is a first line segment,
            // extend it to be after the lyrics syllable (otherwise
            // the melisma segment will be often covered by the syllable itself)
            if (tempMelismaTicks && segments.size() > 0)
                  segmentAt(0)->rxpos2() += lyrics()->width();
            }
      }

//---------------------------------------------------------
//   createLineSegment
//---------------------------------------------------------

LineSegment* LyricsLine::createLineSegment()
      {
      LyricsLineSegment* seg = new LyricsLineSegment(score());
      seg->setTrack(track());
      seg->setColor(color());
      return seg;
      }

//---------------------------------------------------------
//   removeUnmanaged
//    same as Spanner::removeUnmanaged(), but in addition, remove from hosting Lyrics
//---------------------------------------------------------

void LyricsLine::removeUnmanaged()
      {
      Spanner::removeUnmanaged();
      if (lyrics())
            lyrics()->remove(this);
      }

//---------------------------------------------------------
//   setProperty
//---------------------------------------------------------

bool LyricsLine::setProperty(Pid propertyId, const QVariant& v)
      {
      switch (propertyId) {
            case Pid::SPANNER_TICKS:
                  {
                  // if parent lyrics has a melisma, change its length too
                  if (parent() && parent()->type() == ElementType::LYRICS
                              && toLyrics(parent())->ticks() > 0) {
                        int newTicks   = toLyrics(parent())->ticks() + v.toInt() - ticks();
                        parent()->undoChangeProperty(Pid::LYRIC_TICKS, newTicks);
                        }
                  setTicks(v.toInt());
                  }
                  break;
            default:
                  if (!SLine::setProperty(propertyId, v))
                        return false;
                  break;
            }
      score()->setLayoutAll();
      return true;
      }

//=========================================================
//   LyricsLineSegment
//=========================================================

LyricsLineSegment::LyricsLineSegment(Score* s)
      : LineSegment(s, ElementFlag::ON_STAFF | ElementFlag::NOT_SELECTABLE)
      {
      setGenerated(true);
      }

//---------------------------------------------------------
//   layout
//---------------------------------------------------------

void LyricsLineSegment::layout()
      {
      bool        endOfSystem       = false;
      bool        isEndMelisma      = lyricsLine()->lyrics()->ticks() > 0;
      Lyrics*     lyr               = 0;
      Lyrics*     nextLyr           = 0;
      qreal       fromX             = 0;
      qreal       toX               = 0;             // start and end point of intra-lyrics room
      qreal       sp                = spatium();
      System*     sys;

      if (lyricsLine()->ticks() <= 0) {   // if no span,
            _numOfDashes = 0;             // nothing to draw
            return;                       // and do nothing
            }

      // HORIZONTAL POSITION
      // A) if line precedes a syllable, advance line end to right before the next syllable text
      // if not a melisma and there is a next syllable;
      if (!isEndMelisma && lyricsLine()->nextLyrics() && isSingleEndType()) {
            lyr         = nextLyr = lyricsLine()->nextLyrics();
            sys         = lyr->segment()->system();
            endOfSystem = (sys != system());
            // if next lyrics is on a different system, this line segment is at the end of its system:
            // do not adjust for next lyrics position
            if (!endOfSystem) {
                  qreal lyrX        = lyr->bbox().x();
                  qreal lyrXp       = lyr->pagePos().x();
                  qreal sysXp       = sys->pagePos().x();
                  toX               = lyrXp - sysXp + lyrX;       // syst.rel. X pos.
                  qreal offsetX     = toX - pos().x() - pos2().x() - Lyrics::LYRICS_DASH_DEFAULT_PAD * sp;
                  //                    delta from current end pos.| ending padding
                  rxpos2()          += offsetX;
                  }
            }
      // B) if line follows a syllable, advance line start to after the syllable text
      lyr  = lyricsLine()->lyrics();
      sys  = lyr->segment()->system();
      if (sys && isSingleBeginType()) {
            qreal lyrX        = lyr->bbox().x();
            qreal lyrXp       = lyr->pagePos().x();
            qreal lyrW        = lyr->bbox().width();
            qreal sysXp       = sys->pagePos().x();
            fromX             = lyrXp - sysXp + lyrX + lyrW;
            //               syst.rel. X pos. | lyr.advance
            qreal offsetX     = fromX - pos().x();
            offsetX           += (isEndMelisma ? Lyrics::MELISMA_DEFAULT_PAD : Lyrics::LYRICS_DASH_DEFAULT_PAD) * sp;

            //               delta from curr.pos. | add initial padding
            rxpos()           += offsetX;
            rxpos2()          -= offsetX;
            }

      // VERTICAL POSITION: at the base line of the syllable text
      if (!isEndType())
            rypos() = lyr->ipos().y();
      else {
            // use Y position of *next* syllable if there is one on same system
            Lyrics* nextLyr = searchNextLyrics(lyr->segment(), lyr->staffIdx(), lyr->no(), lyr->placement());
            if (nextLyr && nextLyr->segment()->system() == system())
                  rypos() = nextLyr->y();
            else
                  rypos() = lyr->y();
            }

      // MELISMA vs. DASHES
      if (isEndMelisma) {                 // melisma
            _numOfDashes = 1;
            rypos()      -= lyricsLine()->lineWidth() * .5; // let the line 'sit on' the base line
            qreal offsetX = score()->styleP(Sid::minNoteDistance) * mag();
            // if final segment, extend slightly after the chord, otherwise shorten it
            rxpos2() += (isBeginType() || isEndType()) ? -offsetX : +offsetX;
            }
      else {                              // dash(es)
#if defined(USE_FONT_DASH_METRIC)
            rypos()     += lyr->dashY();
            _dashLength = lyr->dashLength();
#else
            // set conventional dash Y pos
            rypos() -= MScore::pixelRatio * lyr->fontMetrics().xHeight() * Lyrics::LYRICS_DASH_Y_POS_RATIO;
            _dashLength = score()->styleP(Sid::lyricsDashMaxLength) * mag();  // and dash length
#endif
            qreal len         = pos2().x();
            qreal minDashLen  = score()->styleS(Sid::lyricsDashMinLength).val() * sp;
            qreal maxDashDist = score()->styleS(Sid::lyricsDashMaxDistance).val() * sp;
            if (len < minDashLen) {                                           // if no room for a dash
                  // if at end of system or dash is forced
                  if (endOfSystem || score()->styleB(Sid::lyricsDashForce)) {
                        rxpos2()          = minDashLen;                       //     draw minimal dash
                        _numOfDashes      = 1;
                        _dashLength       = minDashLen;
                        }
                  else                                                        //   if within system or dash not forced
                        _numOfDashes = 0;                                     //     draw no dash
                  }
            else if (len < (maxDashDist * 2.0)) {                           // if no room for two dashes
                  _numOfDashes = 1;                                           //    draw one dash
                  if (_dashLength > len)                                      // if no room for a full dash
                        _dashLength = len;                                    //    shorten it
                  }
            else
                  _numOfDashes = len / (maxDashDist);                         // draw several dashes

            // adjust next lyrics horiz. position if too little a space forced to skip the dash
            if (_numOfDashes == 0 && nextLyr != nullptr && len > 0)
                  nextLyr->rxpos() -= (toX - fromX);
            }

      // set bounding box
      QRectF r = QRectF(0.0, 0.0, pos2().x(), pos2().y()).normalized();
      qreal lw = lyricsLine()->lineWidth() * .5;
      setbbox(r.adjusted(-lw, -lw, lw, lw));
      }

//---------------------------------------------------------
//   draw
//---------------------------------------------------------

void LyricsLineSegment::draw(QPainter* painter) const
      {
      if (_numOfDashes < 1)               // nothing to draw
            return;

      QPen pen(lyricsLine()->lyrics()->curColor());
      pen.setWidthF(lyricsLine()->lineWidth());
      pen.setCapStyle(Qt::FlatCap);
      painter->setPen(pen);
      if (lyricsLine()->lyrics()->ticks() > 0)           // melisma
            painter->drawLine(QPointF(), pos2());
      else {                                          // dash(es)
            qreal step  = pos2().x() / (_numOfDashes+1);
            qreal x     = step - _dashLength * .5;
            for (int i = 0; i < _numOfDashes; i++, x += step)
                  painter->drawLine(QPointF(x, 0.0), QPointF(x + _dashLength, 0.0));
            }
      }

}

