// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// ## Description
//
// This file implements the formatting and layout algorithms that are used
// to position notes in a voice. The algorithm can align multiple voices both
// within a stave, and across multiple staves.
//
// To do this, the formatter breaks up voices into a grid of rational-valued
// `ticks`, to which each note is assigned. Then, minimum widths are assigned
// to each tick based on the widths of the notes and modifiers in that tick. This
// establishes the smallest amount of space required for each tick.
//
// Finally, the formatter distributes the left over space proportionally to
// all the ticks, setting the `x` values of the notes in each tick.
//
// See `tests/formatter_tests.js` for usage examples. The helper functions included
// here (`FormatAndDraw`, `FormatAndDrawTab`) also serve as useful usage examples.

import { Vex } from './vex';
import { Beam } from './beam';
import { Flow } from './tables';
import { Fraction } from './fraction';
import { Voice } from './voice';
import { StaveConnector } from './staveconnector';
import { StaveNote } from './stavenote';
import { Note } from './note';
import { ModifierContext } from './modifiercontext';
import { TickContext } from './tickcontext';

// To enable logging for this class. Set `Vex.Flow.Formatter.DEBUG` to `true`.
function L(...args) { if (Formatter.DEBUG) Vex.L('Vex.Flow.Formatter', args); }

function getNext(notes, i){
  const nextNote = notes[i + 1]
  if (nextNote && nextNote?.keys?.includes('b/4') || nextNote?.keys?.includes('r/4')) {
    return getNext(notes, i + 1)
  }
  return nextNote
}

// Helper function to locate the next non-rest note(s).
function lookAhead(notes, restLine, i, compare, base_height = 0) {
  // console.log(notes, restLine, i, compare)
  // If no valid next note group, nextRestLine is same as current.
  let nextRestLine = restLine;

  // Get the rest line for next valid non-rest note group.
  for (i += 1; i < notes.length; i += 1) {
    const note = notes[i];
    if (!note.isRest() && !note.shouldIgnoreTicks()) {
      nextRestLine = note.getLineForRest();
      break;
    }
  }

  // Locate the mid point between two lines.
  if (compare && restLine !== nextRestLine) {
    const top = Math.max(restLine, nextRestLine);
    const bot = Math.min(restLine, nextRestLine);
    nextRestLine = Vex.MidLine(top, bot);
  }
  for (i = 0; i < notes.length; i += 1) {
    const note = notes[i];
    if (note.beam && (nextRestLine > 4.5 || nextRestLine < 1.5)) {
      const stem = notes[i].stem
      if (stem) {
        if (stem.stem_direction > 0) {
          const height = nextRestLine < 1.5 ? Math.abs(nextRestLine - 1.5) * base_height : 0
          stem.setResetHeight(height)
        } else {
          const height = nextRestLine > 4.5 ? Math.abs(nextRestLine - 4.5) * base_height : 0
          stem.setResetHeight(height)
        }
      }
    }
  }
  return nextRestLine;
}

// Take an array of `voices` and place aligned tickables in the same context. Returns
// a mapping from `tick` to `ContextType`, a list of `tick`s, and the resolution
// multiplier.
//
// Params:
// * `voices`: Array of `Voice` instances.
// * `ContextType`: A context class (e.g., `ModifierContext`, `TickContext`)
// * `addToContext`: Function to add tickable to context.
function createContexts(voices, ContextType, addToContext) {
  if (!voices || !voices.length) {
    throw new Vex.RERR('BadArgument', 'No voices to format');
  }

  // Find out highest common multiple of resolution multipliers.
  // The purpose of this is to find out a common denominator
  // for all fractional tick values in all tickables of all voices,
  // so that the values can be expanded and the numerator used
  // as an integer tick value.
  const totalTicks = voices[0].getTotalTicks();
  const resolutionMultiplier = voices.reduce((resolutionMultiplier, voice) => {
    if (!voice.getTotalTicks().equals(totalTicks)) {
      throw new Vex.RERR(
        'TickMismatch', 'Voices should have same total note duration in ticks.'
      );
    }

    if (voice.getMode() === Voice.Mode.STRICT && !voice.isComplete()) {
      throw new Vex.RERR(
        'IncompleteVoice', 'Voice does not have enough notes.'
      );
    }

    return Math.max(
      resolutionMultiplier,
      Fraction.LCM(resolutionMultiplier, voice.getResolutionMultiplier())
    );
  }, 1);

  // Initialize tick maps.
  const tickToContextMap = {};
  const tickList = [];
  const contexts = [];

  // For each voice, extract notes and create a context for every
  // new tick that hasn't been seen before.
  voices.forEach(voice => {
    // Use resolution multiplier as denominator to expand ticks
    // to suitable integer values, so that no additional expansion
    // of fractional tick values is needed.
    const ticksUsed = new Fraction(0, resolutionMultiplier);

    voice.getTickables().forEach(tickable => {
      const integerTicks = ticksUsed.numerator;

      // If we have no tick context for this tick, create one.
      if (!tickToContextMap[integerTicks]) {
        const newContext = new ContextType();
        contexts.push(newContext);
        tickToContextMap[integerTicks] = newContext;
      }

      // Add this tickable to the TickContext.
      addToContext(tickable, tickToContextMap[integerTicks]);

      // Maintain a sorted list of tick contexts.
      tickList.push(integerTicks);
      ticksUsed.add(tickable.getTicks());
    });
  });

  return {
    map: tickToContextMap,
    array: contexts,
    list: Vex.SortAndUnique(tickList, (a, b) => a - b, (a, b) => a === b),
    resolutionMultiplier,
  };
}

