// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// ## Description
//
// This file implements an abstract interface for notes and chords that
// are rendered on a stave. Notes have some common properties: All of them
// have a value (e.g., pitch, fret, etc.) and a duration (quarter, half, etc.)
//
// Some notes have stems, heads, dots, etc. Most notational elements that
// surround a note are called *modifiers*, and every note has an associated
// array of them. All notes also have a rendering context and belong to a stave.

import { Vex } from './vex';
import { Flow } from './tables';
import { Tickable } from './tickable';

export class Note extends Tickable {
  static get CATEGORY() { return 'note'; }
  static get STAVEPADDING() { return 12; }

  // Debug helper. Displays various note metrics for the given
  // note.
  static plotMetrics(ctx, note, yPos) {
    const metrics = note.getMetrics();
    const xStart = note.getAbsoluteX() - metrics.modLeftPx - metrics.extraLeftPx;
    const xPre1 = note.getAbsoluteX() - metrics.extraLeftPx;
    const xAbs = note.getAbsoluteX();
    const xPost1 = note.getAbsoluteX() + metrics.noteWidth;
    const xPost2 = note.getAbsoluteX() + metrics.noteWidth + metrics.extraRightPx;
    const xEnd = note.getAbsoluteX()
      + metrics.noteWidth
      + metrics.extraRightPx
      + metrics.modRightPx;
    const xFreedomRight = xEnd + note.getFormatterMetrics().freedom.right;

    const xWidth = xEnd - xStart;
    ctx.save();
    ctx.setFont('Arial', 8, '');
    ctx.fillText(Math.round(xWidth) + 'px', xStart + note.getXShift(), yPos);

    const y = (yPos + 7);
    function stroke(x1, x2, color, yy = y) {
      ctx.beginPath();
      ctx.setStrokeStyle(color);
      ctx.setFillStyle(color);
      ctx.setLineWidth(3);
      ctx.moveTo(x1 + note.getXShift(), yy);
      ctx.lineTo(x2 + note.getXShift(), yy);
      ctx.stroke();
    }

    stroke(xStart, xPre1, 'red');
    stroke(xPre1, xAbs, '#999');
    stroke(xAbs, xPost1, 'green');
    stroke(xPost1, xPost2, '#999');
    stroke(xPost2, xEnd, 'red');
    stroke(xEnd, xFreedomRight, '#DD0');
    stroke(xStart - note.getXShift(), xStart, '#BBB'); // Shift
    Vex.drawDot(ctx, xAbs + note.getXShift(), y, 'blue');

    const formatterMetrics = note.getFormatterMetrics();
    if (formatterMetrics.iterations > 0) {
      const spaceDeviation = formatterMetrics.space.deviation;
      const prefix = spaceDeviation >= 0 ? '+' : '';
      ctx.setFillStyle('red');
      ctx.fillText(prefix + Math.round(spaceDeviation),
        xAbs + note.getXShift(), yPos - 10);
    }
    ctx.restore();
  }

