{* UltraStar Deluxe - Karaoke Game
 *
 * UltraStar Deluxe is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING. If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * $URL: https://ultrastardx.svn.sourceforge.net/svnroot/ultrastardx/trunk/src/base/ULyrics.pas $
 * $Id: ULyrics.pas 1939 2009-11-09 00:27:55Z s_alexander $
 *}

unit ULyrics;

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I switches.inc}

uses
  dglOpenGL,
  UCommon,
  UIni,
  UTexture,
  UThemes,
  UMusic;

type
  TLyricsEffect = (lfxSimple, lfxZoom, lfxSlide, lfxBall, lfxShift);
  
  PLyricWord = ^TLyricWord;
  TLyricWord = record
    X:          real;     // left corner
    Width:      real;     // width
    Start:      Longint; // start of the word in quarters (beats)
    Length:     Longint; // length of the word in quarters
    Text:       UTF8String; // text
    Freestyle:  boolean;  // is freestyle?
  end;
  TLyricWordArray = array of TLyricWord;

  TLyricLine = class
    public
      Text:           UTF8String;   // text
      Width:          real;         // width
      Height:         real;         // height
      Words:          TLyricWordArray;   // words in this line
      CurWord:        integer;      // current active word idx (only valid if line is active)
      Start:          integer;      // start of this line in quarters (Note: negative start values are possible due to gap)
      StartNote:      integer;      // start of the first note of this line in quarters
      Length:         integer;      // length in quarters (from start of first to the end of the last note)
      Players:        byte;         // players that should sing that line (bitset, Player1: 1, Player2: 2, Player3: 4)
      LastLine:       boolean;      // is this the last line of the song?

      constructor Create();
      destructor Destroy(); override;
      procedure Reset();
  end;

  TLyricEngine = class
    private
      LastDrawBeat:   real;
      UpperLine:      TLyricLine;    // first line displayed (top)
      LowerLine:      TLyricLine;    // second lind displayed (bottom)
      QueueLine:      TLyricLine;    // third line (will be displayed when lower line is finished)

      BallTex:        TTexture;      // texture of the ball for the lyric effect

      QueueFull:      boolean;       // set to true if the queue is full and a line will be replaced with the next AddLine
      LCounter:       integer;       // line counter

      // Some helper procedures for lyric drawing
      procedure DrawLyrics (Beat: real);
      procedure UpdateLineMetrics(LyricLine: TLyricLine) overload;
      procedure DrawLyricsWords(LyricLine: TLyricLine; X, Y: real; StartWord, EndWord: integer);
      procedure DrawLyricsLine(X, W, Y, H: real; Line: TLyricLine; Beat: real);
      procedure DrawBall(XBall, YBall, Alpha: real);

    public
      // positions, line specific settings
      UpperLineX:     real;       // X start-pos of UpperLine
      UpperLineW:     real;       // Width of UpperLine with icon(s) and text
      UpperLineY:     real;       // Y start-pos of UpperLine
      UpperLineH:     real;       // Max. font-size of lyrics text in UpperLine

      LowerLineX:     real;       // X start-pos of LowerLine
      LowerLineW:     real;       // Width of LowerLine with icon(s) and text
      LowerLineY:     real;       // Y start-pos of LowerLine
      LowerLineH:     real;       // Max. font-size of lyrics text in LowerLine

      // display propertys
      LineColor_en:   TRGBA;      // Color of words in an enabled line
      LineColor_dis:  TRGBA;      // Color of words in a disabled line
      LineColor_act:  TRGBA;      // Color of the active word

      {
      LineOColor_en:   TRGBA;      // Color of outline words in an enabled line
      LineOColor_dis:  TRGBA;      // Color of outline words in a disabled line
      LineOColor_act:  TRGBA;      // Color of outline the active word
      }

      FontFamily:      byte;       // Font family for the lyric text
      FontStyle:       byte;       // Font style for the lyric text

      { // currently not used
       FadeInEffect:   byte;       // Effect for line fading in: 0: No Effect; 1: Fade Effect; 2: Move Upwards from Bottom to Pos
       FadeOutEffect:  byte;       // Effect for line fading out: 0: No Effect; 1: Fade Effect; 2: Move Upwards
      }

      // song specific settings
      BPM:            real;

      // properties to easily read options of this class
      property IsQueueFull: boolean read QueueFull;  // line in queue?
      property LineCounter: integer read LCounter;   // lines that were progressed so far (after last clear)

      procedure AddLine(Line: PLine);              // adds a line to the queue, if there is space
      procedure Draw (Beat: real);                 // draw the current (active at beat) lyrics

      // clears all cached song specific information
      procedure Clear(cBPM: real = 0);

      function GetUpperLine(): TLyricLine;
      function GetLowerLine(): TLyricLine;
      procedure UpdateLineMetrics() overload;

      function GetUpperLineIndex(): integer;

      constructor Create(ULX, ULY, ULW, ULH, LLX, LLY, LLW, LLH: real);
      procedure   LoadTextures;
      destructor  Destroy; override;
  end;