export class Formatter {
  // Helper function to layout "notes" one after the other without
  // regard for proportions. Useful for tests and debugging.
  static SimpleFormat(notes, x = 0, { paddingBetween = 10 } = {}) {
    notes.reduce((x, note) => {
      note.addToModifierContext(new ModifierContext());
      const tick = new TickContext().addTickable(note).preFormat();
      const extra = tick.getExtraPx();
      tick.setX(x + extra.left);

      return x + tick.getWidth() + extra.right + paddingBetween;
    }, x);
  }

  // Helper function to plot formatter debug info.
  static plotDebugging(ctx, formatter, xPos, y1, y2) {
    const x = xPos + Note.STAVEPADDING;
    const contextGaps = formatter.contextGaps;
    function stroke(x1, x2, color) {
      ctx.beginPath();
      ctx.setStrokeStyle(color);
      ctx.setFillStyle(color);
      ctx.setLineWidth(1);
      ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
    }

    ctx.save();
    ctx.setFont('Arial', 8, '');

    contextGaps.gaps.forEach(gap => {
      stroke(x + gap.x1, x + gap.x2, '#aaa');
      // Vex.drawDot(ctx, xPos + gap.x1, yPos, 'blue');
      ctx.fillText(Math.round(gap.x2 - gap.x1), x + gap.x1, y2 + 12);
    });

    ctx.fillText(Math.round(contextGaps.total) + 'px', x - 20, y2 + 12);
    ctx.setFillStyle('red');

    ctx.fillText('Loss: ' +
      formatter.lossHistory.map(loss => Math.round(loss)), x - 20, y2 + 22);
    ctx.restore();
  }

  // Helper function to format and draw a single voice. Returns a bounding
  // box for the notation.
  //
  // Parameters:
  // * `ctx` - The rendering context
  // * `stave` - The stave to which to draw (`Stave` or `TabStave`)
  // * `notes` - Array of `Note` instances (`StaveNote`, `TextNote`, `TabNote`, etc.)
  // * `params` - One of below:
  //    * Setting `autobeam` only `(context, stave, notes, true)` or
  //      `(ctx, stave, notes, {autobeam: true})`
  //    * Setting `align_rests` a struct is needed `(context, stave, notes, {align_rests: true})`
  //    * Setting both a struct is needed `(context, stave, notes, {
  //      autobeam: true, align_rests: true})`
  //
  // `autobeam` automatically generates beams for the notes.
  // `align_rests` aligns rests with nearby notes.
  static FormatAndDraw(ctx, stave, notes, params) {
    const options = {
      auto_beam: false,
      align_rests: false,
    };

    if (typeof params === 'object') {
      Vex.Merge(options, params);
    } else if (typeof params === 'boolean') {
      options.auto_beam = params;
    }

    // Start by creating a voice and adding all the notes to it.
    const voice = new Voice(Flow.TIME4_4)
      .setMode(Voice.Mode.SOFT)
      .addTickables(notes);

    // Then create beams, if requested.
    const beams = options.auto_beam ? Beam.applyAndGetBeams(voice) : [];

    // Instantiate a `Formatter` and format the notes.
    new Formatter()
      .joinVoices([voice], { align_rests: options.align_rests })
      .formatToStave([voice], stave, { align_rests: options.align_rests, stave });

    // Render the voice and beams to the stave.
    voice.setStave(stave).draw(ctx, stave);
    beams.forEach(beam => beam.setContext(ctx).draw());

    // Return the bounding box of the voice.
    return voice.getBoundingBox();
  }

