text: // [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
  // Author: Larry Kuhns
  //
  // ## Description
  // This file implements the `Stroke` class which renders chord strokes
  // that can be arpeggiated, brushed, rasquedo, etc.
  
  import { Vex } from './vex';
  import { Modifier } from './modifier';
  import { StaveNote } from './stavenote';
  import { Glyph } from './glyph';
  
  export class Stroke extends Modifier {
    static get CATEGORY() { return 'strokes'; }
    static get Type() {
      return {
        BRUSH_DOWN: 1,
        BRUSH_UP: 2,
        ROLL_DOWN: 3,        // Arpegiated chord
        ROLL_UP: 4,          // Arpegiated chord
        RASQUEDO_DOWN: 5,
        RASQUEDO_UP: 6,
      };
    }
  
    // Arrange strokes inside `ModifierContext`
    static format(strokes, state) {
      const left_shift = state.left_shift;
      const stroke_spacing = 0;
  
      if (!strokes || strokes.length === 0) return this;
  
      const strokeList = strokes.map((stroke) => {
        const note = stroke.getNote();
        if (note instanceof StaveNote) {
          const { line, displaced } = note.getKeyProps()[stroke.getIndex()];
          const shift = displaced ? note.getExtraLeftPx() : 0;
          return { line, shift, stroke };
        } else {
          const { str: string } = note.getPositions()[stroke.getIndex()];
          return { line: string, shift: 0, stroke };
        }
      });
  
      const strokeShift = left_shift;
  
      // There can only be one stroke .. if more than one, they overlay each other
      const xShift = strokeList.reduce((xShift, { stroke, shift }) => {
        stroke.setXShift(strokeShift + shift);
        return Math.max(stroke.getWidth() + stroke_spacing, xShift);
      }, 0);
  
      state.left_shift += xShift;
      return true;
    }
  
    constructor(type, options) {
      super();
      this.setAttribute('type', 'Stroke');
  
      this.note = null;
      this.options = Vex.Merge({}, options);
  
      // multi voice - span stroke across all voices if true
      this.all_voices = 'all_voices' in this.options ? this.options.all_voices : true;
  
      // multi voice - end note of stroke, set in draw()
      this.note_end = null;
      this.index = null;
      this.type = type;
      this.position = Modifier.Position.LEFT;
  
      this.render_options = {
        font_scale: 38,
        stroke_px: 3,
        stroke_spacing: 10,
      };
  
      this.font = {
        family: 'serif',
        size: 10,
        weight: 'bold italic',
      };
  
      this.setXShift(0);
      this.setWidth(10);
    }
  
    getCategory() { return Stroke.CATEGORY; }
    getPosition() { return this.position; }
    addEndNote(note) { this.note_end = note; return this; }
  
    draw() {
      this.checkContext();
      this.setRendered();
  
      if (!(this.note && (this.index != null))) {
        throw new Vex.RERR('NoAttachedNote', "Can't draw stroke without a note and index.");
      }
  
      const start = this.note.getModifierStartXY(this.position, this.index);
      let ys = this.note.getYs();
      let topY = start.y;
      let botY = start.y;
      const x = start.x - 5;
      const line_space = this.note.stave.options.spacing_between_lines_px;
  
      const notes = this.getModifierContext().getModifiers(this.note.getCategory());
      for (let i = 0; i < notes.length; i++) {
        ys = notes[i].getYs();
        for (let n = 0; n < ys.length; n++) {
          if (this.note === notes[i] || this.all_voices) {
            topY = Vex.Min(topY, ys[n]);
            botY = Vex.Max(botY, ys[n]);
          }
        }
      }
  
      let arrow;
      let arrow_shift_x;
      let arrow_y;
      let text_shift_x;
      let text_y;
      switch (this.type) {
        case Stroke.Type.BRUSH_DOWN:
          arrow = 'vc3';
          arrow_shift_x = -3;
          arrow_y = topY - (line_space / 2) + 10;
          botY += (line_space / 2);
          break;
        case Stroke.Type.BRUSH_UP:
          arrow = 'v11';
          arrow_shift_x = 0.5;
          arrow_y = botY + (line_space / 2);
          topY -= (line_space / 2);
          break;
        case Stroke.Type.ROLL_DOWN:
        case Stroke.Type.RASQUEDO_DOWN:
          arrow = 'vc3';
          arrow_shift_x = -3;
          text_shift_x = this.x_shift + arrow_shift_x - 2;
          if (this.note instanceof StaveNote) {
            topY += 1.5 * line_space;
            if ((botY - topY) % 2 !== 0) {
              botY += 0.5 * line_space;
            } else {
              botY += line_space;
            }
            arrow_y = topY - line_space;
            text_y = botY + line_space + 2;
          } else {
            topY += 1.5 * line_space;
            botY += line_space;
            arrow_y = topY - 0.75 * line_space;
            text_y = botY + 0.25 * line_space;
          }
          break;
        case Stroke.Type.ROLL_UP:
        case Stroke.Type.RASQUEDO_UP:
          arrow = 'v52';
          arrow_shift_x = -4;
          text_shift_x = this.x_shift + arrow_shift_x - 1;
          if (this.note instanceof StaveNote) {
            arrow_y = line_space / 2;
            topY += 0.5 * line_space;
            if ((botY - topY) % 2 === 0) {
              botY += line_space / 2;
            }
            arrow_y = botY + 0.5 * line_space;
            text_y = topY - 1.25 * line_space;
          } else {
            topY += 0.25 * line_space;
            botY += 0.5 * line_space;
            arrow_y = botY + 0.25 * line_space;
            text_y = topY - line_space;
          }
          break;
        default:
          throw new Vex.RERR('InvalidType', `The stroke type ${this.type} does not exist`);
      }
  
      // Draw the stroke
      if (this.type === Stroke.Type.BRUSH_DOWN || this.type === Stroke.Type.BRUSH_UP) {
        this.context.fillRect(x + this.x_shift, topY, 1, botY - topY);
      } else {
        if (this.note instanceof StaveNote) {
          for (let i = topY; i <= botY; i += line_space) {
            Glyph.renderGlyph(
              this.context,
              x + this.x_shift - 4,
              i,
              this.render_options.font_scale,
              'va3'
            );
          }
        } else {
          let i;
          for (i = topY; i <= botY; i += 10) {
            Glyph.renderGlyph(
              this.context,
              x + this.x_shift - 4,
              i,
              this.render_options.font_scale,
              'va3'
            );
          }
          if (this.type === Stroke.Type.RASQUEDO_DOWN) {
            text_y = i + 0.25 * line_space;
          }
        }
      }
  
      // Draw the arrow head
      Glyph.renderGlyph(
        this.context,
        x + this.x_shift + arrow_shift_x,
        arrow_y,
        this.render_options.font_scale,
        arrow
      );
  
      // Draw the rasquedo "R"
      if (this.type === Stroke.Type.RASQUEDO_DOWN || this.type === Stroke.Type.RASQUEDO_UP) {
        this.context.save();
        this.context.setFont(this.font.family, this.font.size, this.font.weight);
        this.context.fillText('R', x + text_shift_x, text_y);
        this.context.restore();
      }
    }
  }
  