implementation

uses
  SysUtils,
  USkins,
  TextGL,
  UGraphic,
  UDisplay,
  ULog,
  math;

{ TLyricLine }

constructor TLyricLine.Create();
begin
  inherited;
  Reset();
end;

destructor TLyricLine.Destroy();
begin
  SetLength(Words, 0);
  inherited;
end;

procedure TLyricLine.Reset();
begin
  Start     := 0;
  StartNote := 0;
  Length    := 0;
  LastLine  := False;

  Text      := '';
  Width     := 0;

  // duet mode: players of that line (default: all)
  Players   := $FF;

  SetLength(Words, 0);
  CurWord   := -1;
end;


{ TLyricEngine }

{**
 * Initializes the engine.
 *}
constructor TLyricEngine.Create(ULX, ULY, ULW, ULH, LLX, LLY, LLW, LLH: real);
begin
  inherited Create();

  BPM := 0;
  LCounter := 0;
  QueueFull := False;

  UpperLine := TLyricLine.Create;
  LowerLine := TLyricLine.Create;
  QueueLine := TLyricLine.Create;

  LastDrawBeat := 0;

  UpperLineX := ULX;
  UpperLineW := ULW;
  UpperLineY := ULY;
  UpperLineH := ULH;

  LowerLineX := LLX;
  LowerLineW := LLW;
  LowerLineY := LLY;
  LowerLineH := LLH;

  LoadTextures;
end;


{**
 * Frees memory.
 *}
destructor TLyricEngine.Destroy;
begin
  UpperLine.Free;
  LowerLine.Free;
  QueueLine.Free;
  FreeTexture(BallTex);
  inherited;
end;

{**
 * Clears all cached Song specific Information.
 *}
procedure TLyricEngine.Clear(cBPM: real);
begin
  UpperLine.CurWord := -1;
  BPM := cBPM;
  LCounter := 0;
  QueueFull := False;

  LastDrawBeat:=0;
end;


{**
 * Loads textures needed for the drawing the lyrics,
 * player icons, a ball for the ball effect and the lyric indicator.
 *}
procedure TLyricEngine.LoadTextures;
var
  I: Integer;
begin

  // ball for current word hover in ball effect
  BallTex := Texture.LoadTexture(Skin.GetTextureFileName('Ball'), TEXTURE_TYPE_TRANSPARENT, 0);
end;

{**
 * Adds LyricLine to queue.
 * The LyricEngine stores three lines in its queue:
 *   UpperLine: the upper line displayed in the lyrics
 *   LowerLine: the lower line displayed in the lyrics
 *   QueueLine: an offscreen line that precedes LowerLine
 * If the queue is full the next call to AddLine will replace UpperLine with
 * LowerLine, LowerLine with QueueLine and QueueLine with the Line parameter.
 *}
procedure TLyricEngine.AddLine(Line: PLine);
var
  LyricLine: TLyricLine;
  NoteIndex: integer;