  // Helper function to format and draw aligned tab and stave notes in two
  // separate staves.
  //
  // Parameters:
  // * `ctx` - The rendering context
  // * `tabstave` - A `TabStave` instance on which to render `TabNote`s.
  // * `stave` - A `Stave` instance on which to render `Note`s.
  // * `notes` - Array of `Note` instances for the stave (`StaveNote`, `BarNote`, etc.)
  // * `tabnotes` - Array of `Note` instances for the tab stave (`TabNote`, `BarNote`, etc.)
  // * `autobeam` - Automatically generate beams.
  // * `params` - A configuration object:
  //    * `autobeam` automatically generates beams for the notes.
  //    * `align_rests` aligns rests with nearby notes.
  static FormatAndDrawTab(ctx, tabstave, stave, tabnotes, notes, autobeam, params) {
    const opts = {
      auto_beam: autobeam,
      align_rests: false,
    };

    if (typeof params === 'object') {
      Vex.Merge(opts, params);
    } else if (typeof params === 'boolean') {
      opts.auto_beam = params;
    }

    // Create a `4/4` voice for `notes`.
    const notevoice = new Voice(Flow.TIME4_4)
      .setMode(Voice.Mode.SOFT)
      .addTickables(notes);

    // Create a `4/4` voice for `tabnotes`.
    const tabvoice = new Voice(Flow.TIME4_4)
      .setMode(Voice.Mode.SOFT)
      .addTickables(tabnotes);

    // Then create beams, if requested.
    const beams = opts.auto_beam ? Beam.applyAndGetBeams(notevoice) : [];

    // Instantiate a `Formatter` and align tab and stave notes.
    new Formatter()
      .joinVoices([notevoice], { align_rests: opts.align_rests })
      .joinVoices([tabvoice])
      .formatToStave([notevoice, tabvoice], stave, { align_rests: opts.align_rests });

    // Render voices and beams to staves.
    notevoice.draw(ctx, stave);
    tabvoice.draw(ctx, tabstave);
    beams.forEach(beam => beam.setContext(ctx).draw());

    // Draw a connector between tab and note staves.
    new StaveConnector(stave, tabstave).setContext(ctx).draw();
  }

  // Auto position rests based on previous/next note positions.
  //
  // Params:
  // * `notes`: An array of notes.
  // * `alignAllNotes`: If set to false, only aligns non-beamed notes.
  // * `alignTuplets`: If set to false, ignores tuplets.
  static AlignRestsToNotes(notes, alignAllNotes, alignTuplets) {
    notes.forEach((note, index) => {
      if (note instanceof StaveNote && note.isRest()) {
        if (note.tuplet && !alignTuplets) return;

        //修改 8分休止符是否在横梁内
        if (["va5", "v3c"].includes(note.glyph.code_head)) {
          const base_height = note.glyph.code_head === "va5" ? 10 : 14
          const preNote = notes[index - 1];
          const nextNote = getNext(notes, index);
          let isThrough = false
          if (preNote && nextNote && preNote.beam && nextNote.beam) {
            isThrough = preNote.beam.notes.includes(nextNote)
          }
          if (isThrough) {
            note.setKeyLine(0, 3);
            let _notes = [...preNote.beam.notes, ...nextNote.beam.notes]
            if (note.beam && note.beam.notes){
              _notes.push(...note.beam.notes)
            }
            _notes = Array.from(new Set(_notes))
            for (let i = 0; i < _notes.length; i += 1) {
              const nextRestLine = _notes[i].getKeyProps()[0].line
              const stem = _notes[i].stem
                if (stem) {
                  if (stem.stem_direction > 0) {
                    const height = nextRestLine < 1.5 ? Math.abs(nextRestLine - 1.5) * base_height : 0
                    stem.setResetHeight(height)
                  } else {
                    const height = nextRestLine > 4.5 ? Math.abs(nextRestLine - 4.5) * base_height : 0
                    stem.setResetHeight(height)
                  }
                }
            }
            // Align rests with previous/next notes.
            // const props = note.getKeyProps()[0];
            // if (index === 0) {
            //   props.line = lookAhead(notes, props.line, index, false, base_height);
              
            // } else if (index > 0 && index < notes.length) {
            //   // If previous note is a rest, use its line number.
            //   let restLine;
            //   if (notes[index - 1].isRest()) {
            //     restLine = notes[index - 1].getKeyProps()[0].line;
            //     props.line = restLine;
            //   } else {
            //     restLine = notes[index - 1].getLineForRest();
            //     // Get the rest line for next valid non-rest note group.
            //     props.line = lookAhead(notes, restLine, index, true, base_height);
            //   }
              
            //   note.setKeyLine(0, props.line);
            // }
          }
        };
      }
    });

    return this;
  }