  // Every note is a tickable, i.e., it can be mutated by the `Formatter` class for
  // positioning and layout.
  // To create a new note you need to provide a `noteStruct`, which consists
  // of the following fields:
  //
  // `type`: The note type (e.g., `r` for rest, `s` for slash notes, etc.)
  // `dots`: The number of dots, which affects the duration.
  // `duration`: The time length (e.g., `q` for quarter, `h` for half, `8` for eighth etc.)
  //
  // The range of values for these parameters are available in `src/tables.js`.
  constructor(noteStruct) {
    super();
    this.setAttribute('type', 'Note');

    if (!noteStruct) {
      throw new Vex.RuntimeError(
        'BadArguments', 'Note must have valid initialization data to identify duration and type.'
      );
    }

    // Parse `noteStruct` and get note properties.
    const initStruct = Flow.parseNoteStruct(noteStruct);
    if (!initStruct) {
      throw new Vex.RuntimeError(
        'BadArguments', `Invalid note initialization object: ${JSON.stringify(noteStruct)}`
      );
    }

    // Set note properties from parameters.
    this.duration = initStruct.duration;
    this.dots = initStruct.dots;
    this.noteType = initStruct.type;
    this.customTypes = initStruct.customTypes;

    //简谱
    this.DYMusicScoreType = noteStruct.DYMusicScoreType

    if (noteStruct.duration_override) {
      // Custom duration
      this.setDuration(noteStruct.duration_override);
    } else {
      // Default duration
      this.setIntrinsicTicks(initStruct.ticks);
    }

    this.modifiers = [];

    // Get the glyph code for this note from the font.
    this.glyph = Flow.getGlyphProps(this.duration, this.noteType);
    this.customGlyphs = this.customTypes.map(t => Flow.getGlyphProps(this.duration, t));

    if (this.positions && (typeof (this.positions) !== 'object' || !this.positions.length)) {
      throw new Vex.RuntimeError('BadArguments', 'Note keys must be array type.');
    }

    // Note to play for audio players.
    this.playNote = null;

    // Positioning contexts used by the Formatter.
    this.tickContext = null;    // The current tick context.
    this.modifierContext = null;
    this.ignore_ticks = false;

    // Positioning variables
    this.width = 0;             // Width in pixels calculated after preFormat
    this.extraLeftPx = 0;       // Extra room on left for offset note head
    this.extraRightPx = 0;      // Extra room on right for offset note head
    this.x_shift = 0;           // X shift from tick context X
    this.left_modPx = 0;        // Max width of left modifiers
    this.right_modPx = 0;       // Max width of right modifiers
    this.voice = null;          // The voice that this note is in
    this.preFormatted = false;  // Is this note preFormatted?
    this.ys = [];               // list of y coordinates for each note
    // we need to hold on to these for ties and beams.

    if (noteStruct.align_center) {
      this.setCenterAlignment(noteStruct.align_center);
    }

    // The render surface.
    this.stave = null;
    this.render_options = {
      annotation_spacing: 5,
      stave_padding: Note.STAVEPADDING,
    };
  }

  // Get and set the play note, which is arbitrary data that can be used by an
  // audio player.
  getPlayNote() { return this.playNote; }
  setPlayNote(note) { this.playNote = note; return this; }

  // Don't play notes by default, call them rests. This is also used by things like
  // beams and dots for positioning.
  isRest() { return false; }

  // TODO(0xfe): Why is this method here?
  addStroke(index, stroke) {
    stroke.setNote(this);
    stroke.setIndex(index);
    this.modifiers.push(stroke);
    this.setPreFormatted(false);
    return this;
  }

  // Get and set the target stave.
  getStave() { return this.stave; }
  setStave(stave) {
    this.stave = stave;
    this.setYs([stave.getYForLine(0)]); // Update Y values if the stave is changed.
    this.context = this.stave.context;
    return this;
  }

  // `Note` is not really a modifier, but is used in
  // a `ModifierContext`.
  getCategory() { return Note.CATEGORY; }

  // Set the rendering context for the note.
  setContext(context) { this.context = context; return this; }

  // Get and set spacing to the left and right of the notes.
  getExtraLeftPx() { return this.extraLeftPx; }
  getExtraRightPx() { return this.extraRightPx; }
  setExtraLeftPx(x) { this.extraLeftPx = x; return this; }
  setExtraRightPx(x) { this.extraRightPx = x; return this; }

  // Returns true if this note has no duration (e.g., bar notes, spacers, etc.)
  shouldIgnoreTicks() { return this.ignore_ticks; }

  // Get the stave line number for the note.
  getLineNumber() { return 0; }

  // Get the stave line number for rest.
  getLineForRest() { return 0; }

  // Get the glyph associated with this note.
  getGlyph() { return this.glyph; }

  getGlyphWidth() {
    return this.glyph.getWidth ? this.glyph.getWidth(this.render_options.glyph_font_scale) : 15;
  }

  // Set and get Y positions for this note. Each Y value is associated with
  // an individual pitch/key within the note/chord.
  setYs(ys) { this.ys = ys; return this; }
  getYs() {
    if (this.ys.length === 0) {
      throw new Vex.RERR('NoYValues', 'No Y-values calculated for this note.');
    }

    return this.ys;
  }

  // Get the Y position of the space above the stave onto which text can
  // be rendered.
  getYForTopText(text_line) {
    if (!this.stave) {
      throw new Vex.RERR('NoStave', 'No stave attached to this note.');
    }

    return this.stave.getYForTopText(text_line);
  }

  // Get a `BoundingBox` for this note.
  getBoundingBox() { return null; }

  // Returns the voice that this note belongs in.
  getVoice() {
    if (!this.voice) throw new Vex.RERR('NoVoice', 'Note has no voice.');
    return this.voice;
  }

  // Attach this note to `voice`.
  setVoice(voice) {
    this.voice = voice;
    this.preFormatted = false;
    return this;
  }

  // Get and set the `TickContext` for this note.
  getTickContext() { return this.tickContext; }
  setTickContext(tc) {
    this.tickContext = tc;
    this.preFormatted = false;
    return this;
  }

  // Accessors for the note type.
  getDuration() { return this.duration; }
  isDotted() { return (this.dots > 0); }
  hasStem() { return false; }
  getDots() { return this.dots; }
  getNoteType() { return this.noteType; }
  setBeam() { return this; } // ignore parameters

  // Attach this note to a modifier context.
  setModifierContext(mc) { this.modifierContext = mc; return this; }

  // Attach a modifier to this note.
  addModifier(modifier, index = 0) {
    modifier.setNote(this);
    modifier.setIndex(index);
    this.modifiers.push(modifier);
    this.setPreFormatted(false);
    return this;
  }

  // Get the coordinates for where modifiers begin.
  getModifierStartXY() {
    if (!this.preFormatted) {
      throw new Vex.RERR('UnformattedNote', "Can't call GetModifierStartXY on an unformatted note");
    }

    return {
      x: this.getAbsoluteX(),
      y: this.ys[0],
    };
  }

  // Get bounds and metrics for this note.
  //
  // Returns a struct with fields:
  // `width`: The total width of the note (including modifiers.)
  // `noteWidth`: The width of the note head only.
  // `left_shift`: The horizontal displacement of the note.
  // `modLeftPx`: Start `X` for left modifiers.
  // `modRightPx`: Start `X` for right modifiers.
  // `extraLeftPx`: Extra space on left of note.
  // `extraRightPx`: Extra space on right of note.
  getMetrics() {
    if (!this.preFormatted) {
      throw new Vex.RERR('UnformattedNote', "Can't call getMetrics on an unformatted note.");
    }

    let modLeftPx = 0;
    let modRightPx = 0;
    if (this.modifierContext != null) {
      modLeftPx = this.modifierContext.state.left_shift;
      modRightPx = this.modifierContext.state.right_shift;
    }

    const width = this.getWidth();
    return {
      width,
      noteWidth: width - modLeftPx - modRightPx - this.extraLeftPx - this.extraRightPx,
      left_shift: this.x_shift, // TODO(0xfe): Make style consistent

      // Modifiers, accidentals etc.
      modLeftPx,
      modRightPx,

      // Displaced note head on left or right.
      extraLeftPx: this.extraLeftPx,
      extraRightPx: this.extraRightPx,
    };
  }

  // Get and set width of note. Used by the formatter for positioning.
  setWidth(width) { this.width = width; }
  getWidth() {
    if (!this.preFormatted) {
      throw new Vex.RERR('UnformattedNote', "Can't call GetWidth on an unformatted note.");
    }

    return this.width + (this.modifierContext ? this.modifierContext.getWidth() : 0);
  }

  // Displace note by `x` pixels. Used by the formatter.
  setXShift(x) { this.x_shift = x; return this; }
  getXShift() { return this.x_shift; }

  // Get `X` position of this tick context.
  getX() {
    if (!this.tickContext) {
      throw new Vex.RERR('NoTickContext', 'Note needs a TickContext assigned for an X-Value');
    }

    return this.tickContext.getX() + this.x_shift;
  }

  // Get the absolute `X` position of this note's tick context. This
  // excludes x_shift, so you'll need to factor it in if you're
  // looking for the post-formatted x-position.
  getAbsoluteX() {
    if (!this.tickContext) {
      throw new Vex.RERR('NoTickContext', 'Note needs a TickContext assigned for an X-Value');
    }

    // Position note to left edge of tick context.
    let x = this.tickContext.getX();
    if (this.stave) {
      x += this.stave.getNoteStartX() + this.render_options.stave_padding;
    }

    if (this.isCenterAligned()) {
      // 简谱 全音符 不全部居中
      if (this.DYMusicScoreType === 'jianpu') {
        // x += this.getCenterXShift();
      } else {
        x += this.getCenterXShift();
      }
      
    }

    return x;
  }
  setPreFormatted(value) {
    this.preFormatted = value;

    // Maintain the width of left and right modifiers in pixels.
    if (this.preFormatted) {
      const extra = this.tickContext.getExtraPx();
      this.left_modPx = Math.max(this.left_modPx, extra.left);
      this.right_modPx = Math.max(this.right_modPx, extra.right);
    }
  }
}