begin
  // only add lines, if there is space
  if not IsQueueFull then
  begin
    // set LyricLine to line to write to
    if (LineCounter = 0) then
      LyricLine := UpperLine
    else if (LineCounter = 1) then
      LyricLine := LowerLine
    else
    begin
      // now the queue is full
      LyricLine := QueueLine;
      QueueFull := True;
    end;
  end
  else
  begin // rotate lines (round-robin-like)
    LyricLine := UpperLine;
    UpperLine := LowerLine;
    LowerLine := QueueLine;
    QueueLine := LyricLine;
  end;

  // reset line state
  LyricLine.Reset();
                          
  // check if sentence has notes
  if (Line <> nil) and (Length(Line.Notes) > 0) then
  begin

    // copy values from SongLine to LyricLine
    LyricLine.Start     := Line.StartBeat;
    LyricLine.StartNote := Line.Notes[0].StartBeat;
    LyricLine.Length    := Line.Notes[High(Line.Notes)].StartBeat +
                           Line.Notes[High(Line.Notes)].Duration -
                           Line.Notes[0].StartBeat;
    LyricLine.LastLine  := Line.LastLine;

    // copy words
    SetLength(LyricLine.Words, Length(Line.Notes));
    for NoteIndex := 0 to High(Line.Notes) do
    begin
      LyricLine.Words[NoteIndex].Start     := Line.Notes[NoteIndex].StartBeat;
      LyricLine.Words[NoteIndex].Length    := Line.Notes[NoteIndex].Duration;
      LyricLine.Words[NoteIndex].Text      := Line.Notes[NoteIndex].Text;
      LyricLine.Words[NoteIndex].Freestyle := Line.Notes[NoteIndex].NoteType = ntFreestyle;

      LyricLine.Text := LyricLine.Text + LyricLine.Words[NoteIndex].Text;
    end;

    UpdateLineMetrics(LyricLine);
  end;

  // increase the counter
  Inc(LCounter);
end;

{**
 * Draws Lyrics.
 * Draw just manages the Lyrics, drawing is done by a call of DrawLyrics.
 * @param Beat: current Beat in Quarters
 *}
procedure TLyricEngine.Draw(Beat: real);
begin
  DrawLyrics(Beat);
  LastDrawBeat := Beat;
end;

{**
 * Main Drawing procedure.
 *}
procedure TLyricEngine.DrawLyrics(Beat: real);
begin
  DrawLyricsLine(UpperLineX, UpperLineW, UpperLineY, UpperLineH, UpperLine, Beat);
  DrawLyricsLine(LowerLineX, LowerLineW, LowerLineY, LowerLineH, LowerLine, Beat);
end;


{**
 * Draws the Ball over the LyricLine if needed.
 *}
procedure TLyricEngine.DrawBall(XBall, YBall, Alpha: real);
begin
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glBindTexture(GL_TEXTURE_2D, BallTex.TexNum);

  glColor4f(1, 1, 1, Alpha);
  glBegin(GL_QUADS);
    glTexCoord2f(0, 0); glVertex2f(XBall - 10, YBall);
    glTexCoord2f(0, 1); glVertex2f(XBall - 10, YBall + 20);
    glTexCoord2f(1, 1); glVertex2f(XBall + 10, YBall + 20);
    glTexCoord2f(1, 0); glVertex2f(XBall + 10, YBall);
  glEnd;

  glDisable(GL_BLEND);
  glDisable(GL_TEXTURE_2D);
end;

procedure TLyricEngine.DrawLyricsWords(LyricLine: TLyricLine;
    X, Y: real; StartWord, EndWord: integer);
var
  I: integer;
  PosX: real;
  CurWord: PLyricWord;
begin
  PosX := X - LyricLine.Words[StartWord].X;

  // set word positions and line size and draw the line
  for I := StartWord to EndWord do
  begin
    CurWord := @LyricLine.Words[I];
    SetFontItalic(CurWord.Freestyle);
    SetFontPos(PosX + CurWord.X, Y);
    glPrint(CurWord.Text);
  end;
end;

procedure TLyricEngine.UpdateLineMetrics(LyricLine: TLyricLine);
var
  I: integer;
  PosX: real;
  TextSoFar: UTF8String;
  CurWord: PLyricWord;
  RequestWidth, RequestHeight: real;
begin
  PosX := 0;
  TextSoFar := '';

  // setup font
  SetFontFamily(FontFamily);
  SetFontStyle(FontStyle);
  ResetFont();

  // check if line is lower or upper line and set sizes accordingly
  // Note: at the moment upper and lower lines have same width/height
  // and this function is just called by AddLine() but this may change
  // so that it is called by DrawLyricsLine().
  //if (LyricLine = LowerLine) then
  //begin
  //  RequestWidth  := LowerLineW;
  //  RequestHeight := LowerLineH;
  //end
  //else
  //begin
    RequestWidth  := UpperLineW;
    RequestHeight := UpperLineH;
  //end;

  // set font size to a reasonable value
  LyricLine.Height := RequestHeight * 0.9;
  SetFontSize(LyricLine.Height);
  LyricLine.Width := glTextWidth(LyricLine.Text);

  // change font-size to fit into the lyric bar
  if (LyricLine.Width > RequestWidth) then
  begin
    LyricLine.Height := Trunc(LyricLine.Height * (RequestWidth / LyricLine.Width));
    // the line is very loooong, set font to at least 1px
    if (LyricLine.Height < 1) then
      LyricLine.Height := 1;

    SetFontSize(LyricLine.Height);
    LyricLine.Width := glTextWidth(LyricLine.Text);
  end;

  // calc word positions and widths
  for I := 0 to High(LyricLine.Words) do
  begin
    CurWord := @LyricLine.Words[I];

    // - if current word is italic but not the next word get the width of the
    // italic font to avoid overlapping.
    if CurWord.Freestyle then
      SetFontItalic(true);

    CurWord.Width := glTextWidth(CurWord.Text);
    TextSoFar := TextSoFar + CurWord.Text;
    CurWord.X := PosX + glTextWidth(TextSoFar) - CurWord.Width;
    SetFontItalic(false);

    if (I+1 < Length(LyricLine.Words)) and
       (CurWord.Freestyle <> LyricLine.Words[I+1].Freestyle) then
    begin
      PosX := CurWord.X + CurWord.Width;
      TextSoFar := '';
    end;
  end;
end;

procedure TLyricEngine.UpdateLineMetrics();
begin
  UpdateLineMetrics(UpperLine);
  UpdateLineMetrics(LowerLine);
end;


{**
 * Draws one LyricLine
 *}
procedure TLyricEngine.DrawLyricsLine(X, W, Y, H: real; Line: TLyricLine; Beat: real);
var
  CurWord:        PLyricWord;     // current word
  LastWord:       PLyricWord;     // last word in line
  NextWord:       PLyricWord;     // word following current word
  Progress:       real;           // progress of singing the current word
  LyricX, LyricY: real;           // left/top lyric position
  WordY: real;                    // word y-position
  LyricsEffect: TLyricsEffect;
  Alpha: real;    // alphalevel to fade out at end
  ClipPlaneEq: array[0..3] of GLdouble; // clipping plane for slide effect

  OutlineColor_act: TRGB;                  // outline color actual line
  OutlineColor_dis: TRGB;                  // outline color next line
  OutlineColor_en:  TRGB;                  // outline color sing line
  {// duet mode
  IconSize: real;                 // size of player icons
  IconAlpha: real;                // alpha level of player icons
  }