  constructor() {
    // Minimum width required to render all the notes in the voices.
    this.minTotalWidth = 0;

    // This is set to `true` after `minTotalWidth` is calculated.
    this.hasMinTotalWidth = false;

    // Total number of ticks in the voice.
    this.totalTicks = new Fraction(0, 1);

    // Arrays of tick and modifier contexts.
    this.tickContexts = null;
    this.modiferContexts = null;

    // Gaps between contexts, for free movement of notes post
    // formatting.
    this.contextGaps = {
      total: 0,
      gaps: [],
    };

    this.voices = [];
  }

  // Find all the rests in each of the `voices` and align them
  // to neighboring notes. If `alignAllNotes` is `false`, then only
  // align non-beamed notes.
  alignRests(voices, alignAllNotes) {
    if (!voices || !voices.length) {
      throw new Vex.RERR('BadArgument', 'No voices to format rests');
    }

    voices.forEach(voice =>
      Formatter.AlignRestsToNotes(voice.getTickables(), alignAllNotes));
  }

  // Calculate the minimum width required to align and format `voices`.
  preCalculateMinTotalWidth(voices) {
    // Cache results.
    if (this.hasMinTotalWidth) return this.minTotalWidth;

    // Create tick contexts if not already created.
    if (!this.tickContexts) {
      if (!voices) {
        throw new Vex.RERR(
          'BadArgument', "'voices' required to run preCalculateMinTotalWidth"
        );
      }

      this.createTickContexts(voices);
    }

    const { list: contextList, map: contextMap } = this.tickContexts;

    // Go through each tick context and calculate total width.
    this.minTotalWidth = contextList
      .map(tick => {
        const context = contextMap[tick];
        context.preFormat();
        return context.getWidth();
      })
      .reduce((a, b) => a + b, 0);

    this.hasMinTotalWidth = true;

    return this.minTotalWidth;
  }

  // Get minimum width required to render all voices. Either `format` or
  // `preCalculateMinTotalWidth` must be called before this method.
  getMinTotalWidth() {
    if (!this.hasMinTotalWidth) {
      throw new Vex.RERR(
        'NoMinTotalWidth',
        "Call 'preCalculateMinTotalWidth' or 'preFormat' before calling 'getMinTotalWidth'"
      );
    }

    return this.minTotalWidth;
  }

  // Create `ModifierContext`s for each tick in `voices`.
  createModifierContexts(voices) {
    const contexts = createContexts(
      voices,
      ModifierContext,
      (tickable, context) => tickable.addToModifierContext(context)
    );

    this.modiferContexts = contexts;
    return contexts;
  }

  // Create `TickContext`s for each tick in `voices`. Also calculate the
  // total number of ticks in voices.
  createTickContexts(voices) {
    const contexts = createContexts(
      voices,
      TickContext,
      (tickable, context) => context.addTickable(tickable)
    );

    contexts.array.forEach(context => {
      context.tContexts = contexts.array;
    });

    this.totalTicks = voices[0].getTicksUsed().clone();
    this.tickContexts = contexts;
    return contexts;
  }

  // This is the core formatter logic. Format voices and justify them
  // to `justifyWidth` pixels. `renderingContext` is required to justify elements
  // that can't retreive widths without a canvas. This method sets the `x` positions
  // of all the tickables/notes in the formatter.
  preFormat(justifyWidth = 0, renderingContext, voices, stave) {
    // Initialize context maps.
    const contexts = this.tickContexts;
    const { list: contextList, map: contextMap, resolutionMultiplier } = contexts;

    // If voices and a stave were provided, set the Stave for each voice
    // and preFormat to apply Y values to the notes;
    if (voices && stave) {
      voices.forEach(voice => voice.setStave(stave).preFormat());
    }

    // Now distribute the ticks to each tick context, and assign them their
    // own X positions.
    let x = 0;
    let shift = 0;
    const centerX = justifyWidth / 2;
    this.minTotalWidth = 0;

    // Pass 1: Give each note maximum width requested by context.
    contextList.forEach((tick) => {
      const context = contextMap[tick];
      if (renderingContext) context.setContext(renderingContext);

      // Make sure that all tickables in this context have calculated their
      // space requirements.
      context.preFormat();

      const width = context.getWidth();
      this.minTotalWidth += width;

      const metrics = context.getMetrics();
      x = x + shift + metrics.extraLeftPx;
      context.setX(x);

      // Calculate shift for the next tick.
      shift = width - metrics.extraLeftPx;
    });

    this.minTotalWidth = x + shift;
    this.hasMinTotalWidth = true;

    // No justification needed. End formatting.
    if (justifyWidth <= 0) return;

    // Pass 2: Take leftover width, and distribute it to proportionately to
    // all notes.
    const remainingX = justifyWidth - this.minTotalWidth;
    const leftoverPxPerTick = remainingX / (this.totalTicks.value() * resolutionMultiplier);
    let spaceAccum = 0;

    contextList.forEach((tick, index) => {
      const prevTick = contextList[index - 1] || 0;
      const context = contextMap[tick];
      const tickSpace = (tick - prevTick) * leftoverPxPerTick;

      spaceAccum += tickSpace;
      context.setX(context.getX() + spaceAccum);

      // Move center aligned tickables to middle
      context
        .getCenterAlignedTickables()
        .forEach(tickable => { // eslint-disable-line
          tickable.center_x_shift = centerX - context.getX();
        });
    });

    // Just one context. Done formatting.
    if (contextList.length === 1) return;

    this.justifyWidth = justifyWidth;
    this.lossHistory = [];
    this.evaluate();
  }

  // Calculate the total cost of this formatting decision.
  evaluate() {
    const justifyWidth = this.justifyWidth;
    // Calculate available slack per tick context. This works out how much freedom
    // to move a context has in either direction, without affecting other notes.
    this.contextGaps = { total: 0, gaps: [] };
    this.tickContexts.list.forEach((tick, index) => {
      if (index === 0) return;
      const prevTick = this.tickContexts.list[index - 1];
      const prevContext = this.tickContexts.map[prevTick];
      const context = this.tickContexts.map[tick];
      const prevMetrics = prevContext.getMetrics();

      const insideRightEdge = prevContext.getX() + prevMetrics.width;
      const insideLeftEdge = context.getX();
      const gap = insideLeftEdge - insideRightEdge;
      this.contextGaps.total += gap;
      this.contextGaps.gaps.push({ x1: insideRightEdge, x2: insideLeftEdge });

      // Tell the tick contexts how much they can reposition themselves.
      context.getFormatterMetrics().freedom.left = gap;
      prevContext.getFormatterMetrics().freedom.right = gap;
    });

    // Calculate mean distance in each voice for each duration type, then calculate
    // how far each note is from the mean.
    const durationStats = this.durationStats = {};

    function updateStats(duration, space) {
      const stats = durationStats[duration];
      if (stats === undefined) {
        durationStats[duration] = { mean: space, count: 1 };
      } else {
        stats.count += 1;
        stats.mean = (stats.mean + space) / 2;
      }
    }

    this.voices.forEach(voice => {
      voice.getTickables().forEach((note, i, notes) => {
        const duration = note.getTicks().clone().simplify().toString();
        const metrics = note.getMetrics();
        const formatterMetrics = note.getFormatterMetrics();
        const leftNoteEdge = note.getX() + metrics.noteWidth +
          metrics.modRightPx + metrics.extraRightPx;
        let space = 0;

        if (i < (notes.length - 1)) {
          const rightNote = notes[i + 1];
          const rightMetrics = rightNote.getMetrics();
          const rightNoteEdge = rightNote.getX() -
            rightMetrics.modLeftPx - rightMetrics.extraLeftPx;

          space = rightNoteEdge - leftNoteEdge;
          formatterMetrics.space.used = rightNote.getX() - note.getX();
          rightNote.getFormatterMetrics().freedom.left = space;
        } else {
          space = justifyWidth - leftNoteEdge;
          formatterMetrics.space.used = justifyWidth - note.getX();
        }

        formatterMetrics.freedom.right = space;
        updateStats(duration, formatterMetrics.space.used);
      });
    });

    // Calculate how much each note deviates from the mean. Loss function is square
    // root of the sum of squared deviations.
    let totalDeviation = 0;
    this.voices.forEach(voice => {
      voice.getTickables().forEach((note) => {
        const duration = note.getTicks().clone().simplify().toString();
        const metrics = note.getFormatterMetrics();
        metrics.iterations += 1;
        metrics.space.deviation = metrics.space.used - durationStats[duration].mean;
        metrics.duration = duration;
        metrics.space.mean = durationStats[duration].mean;

        totalDeviation += Math.pow(durationStats[duration].mean, 2);
      });
    });

    this.totalCost = Math.sqrt(totalDeviation);
    this.lossHistory.push(this.totalCost);
    return this;
  }

