# VexTab Artist
# Copyright 2012 Mohit Cheppudira <mohit@muthanna.com>
#
# This class is responsible for rendering the elements
# parsed by Vex.Flow.VexTab.


Vex = require 'vexflow'
_ = require 'lodash'

class Artist
  @DEBUG = false
  L = (args...) -> console?.log("(Vex.Flow.Artist)", args...) if Artist.DEBUG

  @NOLOGO = false

  constructor: (@x, @y, @width, options) ->
    @options =
      font_face: "Arial"
      font_size: 10
      font_style: null
      bottom_spacing: 20 + (if Artist.NOLOGO then 0 else 10)
      tab_stave_lower_spacing: 10
      note_stave_lower_spacing: 0
      scale: 1.0
    _.extend(@options, options) if options?
    @reset()

  reset: ->
    @tuning = new Vex.Flow.Tuning()
    @key_manager = new Vex.Flow.KeyManager("C")
    @music_api = new Vex.Flow.Music()

    # User customizations
    @customizations =
      "font-size": @options.font_size
      "font-face": @options.font_face
      "font-style": @options.font_style
      "annotation-position": "bottom"
      "scale": @options.scale
      "width": @width
      "stave-distance": 0
      "space": 0
      "player": "false"
      "tempo": 120
      "instrument": "acoustic_grand_piano"
      "accidentals": "standard"  # standard / cautionary
      "tab-stems": "false"
      "tab-stem-direction": "up"
      "beam-rests": "true"
      "beam-stemlets": "true"
      "beam-middle-only": "false"
      "connector-space": 0

    # Generated elements
    @staves = []
    @tab_articulations = []
    @stave_articulations = []

    # Voices for player
    @player_voices = []

    # Current state
    @last_y = @y
    @current_duration = "q"
    @current_clef = "treble"
    @current_bends = {}
    @current_octave_shift = 0
    @bend_start_index = null
    @bend_start_strings = null
    @rendered = false
    @renderer_context = null

  attachPlayer: (player) ->
    @player = player

  setOptions: (options) ->
    L "setOptions: ", options
    # Set @customizations
    valid_options = _.keys(@customizations)
    for k, v of options
      if k in valid_options
        @customizations[k] = v
      else
        throw new Vex.RERR("ArtistError", "Invalid option '#{k}'")

    @last_y += parseInt(@customizations.space, 10)
    @last_y += 15 if @customizations.player is "true"

  getPlayerData: ->
    voices: @player_voices
    context: @renderer_context
    scale: @customizations.scale

  parseBool = (str) ->
    return (str == "true")

  formatAndRender = (ctx, tab, score, text_notes, customizations, options) ->
    tab_stave = tab.stave if tab?
    score_stave = score.stave if score?

    tab_voices = []
    score_voices = []
    text_voices = []
    beams = []
    format_stave = null
    text_stave = null

    beam_config =
      beam_rests: parseBool(customizations["beam-rests"])
      show_stemlets: parseBool(customizations["beam-stemlets"])
      beam_middle_only: parseBool(customizations["beam-middle-only"])
      groups: options.beam_groups

    if tab?
      multi_voice = if (tab.voices.length > 1) then true else false
      for notes, i in tab.voices
        continue if _.isEmpty(notes)
        _.each(notes, (note) -> note.setStave(tab_stave))
        voice = new Vex.Flow.Voice(Vex.Flow.TIME4_4).
          setMode(Vex.Flow.Voice.Mode.SOFT)
        voice.addTickables notes
        tab_voices.push voice

        if customizations["tab-stems"] == "true"
          if multi_voice
            beam_config.stem_direction = if i == 0 then 1 else -1
          else
            beam_config.stem_direction = if customizations["tab-stem-direction"] == "down" then -1 else 1

          beam_config.beam_rests = false
          beams = beams.concat(Vex.Flow.Beam.generateBeams(voice.getTickables(), beam_config))

      format_stave = tab_stave
      text_stave = tab_stave

    beam_config.beam_rests = parseBool(customizations["beam-rests"])

    if score?
      multi_voice = if (score.voices.length > 1) then true else false
      for notes, i in score.voices
        continue if _.isEmpty(notes)
        stem_direction = if i == 0 then 1 else -1
        _.each(notes, (note) -> note.setStave(score_stave))

        voice = new Vex.Flow.Voice(Vex.Flow.TIME4_4).
          setMode(Vex.Flow.Voice.Mode.SOFT)
        voice.addTickables notes
        score_voices.push voice
        if multi_voice
          beam_config.stem_direction = stem_direction
          beams = beams.concat(Vex.Flow.Beam.generateBeams(notes, beam_config))
        else
          beam_config.stem_direction = null
          beams = beams.concat(Vex.Flow.Beam.generateBeams(notes, beam_config))

      format_stave = score_stave
      text_stave = score_stave

    for notes in text_notes
      continue if _.isEmpty(notes)
      _.each(notes, (voice) -> voice.setStave(text_stave))
      voice = new Vex.Flow.Voice(Vex.Flow.TIME4_4).
          setMode(Vex.Flow.Voice.Mode.SOFT)
      voice.addTickables notes
      text_voices.push voice

    if format_stave?
      format_voices = []
      formatter = new Vex.Flow.Formatter()
      align_rests = false

      if tab?
        formatter.joinVoices(tab_voices) unless _.isEmpty(tab_voices)
        format_voices = tab_voices

      if score?
        formatter.joinVoices(score_voices) unless _.isEmpty(score_voices)
        format_voices = format_voices.concat(score_voices)
        align_rests = true if score_voices.length > 1

      if not _.isEmpty(text_notes) and not _.isEmpty(text_voices)
        formatter.joinVoices(text_voices)
        format_voices = format_voices.concat(text_voices)

      formatter.formatToStave(format_voices, format_stave, {align_rests: align_rests}) unless _.isEmpty(format_voices)

      _.each(tab_voices, (voice) -> voice.draw(ctx, tab_stave)) if tab?
      _.each(score_voices, (voice) -> voice.draw(ctx, score_stave)) if score?
      _.each(beams, (beam) -> beam.setContext(ctx).draw())
      _.each(text_voices, (voice) -> voice.draw(ctx, text_stave)) if not _.isEmpty(text_notes)

      if tab? and score?
        (new Vex.Flow.StaveConnector(score.stave, tab.stave)).setContext(ctx).draw()

      if score? then score_voices else tab_voices

  render: (renderer) ->
    L "Render: ", @options
    @closeBends()
    renderer.resize(@customizations.width * @customizations.scale,
        (@last_y + @options.bottom_spacing) * @customizations.scale)
    ctx = renderer.getContext()
    ctx.scale(@customizations.scale, @customizations.scale)
    ctx.clear()
    ctx.setFont(@options.font_face, @options.font_size, "")

    @renderer_context = ctx

    setBar = (stave, notes) ->
      last_note = _.last(notes)
      if last_note instanceof Vex.Flow.BarNote
        notes.pop()
        stave.setEndBarType(last_note.getType())

    for stave in @staves
      L "Rendering staves."
      # If the last note is a bar, then remove it and render it as a stave modifier.
      setBar(stave.tab, stave.tab_notes) if stave.tab?
      setBar(stave.note, stave.note_notes) if stave.note?

      stave.tab.setContext(ctx).draw() if stave.tab?
      stave.note.setContext(ctx).draw() if stave.note?

      stave.tab_voices.push(stave.tab_notes)
      stave.note_voices.push(stave.note_notes)

      voices = formatAndRender(ctx,
                      if stave.tab? then {stave: stave.tab, voices: stave.tab_voices} else null,
                      if stave.note? then {stave: stave.note, voices: stave.note_voices} else null,
                      stave.text_voices,
                      @customizations,
                      {beam_groups: stave.beam_groups})

      @player_voices.push(voices)

    L "Rendering tab articulations."
    for articulation in @tab_articulations
      articulation.setContext(ctx).draw()

    L "Rendering note articulations."
    for articulation in @stave_articulations
      articulation.setContext(ctx).draw()

    if @player?
      if @customizations.player is "true"
        @player.setTempo(parseInt(@customizations.tempo, 10))
        @player.setInstrument(@customizations.instrument)
        @player.render()
      else
        @player.removeControls()
    @rendered = true

    unless Artist.NOLOGO
      LOGO = ""
      width = ctx.measureText(LOGO).width
      ctx.save()
      ctx.setFont("Times", 10, "italic")
      ctx.fillText(LOGO, (@customizations.width - width) / 2, @last_y + 25)
      ctx.restore()

  isRendered: -> @rendered

  draw: (renderer) -> @render renderer

  # Given a fret/string pair, returns a note, octave, and required accidentals
  # based on current guitar tuning and stave key. The accidentals may be different
  # for repeats of the same notes because they get set (or cancelled) by the Key
  # Manager.
  getNoteForFret: (fret, string) ->
    spec = @tuning.getNoteForFret(fret, string)
    spec_props = Vex.Flow.keyProperties(spec)

    selected_note = @key_manager.selectNote(spec_props.key)
    accidental = null

    # Do we need to specify an explicit accidental?
    switch @customizations.accidentals
      when "standard"
        if selected_note.change
          accidental = if selected_note.accidental? then selected_note.accidental else "n"
      when "cautionary"
        if selected_note.change
          accidental = if selected_note.accidental? then selected_note.accidental else "n"
        else
          accidental = if selected_note.accidental? then selected_note.accidental + "_c"
      else
        throw new Vex.RERR("ArtistError", "Invalid value for option 'accidentals': #{@customizations.accidentals}")

    new_note = selected_note.note
    new_octave = spec_props.octave

    # TODO(0xfe): This logic should probably be in the KeyManager code
    old_root = @music_api.getNoteParts(spec_props.key).root
    new_root = @music_api.getNoteParts(selected_note.note).root

    # Figure out if there's an octave shift based on what the Key
    # Manager just told us about the note.
    if new_root == "b" and old_root == "c"
      new_octave--
    else if new_root == "c" and old_root == "b"
      new_octave++

    return [new_note, new_octave, accidental]

  getNoteForABC: (abc, string) ->
    key = abc.key
    octave = string
    accidental = abc.accidental
    accidental += "_#{abc.accidental_type}" if abc.accidental_type?
    return [key, octave, accidental]

  addStaveNote: (note_params) ->
    params =
      is_rest: false
      play_note: null

    _.extend(params, note_params)
    stave_notes = _.last(@staves).note_notes
    stave_note = new Vex.Flow.StaveNote({
      keys: params.spec
      duration: @current_duration + (if params.is_rest then "r" else "")
      clef: if params.is_rest then "treble" else @current_clef
      auto_stem: if params.is_rest then false else true
    })
    for acc, index in params.accidentals
      if acc?
        parts = acc.split("_")
        new_accidental = new Vex.Flow.Accidental(parts[0])

        if parts.length > 1 and parts[1] == "c"
          new_accidental.setAsCautionary()

        stave_note.addAccidental(index, new_accidental)

    if @current_duration[@current_duration.length - 1] == "d"
      stave_note.addDotToAll()

    stave_note.setPlayNote(params.play_note) if params.play_note?
    stave_notes.push stave_note

  addTabNote: (spec, play_note=null) ->
    tab_notes = _.last(@staves).tab_notes
    new_tab_note = new Vex.Flow.TabNote({
      positions: spec,
      duration: @current_duration
      }, (@customizations["tab-stems"] == "true")
    )
    new_tab_note.setPlayNote(play_note) if play_note?
    tab_notes.push new_tab_note

    if @current_duration[@current_duration.length - 1] == "d"
      new_tab_note.addDot()

  makeDuration = (time, dot) -> time + (if dot then "d" else "")
  setDuration: (time, dot=false) ->
    t = time.split(/\s+/)
    L "setDuration: ", t[0], dot
    @current_duration = makeDuration(t[0], dot)

  addBar: (type) ->
    L "addBar: ", type
    @closeBends()
    @key_manager.reset()
    stave = _.last(@staves)

    TYPE = Vex.Flow.Barline.type
    type = switch type
      when "single"
        TYPE.SINGLE
      when "double"
        TYPE.DOUBLE
      when "end"
        TYPE.END
      when "repeat-begin"
        TYPE.REPEAT_BEGIN
      when "repeat-end"
        TYPE.REPEAT_END
      when "repeat-both"
        TYPE.REPEAT_BOTH
      else
        TYPE.SINGLE

    bar_note = new Vex.Flow.BarNote().setType(type)
    stave.tab_notes.push(bar_note)
    stave.note_notes.push(bar_note) if stave.note?

  makeBend = (from_fret, to_fret) ->
    direction = Vex.Flow.Bend.UP
    text = ""

    if parseInt(from_fret, 10) > parseInt(to_fret, 10)
      direction = Vex.Flow.Bend.DOWN
    else
      text = switch Math.abs(to_fret - from_fret)
        when 1 then "1/2"
        when 2 then "Full"
        when 3 then "1 1/2"
        else "Bend to #{to_fret}"

    return {type: direction, text: text}

  openBends: (first_note, last_note, first_indices, last_indices) ->
    L "openBends", first_note, last_note, first_indices, last_indices
    tab_notes = _.last(@staves).tab_notes

    start_note = first_note
    start_indices = first_indices
    if _.isEmpty(@current_bends)
      @bend_start_index = tab_notes.length - 2
      @bend_start_strings = first_indices
    else
      start_note = tab_notes[@bend_start_index]
      start_indices = @bend_start_strings

    first_frets = start_note.getPositions()
    last_frets = last_note.getPositions()
    for index, i in start_indices
      last_index = last_indices[i]
      from_fret = first_note.getPositions()[first_indices[i]]
      to_fret = last_frets[last_index]
      @current_bends[index] ?= []
      @current_bends[index].push makeBend(from_fret.fret, to_fret.fret)

  # Close and apply all the bends to the last N notes.
  closeBends: (offset=1) ->
    return unless @bend_start_index?
    L "closeBends(#{offset})"
    tab_notes = _.last(@staves).tab_notes
    for k, v of @current_bends
      phrase = []
      for bend in v
        phrase.push bend
      tab_notes[@bend_start_index].addModifier(
        new Vex.Flow.Bend(null, null, phrase), k)

    # Replace bent notes with ghosts (make them invisible)
    for tab_note in tab_notes[@bend_start_index+1..((tab_notes.length - 2) + offset)]
      tab_note.setGhost(true)

    @current_bends = {}
    @bend_start_index = null

  makeTuplets: (tuplets, notes) ->
    L "makeTuplets", tuplets, notes
    notes ?= tuplets
    return unless _.last(@staves).note
    stave_notes = _.last(@staves).note_notes
    tab_notes = _.last(@staves).tab_notes

    throw new Vex.RERR("ArtistError", "Not enough notes for tuplet") if stave_notes.length < notes
    modifier = new Vex.Flow.Tuplet(stave_notes[stave_notes.length - notes..], {num_notes: tuplets})
    @stave_articulations.push modifier

    # Creating a Vex.Flow.Tuplet corrects the ticks for the notes, so it needs to
    # be created whether or not it gets rendered. Below, if tab stems are not required
    # the created tuplet is simply thrown away.
    tab_modifier = new Vex.Flow.Tuplet(tab_notes[tab_notes.length - notes..], {num_notes: tuplets})
    if @customizations["tab-stems"] == "true"
      @tab_articulations.push tab_modifier

  getFingering = (text) -> text.match(/^\.fingering\/([^.]+)\./)
  makeFingering: (text) ->
    parts = getFingering(text)
    POS = Vex.Flow.Modifier.Position
    fingers = []
    fingering = []

    if parts?
      fingers = (p.trim() for p in parts[1].split(/-/))
    else
      return null

    badFingering = -> new Vex.RERR("ArtistError", "Bad fingering: #{parts[1]}")

    for finger in fingers
      pieces = finger.match(/(\d+):([ablr]):([fs]):([^-.]+)/)
      throw badFingering() unless pieces?

      note_number = parseInt(pieces[1], 10) - 1
      position = POS.RIGHT
      switch pieces[2]
        when "l"
          position = POS.LEFT
        when "r"
          position = POS.RIGHT
        when "a"
          position = POS.ABOVE
        when "b"
          position = POS.BELOW

      modifier = null
      number = pieces[4]
      switch pieces[3]
        when "s"
          modifier = new Vex.Flow.StringNumber(number).setPosition(position)
        when "f"
          modifier = new Vex.Flow.FretHandFinger(number).setPosition(position)

      fingering.push({num: note_number, modifier: modifier})

    return fingering

  getStrokeParts = (text) -> text.match(/^\.stroke\/([^.]+)\./)
  makeStroke: (text) ->
    parts = getStrokeParts(text)
    TYPE = Vex.Flow.Stroke.Type
    type = null

    if parts?
      switch parts[1]
        when "bu"
          type = TYPE.BRUSH_UP
        when "bd"
          type = TYPE.BRUSH_DOWN
        when "ru"
          type = TYPE.ROLL_UP
        when "rd"
          type = TYPE.ROLL_DOWN
        when "qu"
          type = TYPE.RASQUEDO_UP
        when "qd"
          type = TYPE.RASQUEDO_DOWN
        else
          throw new Vex.RERR("ArtistError", "Invalid stroke type: #{parts[1]}")
      return new Vex.Flow.Stroke(type)
    else
      return null

  getScoreArticulationParts = (text) -> text.match(/^\.(a[^\/]*)\/(t|b)[^.]*\./)
  makeScoreArticulation: (text) ->
    parts = getScoreArticulationParts(text)
    if parts?
      type = parts[1]
      position = parts[2]

      POSTYPE = Vex.Flow.Modifier.Position
      pos = if position is "t" then POSTYPE.ABOVE else POSTYPE.BELOW
      return new Vex.Flow.Articulation(type).setPosition(pos)
    else return null

  makeAnnotation: (text) ->
    font_face = @customizations["font-face"]
    font_size = @customizations["font-size"]
    font_style = @customizations["font-style"]
    aposition = @customizations["annotation-position"]

    VJUST = Vex.Flow.Annotation.VerticalJustify
    default_vjust = if aposition is "top" then VJUST.TOP else VJUST.BOTTOM

    makeIt = (text, just=default_vjust) ->
      new Vex.Flow.Annotation(text).
        setFont(font_face, font_size, font_style).
        setVerticalJustification(just)

    parts = text.match(/^\.([^-]*)-([^-]*)-([^.]*)\.(.*)/)
    if parts?
      font_face = parts[1]
      font_size = parts[2]
      font_style = parts[3]
      text = parts[4]
      return if text then makeIt(text) else null

    parts = text.match(/^\.([^.]*)\.(.*)/)
    if parts?
      just = default_vjust
      text = parts[2]
      switch parts[1]
        when "big"
          font_style = "bold"
          font_size = "14"
        when "italic", "italics"
          font_face = "Times"
          font_style = "italic"
        when "medium"
          font_size = "12"
        when "top"
          just = VJUST.TOP
          @customizations["annotation-position"] = "top"
        when "bottom"
          just = VJUST.BOTTOM
          @customizations["annotation-position"] = "bottom"
      return if text then makeIt(text, just) else null

    return makeIt(text)

  addAnnotations: (annotations) ->
    stave = _.last(@staves)
    stave_notes = stave.note_notes
    tab_notes = stave.tab_notes

    if annotations.length > tab_notes.length
      throw new Vex.RERR("ArtistError", "More annotations than note elements")

    # Add text annotations
    if stave.tab
      for tab_note, i in tab_notes[tab_notes.length - annotations.length..]
        if getScoreArticulationParts(annotations[i])
          score_articulation = @makeScoreArticulation(annotations[i])
          tab_note.addModifier(score_articulation, 0)
        else if getStrokeParts(annotations[i])
          stroke = @makeStroke(annotations[i])
          tab_note.addModifier(stroke, 0)
        else
          annotation = @makeAnnotation(annotations[i])
          tab_note.addModifier(@makeAnnotation(annotations[i]), 0) if annotation
    else
      for note, i in stave_notes[stave_notes.length - annotations.length..]
        unless getScoreArticulationParts(annotations[i])
          annotation = @makeAnnotation(annotations[i])
          note.addAnnotation(0, @makeAnnotation(annotations[i])) if annotation

    # Add glyph articulations, strokes, or fingerings on score
    if stave.note
      for note, i in stave_notes[stave_notes.length - annotations.length..]
        score_articulation = @makeScoreArticulation(annotations[i])
        note.addArticulation(0, score_articulation) if score_articulation?

        stroke = @makeStroke(annotations[i])
        note.addStroke(0, stroke) if stroke?

        fingerings = @makeFingering(annotations[i])
        if fingerings?
          try
            (note.addModifier(fingering.num, fingering.modifier) for fingering in fingerings)
          catch e
            throw new Vex.RERR("ArtistError", "Bad note number in fingering: #{annotations[i]}")

  addTabArticulation: (type, first_note, last_note, first_indices, last_indices) ->
    L "addTabArticulations: ", type, first_note, last_note, first_indices, last_indices

    if type == "t"
      last_note.addModifier(
        new Vex.Flow.Annotation("T").
          setVerticalJustification(Vex.Flow.Annotation.VerticalJustify.BOTTOM))

    if _.isEmpty(first_indices) and _.isEmpty(last_indices) then return

    articulation = null

    if type == "s"
      articulation = new Vex.Flow.TabSlide({
        first_note: first_note
        last_note: last_note
        first_indices: first_indices
        last_indices: last_indices
        })

    if type in ["h", "p"]
      articulation = new Vex.Flow.TabTie({
        first_note: first_note
        last_note: last_note
        first_indices: first_indices
        last_indices: last_indices
        }, type.toUpperCase())

    if type in ["T", "t"]
      articulation = new Vex.Flow.TabTie({
        first_note: first_note
        last_note: last_note
        first_indices: first_indices
        last_indices: last_indices
        }, " ")

    if type == "b"
      @openBends(first_note, last_note, first_indices, last_indices)

    @tab_articulations.push articulation if articulation?

  addStaveArticulation: (type, first_note, last_note, first_indices, last_indices) ->
    L "addStaveArticulations: ", type, first_note, last_note, first_indices, last_indices
    articulation = null
    if type in ["b", "s", "h", "p", "t", "T"]
      articulation = new Vex.Flow.StaveTie({
        first_note: first_note
        last_note: last_note
        first_indices: first_indices
        last_indices: last_indices
        })

    @stave_articulations.push articulation if articulation?

  # This gets the previous (second-to-last) non-bar non-ghost note.
  getPreviousNoteIndex: ->
    tab_notes = _.last(@staves).tab_notes
    index = 2
    while index <= tab_notes.length
      note = tab_notes[tab_notes.length - index]
      return (tab_notes.length - index) if note instanceof Vex.Flow.TabNote
      index++

    return -1

  addDecorator: (decorator) ->
    L "addDecorator: ", decorator
    return unless decorator?

    stave = _.last(@staves)
    tab_notes = stave.tab_notes
    score_notes = stave.note_notes
    modifier = null
    score_modifier = null

    if decorator == "v"
      modifier = new Vex.Flow.Vibrato()
    if decorator == "V"
      modifier = new Vex.Flow.Vibrato().setHarsh(true)
    if decorator == "u"
      modifier = new Vex.Flow.Articulation("a|").setPosition(Vex.Flow.Modifier.Position.BOTTOM)
      score_modifier = new Vex.Flow.Articulation("a|").setPosition(Vex.Flow.Modifier.Position.BOTTOM)
    if decorator == "d"
      modifier = new Vex.Flow.Articulation("am").setPosition(Vex.Flow.Modifier.Position.BOTTOM)
      score_modifier = new Vex.Flow.Articulation("am").setPosition(Vex.Flow.Modifier.Position.BOTTOM)

    _.last(tab_notes).addModifier(modifier, 0) if modifier?
    _.last(score_notes)?.addArticulation(0, score_modifier) if score_modifier?


  addArticulations: (articulations) ->
    L "addArticulations: ", articulations
    stave = _.last(@staves)
    tab_notes = stave.tab_notes
    stave_notes = stave.note_notes
    if _.isEmpty(tab_notes) or _.isEmpty(articulations)
      @closeBends(0)
      return

    current_tab_note = _.last(tab_notes)

    has_bends = false
    for valid_articulation in ["b", "s", "h", "p", "t", "T", "v", "V"]
      indices = (i for art, i in articulations when art? and art == valid_articulation)
      if _.isEmpty(indices) then continue

      if valid_articulation is "b" then has_bends = true
      prev_index = @getPreviousNoteIndex()
      if prev_index is -1
        prev_tab_note = null
        prev_indices = null
      else
        prev_tab_note = tab_notes[prev_index]
        # Figure out which strings the articulations are on
        this_strings = (n.str for n, i in current_tab_note.getPositions() when i in indices)

        # Only allows articulations where both notes are on the same strings
        valid_strings = (pos.str for pos, i in prev_tab_note.getPositions() when pos.str in this_strings)

        # Get indices of articulated notes on previous chord
        prev_indices = (i for n, i in prev_tab_note.getPositions() when n.str in valid_strings)

        # Get indices of articulated notes on current chord
        current_indices = (i for n, i in current_tab_note.getPositions() when n.str in valid_strings)

      if stave.tab?
        @addTabArticulation(valid_articulation,
          prev_tab_note, current_tab_note, prev_indices, current_indices)

      if stave.note?
        @addStaveArticulation(valid_articulation,
          stave_notes[prev_index], _.last(stave_notes),
          prev_indices, current_indices)

    @closeBends(0) unless has_bends

  addRest: (params) ->
    L "addRest: ", params
    @closeBends()

    if params["position"] == 0
      @addStaveNote
        spec: ["r/4"]
        accidentals: []
        is_rest: true
    else
      position = @tuning.getNoteForFret((parseInt(params["position"], 10) + 5) * 2, 6)
      @addStaveNote
        spec: [position]
        accidentals: []
        is_rest: true

    tab_notes = _.last(@staves).tab_notes
    if @customizations["tab-stems"] == "true"
      tab_note = new Vex.Flow.StaveNote({
        keys: [position || "r/4"]
        duration: @current_duration + "r"
        clef: "treble"
        auto_stem: false
      })
      if @current_duration[@current_duration.length - 1] == "d"
        tab_note.addDot(0)
      tab_notes.push tab_note
    else
      tab_notes.push new Vex.Flow.GhostNote(@current_duration)

  addChord: (chord, chord_articulation, chord_decorator) ->
    return if _.isEmpty(chord)
    L "addChord: ", chord
    stave = _.last(@staves)

    specs = []          # The stave note specs
    play_notes = []     # Notes to be played by audio players
    accidentals = []    # The stave accidentals
    articulations = []  # Articulations (ties, bends, taps)
    decorators = []     # Decorators (vibratos, harmonics)
    tab_specs = []      # The tab notes
    durations = []      # The duration of each position
    num_notes = 0

    # Chords are complicated, because they can contain little
    # lines one each string. We need to keep track of the motion
    # of each line so we know which tick they belong in.
    current_string = _.first(chord).string
    current_position = 0

    for note in chord
      num_notes++
      if note.abc? or note.string != current_string
        current_position = 0
        current_string = note.string

      unless specs[current_position]?
        # New position. Create new element arrays for this
        # position.
        specs[current_position] = []
        play_notes[current_position] = []
        accidentals[current_position] = []
        tab_specs[current_position] = []
        articulations[current_position] = []
        decorators[current_position] = []

      [new_note, new_octave, accidental] = [null, null, null]

      play_note = null

      if note.abc?
        octave = if note.octave? then note.octave else note.string
        [new_note, new_octave, accidental] = @getNoteForABC(note.abc, octave)
        if accidental?
          acc = accidental.split("_")[0]
        else
          acc = ""

        play_note = "#{new_note}#{acc}"
        note.fret = 'X' unless note.fret?
      else if note.fret?
        [new_note, new_octave, accidental] = @getNoteForFret(note.fret, note.string)
        play_note = @tuning.getNoteForFret(note.fret, note.string).split("/")[0]
      else
        throw new Vex.RERR("ArtistError", "No note specified")

      play_octave = parseInt(new_octave, 10) + @current_octave_shift

      current_duration = if note.time? then {time: note.time, dot: note.dot} else null
      specs[current_position].push "#{new_note}/#{new_octave}"
      play_notes[current_position].push "#{play_note}/#{play_octave}"
      accidentals[current_position].push accidental
      tab_specs[current_position].push {fret: note.fret, str: note.string}
      articulations[current_position].push note.articulation if note.articulation?
      durations[current_position] = current_duration
      decorators[current_position] = note.decorator if note.decorator?

      current_position++

    for spec, i in specs
      saved_duration = @current_duration
      @setDuration(durations[i].time, durations[i].dot) if durations[i]?
      @addTabNote tab_specs[i], play_notes[i]
      @addStaveNote {spec: spec, accidentals: accidentals[i], play_note: play_notes[i]} if stave.note?
      @addArticulations articulations[i]
      @addDecorator decorators[i] if decorators[i]?

    if chord_articulation?
      art = []
      art.push chord_articulation for num in [1..num_notes]
      @addArticulations art

    @addDecorator chord_decorator if chord_decorator?

  addNote: (note) ->
    @addChord([note])

  addTextVoice: ->
    _.last(@staves).text_voices.push []

  setTextFont: (font) ->
    if font?
      parts = font.match(/([^-]*)-([^-]*)-([^.]*)/)
      if parts?
        @customizations["font-face"] = parts[1]
        @customizations["font-size"] = parseInt(parts[2], 10)
        @customizations["font-style"] = parts[3]

  addTextNote: (text, position=0, justification="center", smooth=true, ignore_ticks=false) ->
    voices = _.last(@staves).text_voices
    throw new Vex.RERR("ArtistError", "Can't add text note without text voice") if _.isEmpty(voices)

    font_face = @customizations["font-face"]
    font_size = @customizations["font-size"]
    font_style = @customizations["font-style"]

    just = switch justification
      when "center"
        Vex.Flow.TextNote.Justification.CENTER
      when "left"
        Vex.Flow.TextNote.Justification.LEFT
      when "right"
        Vex.Flow.TextNote.Justification.RIGHT
      else
        Vex.Flow.TextNote.Justification.CENTER

    duration = if ignore_ticks then "b" else @current_duration

    struct =
      text: text
      duration: duration
      smooth: smooth
      ignore_ticks: ignore_ticks
      font:
        family: font_face
        size: font_size
        weight: font_style

    if text[0] == "#"
      struct.glyph = text[1..]

    note = new Vex.Flow.TextNote(struct).
      setLine(position).setJustification(just)

    _.last(voices).push(note)

  addVoice: (options) ->
    @closeBends()
    stave = _.last(@staves)
    return @addStave(options) unless stave?

    unless _.isEmpty(stave.tab_notes)
      stave.tab_voices.push(stave.tab_notes)
      stave.tab_notes = []

    unless _.isEmpty(stave.note_notes)
      stave.note_voices.push(stave.note_notes)
      stave.note_notes = []

  addStave: (element, options) ->
    opts =
      tuning: "standard"
      clef: "treble"
      key: "C"
      notation: if element == "tabstave" then "false" else "true"
      tablature: if element == "stave" then "false" else "true"
      strings: 6

    _.extend(opts, options)
    L "addStave: ", element, opts

    tab_stave = null
    note_stave = null

    # This is used to line up tablature and notation.
    start_x = @x + @customizations["connector-space"]
    tabstave_start_x = 40

    if opts.notation is "true"
      note_stave = new Vex.Flow.Stave(start_x, @last_y, @customizations.width - 20)
      note_stave.addClef(opts.clef) if opts.clef isnt "none"
      note_stave.addKeySignature(opts.key)
      note_stave.addTimeSignature(opts.time) if opts.time?

      @last_y += note_stave.getHeight() +
                 @options.note_stave_lower_spacing +
                 parseInt(@customizations["stave-distance"], 10)
      tabstave_start_x = note_stave.getNoteStartX()
      @current_clef = if opts.clef is "none" then "treble" else opts.clef

    if opts.tablature is "true"
      tab_stave = new Vex.Flow.TabStave(start_x, @last_y, @customizations.width - 20)
        .setNumLines(opts.strings)
      tab_stave.addTabGlyph() if opts.clef isnt "none"
      tab_stave.setNoteStartX(tabstave_start_x)
      @last_y += tab_stave.getHeight() + @options.tab_stave_lower_spacing

    @closeBends()
    beam_groups = Vex.Flow.Beam.getDefaultBeamGroups(opts.time)
    @staves.push {
      tab: tab_stave,
      note: note_stave,
      tab_voices: [],
      note_voices: [],
      tab_notes: [],
      note_notes: [],
      text_voices: [],
      beam_groups: beam_groups
    }

    @tuning.setTuning(opts.tuning)
    @key_manager.setKey(opts.key)

    return

  runCommand: (line, _l=0, _c=0) ->
    L "runCommand: ", line
    words = line.split(/\s+/)
    switch words[0]
      when "octave-shift"
        @current_octave_shift = parseInt(words[1], 10)
        L "Octave shift: ", @current_octave_shift
      else
        throw new Vex.RERR("ArtistError", "Invalid command '#{words[0]}' at line #{_l} column #{_c}")

module.exports = Artist