begin
  // do not draw empty lines
  if (Length(Line.Words) = 0) then
    Exit;

  // set font size and style
  SetFontFamily(FontFamily);
  SetFontStyle(FontStyle);
  ResetFont();

  // hack to OptionsJukebox lyrics demo
  if (Display.CurrentScreen = @ScreenOptionsJukebox) and (FontStyle = 2) then
    SetFontSize(Line.Height * 0.8)
  else
    SetFontSize(Line.Height);

  // set outline
  if (Display.CurrentScreen = @ScreenJukebox) or (Display.CurrentScreen = @ScreenOptionsJukebox) then
  begin

    if (Ini.CurrentJukeboxSingLineOutlineColor <> 2) then
      OutlineColor_act := GetLyricOutlineColor(Ini.CurrentJukeboxSingLineOutlineColor)
    else
    begin
      if (Display.CurrentScreen = @ScreenJukebox) then
        OutlineColor_act := ScreenJukeboxOptions.GetJukeboxOptionsLyricOtherOutlineColor(0)
      else
        OutlineColor_act := GetJukeboxLyricOtherOutlineColor(0);
    end;

    if (Ini.CurrentJukeboxActualLineOutlineColor <> 2) then
      OutlineColor_en := GetLyricOutlineColor(Ini.CurrentJukeboxActualLineOutlineColor)
    else
    begin
      if (Display.CurrentScreen = @ScreenJukebox) then
        OutlineColor_en := ScreenJukeboxOptions.GetJukeboxOptionsLyricOtherOutlineColor(1)
      else
        OutlineColor_en := GetJukeboxLyricOtherOutlineColor(1);
    end;

    if (Ini.CurrentJukeboxNextLineOutlineColor <> 2) then
      OutlineColor_dis := GetLyricOutlineColor(Ini.CurrentJukeboxNextLineOutlineColor)
    else
    begin
      if (Display.CurrentScreen = @ScreenJukebox) then
        OutlineColor_dis := ScreenJukeboxOptions.GetJukeboxOptionsLyricOtherOutlineColor(2)
      else
        OutlineColor_dis := GetJukeboxLyricOtherOutlineColor(2);
    end;

  end
  else
  begin
    OutlineColor_act := GetLyricOutlineColor(0);
    OutlineColor_en := GetLyricOutlineColor(0);
    OutlineColor_dis := GetLyricOutlineColor(0);
  end;

  // center lyrics
  LyricX := X + (W - Line.Width) / 2;
  LyricY := Y + (H - Line.Height) / 2;
  // get lyrics effect

  if (Display.CurrentScreen = @ScreenJukebox) or (Display.CurrentScreen = @ScreenOptionsJukebox) then
    LyricsEffect := TLyricsEffect(Ini.JukeboxEffect)
  else
    LyricsEffect := TLyricsEffect(Ini.LyricsEffect);

  if (Display.CurrentScreen <> @ScreenJukebox)
    and (Display.CurrentScreen <> @ScreenOptionsJukebox) then
    Alpha := 1
  else
  begin
    if (Display.CurrentScreen = @ScreenOptionsJukebox) then
      Alpha := ILyricsAlphaVals[Ini.JukeboxAlpha]
    else
      Alpha := ScreenJukebox.LyricsAlpha;
  end;

  // check if this line is active (at least its first note must be active)
  if (Beat >= Line.StartNote) then
  begin

    // if this line just got active, CurWord is -1,
    // this means we should try to make the first word active
    if (Line.CurWord = -1) then
      Line.CurWord := 0;

    // check if the current active word is still active.
    // Otherwise proceed to the next word if there is one in this line.
    // Note: the max. value of Line.CurWord is High(Line.Words)
    if (Line.CurWord < High(Line.Words)) and
       (Beat >= Line.Words[Line.CurWord + 1].Start) then
    begin
      Inc(Line.CurWord);
    end;

    // determine current and last word in this line.
    // If the end of the line is reached use the last word as current word.
    LastWord := @Line.Words[High(Line.Words)];
    CurWord := @Line.Words[Line.CurWord];
    if (Line.CurWord+1 < Length(Line.Words)) then
      NextWord := @Line.Words[Line.CurWord+1]
    else
      NextWord := nil;

    // calc the progress of the lyrics effect
    Progress := (Beat - CurWord.Start) / CurWord.Length;
    if (Progress >= 1) then
      Progress := 1;
    if (Progress <= 0) then
      Progress := 0;

    // last word of this line finished, but this line did not hide -> fade out
    if (Display.CurrentScreen <> @ScreenJukebox)
      and (Display.CurrentScreen <> @ScreenOptionsJukebox) then
    begin
      if Line.LastLine and
       (Beat > LastWord.Start + LastWord.Length) then
      begin
        Alpha := 1 - (Beat - (LastWord.Start + LastWord.Length)) / 15;
        if (Alpha < 0) then
          Alpha := 0;
      end
    end
    else
    begin
      if (Display.CurrentScreen = @ScreenOptionsJukebox) then
        Alpha := ILyricsAlphaVals[Ini.JukeboxAlpha]
      else
        Alpha := ScreenJukebox.LyricsAlpha;
    end;

    // outline color
    SetOutlineColor(OutlineColor_act.R, OutlineColor_act.G, OutlineColor_act.B, Alpha);

    // draw sentence before current word
    if (LyricsEffect in [lfxSimple, lfxBall, lfxShift]) then
      // only highlight current word and not that ones before in this line
      glColor4f(LineColor_en.R, LineColor_en.G ,LineColor_en.B, Alpha)
    else
      glColor4f(LineColor_act.R, LineColor_act.G ,LineColor_act.B, Alpha);

    DrawLyricsWords(Line, LyricX, LyricY, 0, Line.CurWord-1);

    // draw rest of sentence (without current word)
    glColor4f(LineColor_en.R, LineColor_en.G ,LineColor_en.B, Alpha);

    if (NextWord <> nil) then
    begin

      // outline color
      SetOutlineColor(OutlineColor_en.R, OutlineColor_en.G, OutlineColor_en.B, Alpha);

      DrawLyricsWords(Line, LyricX + NextWord.X, LyricY,
                      Line.CurWord+1, High(Line.Words));
    end;

    // outline color
    SetOutlineColor(OutlineColor_act.R, OutlineColor_act.G, OutlineColor_act.B, Alpha);

    // draw current word
    if ((LyricsEffect in [lfxSimple, lfxBall, lfxShift])
      // hack to OptionsJukebox lyrics demo
      or ((LyricsEffect = lfxSlide) and (Display.CurrentScreen = @ScreenOptionsJukebox))) then
    begin
      if (LyricsEffect = lfxShift) then
        WordY := LyricY - 8 * (1-Progress)
      else
        WordY := LyricY;

      // change the color of the current word
      glColor4f(LineColor_act.R, LineColor_act.G ,LineColor_act.B, Alpha);

      DrawLyricsWords(Line, LyricX + CurWord.X, WordY, Line.CurWord, Line.CurWord);
    end
    // change color and zoom current word
    else if (LyricsEffect = lfxZoom) then
    begin
      glPushMatrix;

      // zoom at word center
      glTranslatef(LyricX + CurWord.X + CurWord.Width/2,
                   LyricY + Line.Height/2, 0);
      glScalef(1.0 + (1-Progress) * 0.5, 1.0 + (1-Progress) * 0.5, 1);

      glColor4f(LineColor_act.R, LineColor_act.G ,LineColor_act.B, Alpha);

      DrawLyricsWords(Line, -CurWord.Width/2, -Line.Height/2, Line.CurWord, Line.CurWord);

      glPopMatrix;
    end
    // split current word into active and non-active part
    else if (LyricsEffect = lfxSlide) then
    begin
      // enable clipping and set clip equation coefficients to zeros
      glEnable(GL_CLIP_PLANE0);
      FillChar(ClipPlaneEq[0], SizeOf(ClipPlaneEq), 0);

      glPushMatrix;
      glTranslatef(LyricX + CurWord.X, LyricY, 0);

      // clip non-active right part of the current word
      ClipPlaneEq[0] := -1;
      ClipPlaneEq[3] := CurWord.Width * Progress;
      glClipPlane(GL_CLIP_PLANE0, @ClipPlaneEq);
      // and draw active left part
      glColor4f(LineColor_act.R, LineColor_act.G ,LineColor_act.B, Alpha);

      DrawLyricsWords(Line, 0, 0, Line.CurWord, Line.CurWord);

      // clip active left part of the current word
      ClipPlaneEq[0] := -ClipPlaneEq[0];
      ClipPlaneEq[3] := -ClipPlaneEq[3];
      glClipPlane(GL_CLIP_PLANE0, @ClipPlaneEq);
      // and draw non-active right part
      glColor4f(LineColor_en.R, LineColor_en.G ,LineColor_en.B, Alpha);

      DrawLyricsWords(Line, 0, 0, Line.CurWord, Line.CurWord);

      glPopMatrix;

      glDisable(GL_CLIP_PLANE0);
    end;

    // draw the ball onto the current word
    if (LyricsEffect = lfxBall) then
    begin
      DrawBall(LyricX + CurWord.X + CurWord.Width * Progress,
               LyricY - 15 - 15*sin(Progress * Pi), Alpha);
    end;

  end
  else
  begin
    // this section is called if the whole line can be drawn at once and no
    // word is highlighted.

    // enable the upper, disable the lower line
    if (Line = UpperLine) then
    begin
      // outline color
      SetOutlineColor(OutlineColor_en.R, OutlineColor_en.G, OutlineColor_en.B, Alpha);

      glColor4f(LineColor_en.R, LineColor_en.G ,LineColor_en.B, Alpha);
    end
    else
    begin
      // outline color
      SetOutlineColor(OutlineColor_dis.R, OutlineColor_dis.G, OutlineColor_dis.B, Alpha);

      glColor4f(LineColor_dis.R, LineColor_dis.G ,LineColor_dis.B, Alpha);
    end;

    DrawLyricsWords(Line, LyricX, LyricY, 0, High(Line.Words));

  end;

  // reset
  SetOutlineColor(0,0,0,-1);

end;

{**
 * @returns a reference to the upper line
 *}
function TLyricEngine.GetUpperLine(): TLyricLine;
begin
  Result := UpperLine;
end;

{**
 * @returns a reference to the lower line
 *}
function TLyricEngine.GetLowerLine(): TLyricLine;
begin
  Result := LowerLine;
end;

{**
 * @returns the index of the upper line
 *}
function TLyricEngine.GetUpperLineIndex(): integer;
const
  QUEUE_SIZE = 3;
begin
  // no line in queue
  if (LineCounter <= 0) then
    Result := -1
  // no line has been removed from queue yet
  else if (LineCounter <= QUEUE_SIZE) then
    Result := 0
  // lines have been removed from queue already
  else
    Result := LineCounter - QUEUE_SIZE;
end;

end.