  // Run a single iteration of rejustification. At a high level, this method calculates
  // the overall "loss" (or cost) of this layout, and repositions tickcontexts in an
  // attempt to reduce the cost. You can call this method multiple times until it finds
  // and oscillates around a global minimum.
  tune() {
    const sum = (means) => means.reduce((a, b) => a + b);

    // Move `current` tickcontext by `shift` pixels, and adjust the freedom
    // on adjacent tickcontexts.
    function move(current, prev, next, shift) {
      current.setX(current.getX() + shift);
      current.getFormatterMetrics().freedom.left += shift;
      current.getFormatterMetrics().freedom.right -= shift;

      if (prev) prev.getFormatterMetrics().freedom.right += shift;
      if (next) next.getFormatterMetrics().freedom.left -= shift;
    }

    let shift = 0;
    this.tickContexts.list.forEach((tick, index, list) => {
      const context = this.tickContexts.map[tick];
      const prevContext = (index > 0) ? this.tickContexts.map[list[index - 1]] : null;
      const nextContext = (index < list.length - 1) ? this.tickContexts.map[list[index + 1]] : null;

      move(context, prevContext, nextContext, shift);

      const cost = -sum(
        context.getTickables().map(t => t.getFormatterMetrics().space.deviation));

      if (cost > 0) {
        shift = -Math.min(context.getFormatterMetrics().freedom.right, Math.abs(cost));
      } else if (cost < 0) {
        if (nextContext) {
          shift = Math.min(nextContext.getFormatterMetrics().freedom.right, Math.abs(cost));
        } else {
          shift = 0;
        }
      }

      const minShift = Math.min(5, Math.abs(shift));
      shift = shift > 0 ? minShift : -minShift;
    });

    return this.evaluate();
  }

  // This is the top-level call for all formatting logic completed
  // after `x` *and* `y` values have been computed for the notes
  // in the voices.
  postFormat() {
    const postFormatContexts = (contexts) =>
      contexts.list.forEach(tick => contexts.map[tick].postFormat());

    postFormatContexts(this.modiferContexts);
    postFormatContexts(this.tickContexts);

    return this;
  }

  // Take all `voices` and create `ModifierContext`s out of them. This tells
  // the formatters that the voices belong on a single stave.
  joinVoices(voices) {
    this.createModifierContexts(voices);
    this.hasMinTotalWidth = false;
    return this;
  }

  // Align rests in voices, justify the contexts, and position the notes
  // so voices are aligned and ready to render onto the stave. This method
  // mutates the `x` positions of all tickables in `voices`.
  //
  // Voices are full justified to fit in `justifyWidth` pixels.
  //
  // Set `options.context` to the rendering context. Set `options.align_rests`
  // to true to enable rest alignment.
  format(voices, justifyWidth, options) {
    const opts = {
      align_rests: false,
      context: null,
      stave: null,
    };

    Vex.Merge(opts, options);
    this.voices = voices;
    this.alignRests(voices, opts.align_rests);
    this.createTickContexts(voices);
    this.preFormat(justifyWidth, opts.context, voices, opts.stave);

    // Only postFormat if a stave was supplied for y value formatting
    if (opts.stave) this.postFormat();

    return this;
  }

  // This method is just like `format` except that the `justifyWidth` is inferred
  // from the `stave`.
  formatToStave(voices, stave, options) {
    const justifyWidth = stave.getNoteEndX() - stave.getNoteStartX() - 10;
    L('Formatting voices to width: ', justifyWidth);
    const opts = { context: stave.getContext() };
    Vex.Merge(opts, options);
    return this.format(voices, justifyWidth, opts);
  }
}
