text: const ArgumentType = require('../extension-support/argument-type');
  const BlockType = require('../extension-support/block-type');
  const Clone = require('../util/clone');
  const Cast = require('../util/cast');
  const MathUtil = require('../util/math-util');
  const Timer = require('../util/timer');
  
  /**
   * An array of drum names, used in the play drum block.
   * @type {string[]}
   */
  const drumNames = [
      'Snare Drum',
      'Bass Drum',
      'Side Stick',
      'Crash Cymbal',
      'Open Hi-Hat',
      'Closed Hi-Hat',
      'Tambourine',
      'Hand Clap',
      'Claves',
      'Wood Block',
      'Cowbell',
      'Triangle',
      'Bongo',
      'Conga',
      'Cabasa',
      'Guiro',
      'Vibraslap',
      'Open Cuica'
  ];
  
  /**
   * An array of instrument names, used in the set instrument block.
   * @type {string[]}
   */
  const instrumentNames = [
      'Piano',
      'Electric Piano',
      'Organ',
      'Guitar',
      'Electric Guitar',
      'Bass',
      'Pizzicato',
      'Cello',
      'Trombone',
      'Clarinet',
      'Saxophone',
      'Flute',
      'Wooden Flute',
      'Bassoon',
      'Choir',
      'Vibraphone',
      'Music Box',
      'Steel Drum',
      'Marimba',
      'Synth Lead',
      'Synth Pad'
  ];
  
  /**
   * Class for the music-related blocks in Scratch 3.0
   * @param {Runtime} runtime - the runtime instantiating this block package.
   * @constructor
   */
  class Scratch3MusicBlocks {
      constructor (runtime) {
          /**
           * The runtime instantiating this block package.
           * @type {Runtime}
           */
          this.runtime = runtime;
  
          /**
           * The current tempo in beats per minute. The tempo is a global property of the project,
           * not a property of each sprite, so it is not stored in the MusicState object.
           * @type {number}
           */
          this.tempo = 60;
  
          this.drumMenu = this._buildMenu(drumNames);
          this.instrumentMenu = this._buildMenu(instrumentNames);
      }
  
      /**
       * Build a menu using an array of strings.
       * Used for creating the drum and instrument menus.
       * @param  {string[]} names - An array of names.
       * @return {array} - An array of objects with text and value properties, for constructing a block menu.
       * @private
       */
      _buildMenu (names) {
          const menu = [];
          for (let i = 0; i < names.length; i++) {
              const entry = {};
              const num = i + 1; // Menu numbers are one-indexed
              entry.text = `(${num}) ${names[i]}`;
              entry.value = String(num);
              menu.push(entry);
          }
          return menu;
      }
  
      /**
       * The key to load & store a target's music-related state.
       * @type {string}
       */
      static get STATE_KEY () {
          return 'Scratch.music';
      }
  
      /**
       * The default music-related state, to be used when a target has no existing music state.
       * @type {MusicState}
       */
      static get DEFAULT_MUSIC_STATE () {
          return {
              currentInstrument: 0
          };
      }
  
      /**
       * The minimum and maximum MIDI note numbers, for clamping the input to play note.
       * @type {{min: number, max: number}}
       */
      static get MIDI_NOTE_RANGE () {
          return {min: 36, max: 96}; // C2 to C7
      }
  
      /**
       * The minimum and maximum beat values, for clamping the duration of play note, play drum and rest.
       * 100 beats at the default tempo of 60bpm is 100 seconds.
       * @type {{min: number, max: number}}
       */
      static get BEAT_RANGE () {
          return {min: 0, max: 100};
      }
  
      /** The minimum and maximum tempo values, in bpm.
       * @type {{min: number, max: number}}
       */
      static get TEMPO_RANGE () {
          return {min: 20, max: 500};
      }
  
      /**
       * @param {Target} target - collect music state for this target.
       * @returns {MusicState} the mutable music state associated with that target. This will be created if necessary.
       * @private
       */
      _getMusicState (target) {
          let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY);
          if (!musicState) {
              musicState = Clone.simple(Scratch3MusicBlocks.DEFAULT_MUSIC_STATE);
              target.setCustomState(Scratch3MusicBlocks.STATE_KEY, musicState);
          }
          return musicState;
      }
  
      /**
       * @returns {object} metadata for this extension and its blocks.
       */
      getInfo () {
          return {
              id: 'music',
              name: 'Music',
              blocks: [
                  {
                      opcode: 'playDrumForBeats',
                      blockType: BlockType.COMMAND,
                      text: 'play drum [DRUM] for [BEATS] beats',
                      arguments: {
                          DRUM: {
                              type: ArgumentType.NUMBER,
                              menu: 'drums',
                              defaultValue: 1
                          },
                          BEATS: {
                              type: ArgumentType.NUMBER,
                              defaultValue: 0.25
                          }
                      }
                  },
                  {
                      opcode: 'restForBeats',
                      blockType: BlockType.COMMAND,
                      text: 'rest for [BEATS] beats',
                      arguments: {
                          BEATS: {
                              type: ArgumentType.NUMBER,
                              defaultValue: 0.25
                          }
                      }
                  },
                  {
                      opcode: 'playNoteForBeats',
                      blockType: BlockType.COMMAND,
                      text: 'play note [NOTE] for [BEATS] beats',
                      arguments: {
                          NOTE: {
                              type: ArgumentType.NUMBER,
                              defaultValue: 60
                          },
                          BEATS: {
                              type: ArgumentType.NUMBER,
                              defaultValue: 0.25
                          }
                      }
                  },
                  {
                      opcode: 'setInstrument',
                      blockType: BlockType.COMMAND,
                      text: 'set instrument to [INSTRUMENT]',
                      arguments: {
                          INSTRUMENT: {
                              type: ArgumentType.NUMBER,
                              menu: 'instruments',
                              defaultValue: 1
                          }
                      }
                  },
                  {
                      opcode: 'setTempo',
                      blockType: BlockType.COMMAND,
                      text: 'set tempo to [TEMPO]',
                      arguments: {
                          TEMPO: {
                              type: ArgumentType.NUMBER,
                              defaultValue: 60
                          }
                      }
                  },
                  {
                      opcode: 'changeTempo',
                      blockType: BlockType.COMMAND,
                      text: 'change tempo by [TEMPO]',
                      arguments: {
                          TEMPO: {
                              type: ArgumentType.NUMBER,
                              defaultValue: 20
                          }
                      }
                  },
                  {
                      opcode: 'getTempo',
                      text: 'tempo',
                      blockType: BlockType.REPORTER
                  }
              ],
              menus: {
                  drums: this.drumMenu,
                  instruments: this.instrumentMenu
              }
          };
      }
  
      /**
       * Play a drum sound for some number of beats.
       * @param {object} args - the block arguments.
       * @param {object} util - utility object provided by the runtime.
       * @property {int} DRUM - the number of the drum to play.
       * @property {number} BEATS - the duration in beats of the drum sound.
       */
      playDrumForBeats (args, util) {
          if (this._stackTimerNeedsInit(util)) {
              let drum = Cast.toNumber(args.DRUM);
              drum -= 1; // drums are one-indexed
              if (typeof this.runtime.audioEngine === 'undefined') return;
              drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
              let beats = Cast.toNumber(args.BEATS);
              beats = this._clampBeats(beats);
              if (util.target.audioPlayer !== null) {
                  util.target.audioPlayer.playDrumForBeats(drum, beats);
              }
              this._startStackTimer(util, this._beatsToSec(beats));
          } else {
              this._checkStackTimer(util);
          }
      }
  
      /**
       * Rest for some number of beats.
       * @param {object} args - the block arguments.
       * @param {object} util - utility object provided by the runtime.
       * @property {number} BEATS - the duration in beats of the rest.
       */
      restForBeats (args, util) {
          if (this._stackTimerNeedsInit(util)) {
              let beats = Cast.toNumber(args.BEATS);
              beats = this._clampBeats(beats);
              this._startStackTimer(util, this._beatsToSec(beats));
          } else {
              this._checkStackTimer(util);
          }
      }
  
      /**
       * Play a note using the current musical instrument for some number of beats.
       * @param {object} args - the block arguments.
       * @param {object} util - utility object provided by the runtime.
       * @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number.
       * @property {number} BEATS - the duration in beats of the note.
       */
      playNoteForBeats (args, util) {
          if (this._stackTimerNeedsInit(util)) {
              let note = Cast.toNumber(args.NOTE);
              note = MathUtil.clamp(note,
                  Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
              let beats = Cast.toNumber(args.BEATS);
              beats = this._clampBeats(beats);
              const musicState = this._getMusicState(util.target);
              const inst = musicState.currentInstrument;
              if (typeof this.runtime.audioEngine !== 'undefined') {
                  this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, 100);
              }
              this._startStackTimer(util, this._beatsToSec(beats));
          } else {
              this._checkStackTimer(util);
          }
      }
  
      /**
       * Clamp a duration in beats to the allowed min and max duration.
       * @param  {number} beats - a duration in beats.
       * @return {number} - the clamped duration.
       * @private
       */
      _clampBeats (beats) {
          return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max);
      }
  
      /**
       * Convert a number of beats to a number of seconds, using the current tempo.
       * @param  {number} beats - number of beats to convert to secs.
       * @return {number} seconds - number of seconds `beats` will last.
       * @private
       */
      _beatsToSec (beats) {
          return (60 / this.tempo) * beats;
      }
  
      /**
       * Check if the stack timer needs initialization.
       * @param {object} util - utility object provided by the runtime.
       * @return {boolean} - true if the stack timer needs to be initialized.
       * @private
       */
      _stackTimerNeedsInit (util) {
          return !util.stackFrame.timer;
      }
  
      /**
       * Start the stack timer and the yield the thread if necessary.
       * @param {object} util - utility object provided by the runtime.
       * @param {number} duration - a duration in seconds to set the timer for.
       * @private
       */
      _startStackTimer (util, duration) {
          util.stackFrame.timer = new Timer();
          util.stackFrame.timer.start();
          util.stackFrame.duration = duration;
          if (util.stackFrame.duration > 0) {
              util.yield();
          }
      }
  
      /**
       * Check the stack timer, and if its time is not up yet, yield the thread.
       * @param {object} util - utility object provided by the runtime.
       * @private
       */
      _checkStackTimer (util) {
          const timeElapsed = util.stackFrame.timer.timeElapsed();
          if (timeElapsed < util.stackFrame.duration * 1000) {
              util.yield();
          }
      }
  
      /**
       * Select an instrument for playing notes.
       * @param {object} args - the block arguments.
       * @param {object} util - utility object provided by the runtime.
       * @property {int} INSTRUMENT - the number of the instrument to select.
       * @return {Promise} - a promise which will resolve once the instrument has loaded.
       */
      setInstrument (args, util) {
          const musicState = this._getMusicState(util.target);
          let instNum = Cast.toNumber(args.INSTRUMENT);
          instNum -= 1; // instruments are one-indexed
          if (typeof this.runtime.audioEngine === 'undefined') return;
          instNum = MathUtil.wrapClamp(instNum, 0, this.runtime.audioEngine.numInstruments - 1);
          musicState.currentInstrument = instNum;
          return this.runtime.audioEngine.instrumentPlayer.loadInstrument(musicState.currentInstrument);
      }
  
      /**
       * Set the current tempo to a new value.
       * @param {object} args - the block arguments.
       * @property {number} TEMPO - the tempo, in beats per minute.
       */
      setTempo (args) {
          const tempo = Cast.toNumber(args.TEMPO);
          this._updateTempo(tempo);
      }
  
      /**
       * Change the current tempo by some amount.
       * @param {object} args - the block arguments.
       * @property {number} TEMPO - the amount to change the tempo, in beats per minute.
       */
      changeTempo (args) {
          const change = Cast.toNumber(args.TEMPO);
          const tempo = change + this.tempo;
          this._updateTempo(tempo);
      }
  
      /**
       * Update the current tempo, clamping it to the min and max allowable range.
       * @param {number} tempo - the tempo to set, in beats per minute.
       * @private
       */
      _updateTempo (tempo) {
          tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max);
          this.tempo = tempo;
      }
  
      /**
       * Get the current tempo.
       * @return {number} - the current tempo, in beats per minute.
       */
      getTempo () {
          return this.tempo;
      }
  }
  
  module.exports = Scratch3MusicBlocks;
  