#!/usr/bin/ruby
#
# VLLilypondType.reader - Import lilypond files
#

$KCODE = 'u'

require File.dirname($0)+'/plistWriter'
require File.dirname($0)+'/vl'

OUTPUT = {'measures' => []}
CHORDS = []
NOTES  = []
STANZAS= []
MEAS   = []

$RELPITCH = 0
$timeNum  = 4
$timeDenom= 4
$key      = 0
$mode     = '\major'

PITCH = {
  ?c => 0,
  ?d => 2,
  ?e => 4,
  ?f => 5,
  ?g => 7,
  ?a => 9,
  ?b => 11
}

def lyPitch(pitch, base=-1)
  if !pitch || pitch =~ /^[rs]/
    return VL::NoPitch
  end
  p = PITCH[pitch[0]] || 0
  if base > -1
    p += base
  elsif $RELPITCH > 0
    while $RELPITCH-p > 5
      if $RELPITCH-p == 6
        #
        # f -> b choose upward interval, b -> f choose downward
        #
        break if p%12 == PITCH[?f]
      end
      p += 12
    end
  else
    p += 48
  end
  pitch.scan(/'/) {|c| p += 12}
  pitch.scan(/,/) {|c| p -= 12}
  puts "#{pitch}<>#{$RELPITCH} -> #{p}" if $DEBUG
  if base == -1 && $RELPITCH > 0
    $RELPITCH = p
  end
  if pitch =~ /^[ea]s/
    p -= 1
    pitch[0..1] = ""
  end
  pitch.scan('is') { |x| p += 1 }
  pitch.scan('es') { |x| p -= 1 }

  return p
end

$timesNum = 1
$timesDen = 1

def lyDur(dur)
  dur =~ /^(\d+)(\.*)(?:\*(\d+).(\d+))?/
  num = 1
  den = $1.to_i
  if $2 
    (0...$2.length).each do |x|
      den = 2*den
      num = 2*num+1
    end
  end
  if $3 
    num *= $3.to_i
    den *= $4.to_i
  end
  return [num*$timesNum,den*$timesDen]
end

STEPS = {
  ''     => VL::Chord::Maj,
  'm'    => VL::Chord::Min,
  'maj'  => VL::Chord::Maj7, 
  'dim7' => VL::Chord::Dim7,
  'dim'  => VL::Chord::Dim,
  'aug'  => VL::Chord::Aug,
  'sus4' => VL::Chord::Sus4,
  'sus2' => VL::Chord::Sus2,
  'sus'  => VL::Chord::Sus4
}

DEGREE = [
  [VL::Unison, VL::Unison],
  [VL::Min2nd+VL::Maj2nd, VL::Maj2nd],
  [VL::Min3rd+VL::Maj3rd, VL::Maj3rd],
  [VL::Fourth, VL::Fourth],
  [VL::Fifth, VL::Fifth],
  [VL::Aug5th+VL::Dim7th, VL::Dim7th],
  [VL::Min7th+VL::Maj7th, VL::Min7th],
  [VL::Octave, VL::Octave],
  [VL::Min9th+VL::Maj9th, VL::Maj9th],
  [VL::Aug9th+VL::Dim11th, VL::Dim11th],
  [VL::Eleventh, VL::Eleventh],
  [VL::Aug11th+VL::Dim13th, VL::Dim13th],
  [VL::Min13th+VL::Maj13th, VL::Maj13th]
];

MAJORKEY = [
  0,  # C 
  -5, # Db
  2,  # D
  -3, # Eb
  4,  # E
  -1, # F
  -6, # Gb
  1,  # G
  -4, # Ab
  3,  # A
  -2, # Bb
  5,  # B
];

MINORKEY = [
  -3, # Cm  -> Eb
  4,  # Dbm -> E
  -1, # Dm  -> F
  -6, # Ebm -> Gb
  1,  # Em  -> G
  -4, # Fm  -> Ab
  3,  # F#m -> A
  -2, # Gm  -> Bb
  5,  # G#m -> B
  0,  # Am  -> C 
  -5, # Bbm -> Db
  2,  # Bm  -> D
];

def lySteps(steps)
  steps =~ /^(maj|dim7?|aug|sus[42]?|m|)/
  s     = STEPS[$1]
  steps = $'
  if !($1 =~ /\d$/) && steps =~ /^(7|9|11|13)/
    if (s & VL::Maj7th) == 0
      s |= VL::Min7th
    end
    case $1
    when '9'
      s |= VL::Maj9th
    when '11'
      s |= VL::Maj9th+VL::Eleventh
    when '13'
      s |= VL::Maj9th+VL::Eleventh+VL::Maj13th
    end
    steps = $'
  end
  steps.scan(/(\^)?(\d+)([-+])?/) do |ext|
    degree = DEGREE[$2.to_i-1]
    if $1 == '^'
      s &= ~degree[0]
    else
      step = degree[1]
      if $3 == '+'
        step <<= 1
      elsif $3 == '-'
        step >>= 1
      end
      s = (s & ~degree[0]) | step
    end
  end
  return s
end

def parseLilypond
  #
  # Lex
  #
  tokens = []
  INFILE.each do |line|
    line.chomp!.sub!(/%.*/, "")
    line.gsub!(/\\breve/, "1*8/4")
    line.scan(%r$\G\s*(\{|\}|\(|\)|\||=|~|<<|>>|--|#'|#\(|##t|##f|\\\w+|\".*?\"|(\w|'|`)[-+^\w\d.'`,:*/?!]+|.)$) do |token|
      tokens.push(token[0])
    end
  end
  #
  # Parse
  #
  nestLevel = 0
  block     = nil
  level     = -1
  stack     = []
  repeats   = []
  lyrics    = []
  lastDur   = 1
  tied      = false
  repeat    = 0
  lyricFlags= 0
  slur      = false

  while tokens.length > 0
    token = tokens.shift
    #
    # Title, composer, etc.
    #
    if tokens[0] == '='
      case token
      when 'title','composer','poet'
        key   = token=='poet' ? 'lyricist' : token
        value = tokens[1]
        value.sub!(/"(.*)"/, '\1')
        
        OUTPUT[key] = value
        tokens[0..1]= nil
        
        redo
      end
    end
    
    case block
    when '\header', '\paper'
      # Ignore 
    when '\chords', '\chordmode'
      #
      # Possibly chords
      #
      if token.downcase =~ %r{^
                             ([rs] |             # Rest
                              [a-g](?:[ei]?s)?   # g, ges, fis, es, as 
                             ) 
                             (\d+                # 1, 2, 4, 8, 16 ...
                              \.*(?:\*\d+/\d+)?  # ., *3/4
                             )? 
                             (?:\:([-+^:.a-z\d]*))? # :maj9.7-^2
                             (?:/\+?(             # /+
                              [a-g](?:[ei]?s)?   # Root: a, bes, fis, as
                             ))? 
                          $}x
        pitch   = lyPitch($1, 60)
        dur     = $2 || lastDur
        ext     = $3 ? lySteps($3) : 0
        root    = lyPitch($4, 48)
        lastDur = dur
        d       = lyDur(dur)

        chord = {'pitch' => pitch, 'root' => root, 'steps' => ext,
                 'durNum'=> d[0], 'durDenom' => d[1]}
        p token, chord if $DEBUG
        CHORDS.push(chord)
        redo
      end
    when 'voice'
      #
      # Possibly notes
      #
      if token.downcase =~ %r{^
                             ([rs] |             # Rest
                              [a-g](?:[ei]?s)?   # g, ges, fis, es, as 
                              [',]*              # g'''
                             ) 
                             (\d+\.*             # 1, 2, 4, 8, 16 ...
                              (?:\*\d+/\d+)?     # *3/4
                             )? 
                          $}x
        pitch   = lyPitch($1)
        dur     = $2 || lastDur
        lastDur = dur
        d       = lyDur(dur)

        if slur 
          #
          # We don't support slurs, so we turn them into tied notes at the
          # final pitch
          #
          ix = NOTES.size
          tie= true
          while tie do
            break if ix == 0
            note = NOTES[ix -= 1]
            note['pitch'] = pitch;
            note['tied'] ||= 0
            note['tied']  |= VL::TiedWithNext
            tie = (note['tied'] & VL::TiedWithPrev) != 0
          end 
          tied = true
        end
        note = {'pitch' => pitch, 'durNum'=> d[0], 'durDenom' => d[1]}
        note['tied'] = VL::TiedWithPrev if tied
        p token, note if $DEBUG
        tied = false
        NOTES.push(note)
        redo
      elsif token == '~' 
        if note = NOTES.last
          note['tied'] ||= 0
          note['tied'] |= VL::TiedWithNext
        end
        tied = true
      elsif token == '('
        slur = true
      elsif token == ')'
        slur = false
      elsif token == '\repeat' && (tokens[0] == 'volta' || tokens[0] == fold) &&
          tokens[1] =~ /^\d+$/
        stack.push([block, level, "repeat"])
        level        = nestLevel
        repeats.push(repeat)
        repeat       = tokens[1].to_i
        NOTES.push({'begin-repeat' => true, 'times' => repeat})
        tokens[0..1] = nil
        redo
      elsif token == '\alternative'
        inEndings = true
        stack.push([block, level, "endings"])
        level     = nestLevel+1
        voltas    = 0
        curVoltas = nil
        NOTES.push({'begin-ending' => true})
      elsif token == '\times' && tokens[0] =~ %r|^(\d+)/(\d+)|
          $timesNum = $1.to_i
        $timesDen = $2.to_i
        stack.push([block, level, "times"])
        level    = nestLevel
      end
    when '\lyricmode'
      if token == '--'
        lyrics.last[1] |= VL::TiedWithNext if lyrics.size > 0
        lyricFlags     = VL::TiedWithPrev
      elsif token == '\skip'
        p ["", 0] if $DEBUG
        lyrics.push ["", 0]
        lyricFlags     = 0
        if tokens[0] =~ /\d+/
          tokens[0..0] = nil
        end
      elsif token =~ /\\skip\d+/
        p ["", 0] if $DEBUG
        lyrics.push ["", 0]
        lyricFlags     = 0
      elsif token =~ /"(.*)"/
        p [$1, lyricFlags] if $DEBUG
        lyrics.push [$1, lyricFlags]
        lyricFlags     = 0      
      elsif token =~ /^(\w|'|`).*/
        #
        # Handle smart quotes
        #
        token.gsub!(/``/, "\xE2\x80\x9C");
        token.gsub!(/''/, "\xE2\x80\x9D");
        token.gsub!(/'/, "\xE2\x80\x99");
        p [token, lyricFlags] if $DEBUG
        lyrics.push [token, lyricFlags]
        lyricFlags     = 0
      end
    end
    
    #
    # Nesting levels
    #
    case token
    when '{', '<<'
      nestLevel += 1
    when '}', '>>'
      nestLevel -= 1
      if nestLevel <= level
        if lv = stack.pop
          block = lv[0]
          level = lv[1]
          type  = lv[2]
        else
          block = nil
          level = -1
        end
        if type == "repeat"
          if tokens[0] != '\alternative'
            NOTES.push({'end-repeat' => true})
            repeat = repeats.pop
          end
        elsif type == "endings"
          last = tokens[0] == '}'
          if last
            curVoltas = ((1<<repeat) - 1) & ~voltas
          elsif !curVoltas
            curVoltas = 1
            while (voltas&curVoltas) != 0
              curVoltas <<= 1
            end
          end
          NOTES.push({'end-ending' => true, 'volta' => curVoltas, 
                       'last'=>last})
          voltas   |= curVoltas
          curVoltas = 0
          if last
            repeat = repeats.pop
          else
            NOTES.push({'begin-ending' => true})
            stack.push([block, level, "endings"])
            level = nestLevel
          end
        elsif type == "times"
          $timesNum = 1
          $timesDen = 1
        end
      end
    when '\chords', '\header', '\paper', '\lyricmode'
      stack.push([block, level, ""])
      block = token
      level = nestLevel
      STANZAS.push(lyrics= []) if block == '\lyricmode'
    when '\chordmode'
      stack.push([block, level, ""])
      block = '\chords'
      level = nestLevel
    when '\lyricsto'
      tokens[0] = nil 
    when '\new'
      if tokens[0] == "Lyrics"
        if tokens[1] =~ /^\\/
          tokens[0..1] = nil
        else
          stack.push([block, level, ""])
          block = '\lyricmode'
          level = nestLevel
          STANZAS.push(lyrics= [])
          tokens[0..0] = nil
        end
      end
    when '\relative'
      stack.push([block, level, ""])
      if tokens[0] =~ /[a-g](?:[ei]?s)?[',]*/
        $RELPITCH = lyPitch(tokens[0], 48)
        tokens[0..0] = nil
      else
        $RELPITCH = 60
      end
      block     = 'voice'
      level     = nestLevel
    when '\time'
      if tokens[0] =~ %r{(\d+)/(\d+)}
        $timeNum   = $1.to_i
        $timeDenom = $2.to_i
        tokens[0..0] = nil
      end
      if block != 'voice'
        stack.push([block, level, ""])
        block     = 'voice'
        level     = nestLevel-1
      end
    when '\key'
      p    = lyPitch(tokens[0], 0)
      $mode = tokens[1]
      $key  = $mode == '\minor' ? MINORKEY[p] : MAJORKEY[p]
      tokens[0..1] = nil
      if block != 'voice'
        stack.push([block, level, ""])
        block     = 'voice'
        level     = nestLevel-1
      end
    when '\repeat'
      tokens[0..1] = nil
    when '\alternative'
    end
  end
end
  
def peek(where, what)
  return where.first && where.first[what] 
end

def makeMeasures
  measureLen = VL::Fract.new($timeNum, $timeDenom)

  #
  # Make measures
  #
  measCount= -1
  
  while NOTES.size > 0 || CHORDS.size > 0
    measCount         += 1
    meas               = {}
    meas['measure']    = measCount
    meas['properties'] = 0
    if peek(NOTES, 'begin-repeat')
      rep = NOTES.shift
      meas['begin-repeat'] = {'times' => rep['times']}
    end
    if peek(NOTES, 'begin-ending')
      NOTES.shift
      meas['begin-ending'] = {}
    end
    if CHORDS.size > 0
      mchords          = []
      len              = VL::Fract.new(0, 1)
      while len < measureLen && CHORDS.size > 0
        chord    = CHORDS.shift
        chordLen = VL::Fract.new(chord['durNum'], chord['durDenom'])
        if len+chordLen > measureLen
          remLen               = len+chordLen-measureLen
          chordLen            -= remLen
          remChord             = {
            'pitch' => VL::NoPitch, 'root' => VL::NoPitch, 
            'durNum' => remLen.num, 'durDenom' => remLen.denom}
          CHORDS.unshift(remChord)
        end
        mchords.push(chord)
        len += chordLen
      end
      meas['chords']   = mchords
    end
    if NOTES.size > 0
      mnotes           = []
      len              = VL::Fract.new(0, 1)
      while len < measureLen && NOTES.size > 0
        note    = NOTES.shift
        noteLen = VL::Fract.new(note['durNum'], note['durDenom'])
        if len+noteLen > measureLen
          remLen              = len+noteLen-measureLen
          noteLen            -= remLen
          remNote             = note.dup
          remNote['durNum']   = remLen.num
          remNote['durDenom'] = remLen.denom
          remNote['tied']     = (remNote['tied'] || 0) | VL::TiedWithPrev
          note['tied']        = (note['tied'] || 0) | VL::TiedWithNext
          NOTES.unshift(remNote)
        end
        if note['pitch'] != VL::NoPitch &&
            (!note['tied'] || (note['tied'] & VL::TiedWithPrev) == 0)
          ly     = []
          stanza = 0
          STANZAS.each_index do |i|
            lyrics = STANZAS[i]
            if lyrics.size > 0
              stanza = i+1
              syll   = lyrics.shift
              ly.push({'text' => syll[0].gsub('_', ' '), 'kind' => syll[1]})
            else
              ly.push({'text' => '', 'kind' => 0})
            end
          end
          if stanza < ly.size
            ly[stanza..-1] = nil
          end
          note['lyrics'] = ly if stanza > 0
        end
        mnotes.push(note)
        len += noteLen
      end
      meas['melody']   = mnotes
    end
    if peek(NOTES, 'end-ending')
      ending = NOTES.shift
      meas['end-ending'] = {'last' => ending['last'], 'volta' => ending['volta']}
    end
    if peek(NOTES, 'end-repeat')
      NOTES.shift
      meas['end-repeat'] = {}
    end
    MEAS.push(meas)
  end
end
 
begin
  parseLilypond
  makeMeasures

  OUTPUT['measures'] = MEAS
  OUTPUT['properties'] = [{
                            'key'       => $key,
                            'mode'      => $mode == '\minor' ? -1 : 1,
                            'timeNum'   => $timeNum,
                            'timeDenom' => $timeDenom
                          }]                          

  writePlist($stdout, OUTPUT)
rescue => except
  $stderr.print except.message, "\n", except.backtrace.join("\n"), "\n"
end

# Local Variables:
# mode:ruby
# End: