/*
pattern.mjs - Core pattern representation for strudel
Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/pattern.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

import TimeSpan from './timespan.mjs';
import Fraction, { isFraction, lcm } from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';

import {
  uniqsortr,
  removeUndefineds,
  flatten,
  id,
  listRange,
  curry,
  _mod,
  numeralArgs,
  parseNumeral,
  pairs,
  zipWith,
  stringifyValues,
} from './util.mjs';
import drawLine from './drawLine.mjs';
import { errorLogger, logger } from './logger.mjs';
import { strudelScope } from './evaluate.mjs';

let stringParser;

let __steps = true;

export const calculateSteps = function (x) {
  __steps = x ? true : false;
};

// parser is expected to turn a string into a pattern
// if set, the reify function will parse all strings with it
// intended to use with mini to automatically interpret all strings as mini notation
export const setStringParser = (parser) => (stringParser = parser);

/** @class Class representing a pattern. */
export class Pattern {
  /**
   * Create a pattern. As an end user, you will most likely not create a Pattern directly.
   *
   * @param {function} query - The function that maps a `State` to an array of `Hap`.
   * @noAutocomplete
   */
  constructor(query, steps = undefined) {
    this.query = query;
    this._Pattern = true; // this property is used to detectinstance of another Pattern
    this._steps = steps; // in terms of number of steps per cycle
  }

  get _steps() {
    return this.__steps;
  }

  set _steps(steps) {
    this.__steps = steps === undefined ? undefined : Fraction(steps);
  }

  setSteps(steps) {
    this._steps = steps;
    return this;
  }

  withSteps(f) {
    if (!__steps) {
      return this;
    }
    return new Pattern(this.query, this._steps === undefined ? undefined : f(this._steps));
  }

  get hasSteps() {
    return this._steps !== undefined;
  }

  //////////////////////////////////////////////////////////////////////
  // Haskell-style functor, applicative and monadic operations

  /**
   * Returns a new pattern, with the function applied to the value of
   * each hap. It has the alias `fmap`.
   * @tags functional
   * @synonyms fmap
   * @param {Function} func to to apply to the value
   * @returns Pattern
   * @example
   * "0 1 2".withValue(v => v + 10).log()
   */
  withValue(func) {
    const result = new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
    result._steps = this._steps;
    return result;
  }

  // runs func on query state
  withState(func) {
    return new Pattern((state) => this.query(func(state)));
  }

  /**
   * see `withValue`
   * @noAutocomplete
   */
  fmap(func) {
    return this.withValue(func);
  }

  /**
   * Assumes 'this' is a pattern of functions, and given a function to
   * resolve wholes, applies a given pattern of values to that
   * pattern of functions.
   * @tags functional
   * @param {Function} whole_func
   * @param {Function} func
   * @noAutocomplete
   * @returns Pattern
   */
  appWhole(whole_func, pat_val) {
    const pat_func = this;
    const query = function (state) {
      const hap_funcs = pat_func.query(state);
      const hap_vals = pat_val.query(state);
      const apply = function (hap_func, hap_val) {
        const s = hap_func.part.intersection(hap_val.part);
        if (s == undefined) {
          return undefined;
        }
        return new Hap(
          whole_func(hap_func.whole, hap_val.whole),
          s,
          hap_func.value(hap_val.value),
          hap_val.combineContext(hap_func),
        );
      };
      return flatten(
        hap_funcs.map((hap_func) => removeUndefineds(hap_vals.map((hap_val) => apply(hap_func, hap_val)))),
      );
    };
    return new Pattern(query);
  }

  /**
   * When this method is called on a pattern of functions, it matches its haps
   * with those in the given pattern of values.  A new pattern is returned, with
   * each matching value applied to the corresponding function.
   *
   * In this `_appBoth` variant, where timespans of the function and value haps
   * are not the same but do intersect, the resulting hap has a timespan of the
   * intersection. This applies to both the part and the whole timespan.
   * @tags functional
   * @param {Pattern} pat_val
   * @noAutocomplete
   * @returns Pattern
   */
  appBoth(pat_val) {
    const pat_func = this;

    // Tidal's <*>
    const whole_func = function (span_a, span_b) {
      if (span_a == undefined || span_b == undefined) {
        return undefined;
      }
      return span_a.intersection_e(span_b);
    };
    const result = pat_func.appWhole(whole_func, pat_val);
    if (__steps) {
      result._steps = lcm(pat_val._steps, pat_func._steps);
    }
    return result;
  }

  /**
   * As with `appBoth`, but the `whole` timespan is not the intersection,
   * but the timespan from the function of patterns that this method is called
   * on. In practice, this means that the pattern structure, including onsets,
   * are preserved from the pattern of functions (often referred to as the left
   * hand or inner pattern).
   * @tags functional
   * @param {Pattern} pat_val
   * @noAutocomplete
   * @returns Pattern
   */
  appLeft(pat_val) {
    const pat_func = this;

    const query = function (state) {
      const haps = [];
      for (const hap_func of pat_func.query(state)) {
        const hap_vals = pat_val.query(state.setSpan(hap_func.wholeOrPart()));
        for (const hap_val of hap_vals) {
          const new_whole = hap_func.whole;
          const new_part = hap_func.part.intersection(hap_val.part);
          if (new_part) {
            const new_value = hap_func.value(hap_val.value);
            const new_context = hap_val.combineContext(hap_func);
            const hap = new Hap(new_whole, new_part, new_value, new_context);
            haps.push(hap);
          }
        }
      }
      return haps;
    };
    const result = new Pattern(query);
    result._steps = this._steps;
    return result;
  }

  /**
   * As with `appLeft`, but `whole` timespans are instead taken from the
   * pattern of values, i.e. structure is preserved from the right hand/outer
   * pattern.
   * @tags functional
   * @param {Pattern} pat_val
   * @noAutocomplete
   * @returns Pattern
   */
  appRight(pat_val) {
    const pat_func = this;

    const query = function (state) {
      const haps = [];
      for (const hap_val of pat_val.query(state)) {
        const hap_funcs = pat_func.query(state.setSpan(hap_val.wholeOrPart()));
        for (const hap_func of hap_funcs) {
          const new_whole = hap_val.whole;
          const new_part = hap_func.part.intersection(hap_val.part);
          if (new_part) {
            const new_value = hap_func.value(hap_val.value);
            const new_context = hap_val.combineContext(hap_func);
            const hap = new Hap(new_whole, new_part, new_value, new_context);
            haps.push(hap);
          }
        }
      }
      return haps;
    };
    const result = new Pattern(query);
    result._steps = pat_val._steps;
    return result;
  }

  bindWhole(choose_whole, func) {
    const pat_val = this;
    const query = function (state) {
      const withWhole = function (a, b) {
        return new Hap(
          choose_whole(a.whole, b.whole),
          b.part,
          b.value,
          Object.assign({}, a.context, b.context, {
            locations: (a.context.locations || []).concat(b.context.locations || []),
          }),
        );
      };
      const match = function (a) {
        return func(a.value)
          .query(state.setSpan(a.part))
          .map((b) => withWhole(a, b));
      };
      return flatten(pat_val.query(state).map((a) => match(a)));
    };
    return new Pattern(query);
  }

  bind(func) {
    const whole_func = function (a, b) {
      if (a == undefined || b == undefined) {
        return undefined;
      }
      return a.intersection_e(b);
    };
    return this.bindWhole(whole_func, func);
  }

  join() {
    // Flattens a pattern of patterns into a pattern, where wholes are
    // the intersection of matched inner and outer haps.
    return this.bind(id);
  }

  outerBind(func) {
    return this.bindWhole((a) => a, func).setSteps(this._steps);
  }

  outerJoin() {
    // Flattens a pattern of patterns into a pattern, where wholes are
    // taken from outer haps.
    return this.outerBind(id);
  }

  innerBind(func) {
    return this.bindWhole((_, b) => b, func);
  }

  innerJoin() {
    // Flattens a pattern of patterns into a pattern, where wholes are
    // taken from inner haps.
    return this.innerBind(id);
  }

  // Flatterns patterns of patterns, by retriggering/resetting inner patterns at onsets of outer pattern haps
  resetJoin(restart = false) {
    const pat_of_pats = this;
    return new Pattern((state) => {
      return (
        pat_of_pats
          // drop continuous haps from the outer pattern.
          .discreteOnly()
          .query(state)
          .map((outer_hap) => {
            return (
              outer_hap.value
                // reset = align the inner pattern cycle start to outer pattern haps
                // restart = align the inner pattern cycle zero to outer pattern haps
                .late(restart ? outer_hap.whole.begin : outer_hap.whole.begin.cyclePos())
                .query(state)
                .map((inner_hap) =>
                  new Hap(
                    // Supports continuous haps in the inner pattern
                    inner_hap.whole ? inner_hap.whole.intersection(outer_hap.whole) : undefined,
                    inner_hap.part.intersection(outer_hap.part),
                    inner_hap.value,
                  ).setContext(outer_hap.combineContext(inner_hap)),
                )
                // Drop haps that didn't intersect
                .filter((hap) => hap.part)
            );
          })
          .flat()
      );
    });
  }

  restartJoin() {
    return this.resetJoin(true);
  }

  // Like the other joins above, joins a pattern of patterns of values, into a flatter
  // pattern of values. In this case it takes whole cycles of the inner pattern to fit each event
  // in the outer pattern.
  squeezeJoin() {
    // A pattern of patterns, which we call the 'outer' pattern, with patterns
    // as values which we call the 'inner' patterns.
    const pat_of_pats = this;
    function query(state) {
      // Get the events with the inner patterns. Ignore continuous events (without 'wholes')
      const haps = pat_of_pats.discreteOnly().query(state);
      // A function to map over the events from the outer pattern.
      function flatHap(outerHap) {
        // Get the inner pattern, slowed and shifted so that the 'whole'
        // timespan of the outer event corresponds to the first cycle of the
        // inner event
        const inner_pat = outerHap.value._focusSpan(outerHap.wholeOrPart());
        // Get the inner events, from the timespan of the outer event's part
        const innerHaps = inner_pat.query(state.setSpan(outerHap.part));
        // A function to map over the inner events, to combine them with the
        // outer event
        function munge(outer, inner) {
          let whole = undefined;
          if (inner.whole && outer.whole) {
            whole = inner.whole.intersection(outer.whole);
            if (!whole) {
              // The wholes are present, but don't intersect
              return undefined;
            }
          }
          const part = inner.part.intersection(outer.part);
          if (!part) {
            // The parts don't intersect
            return undefined;
          }
          const context = inner.combineContext(outer);
          return new Hap(whole, part, inner.value, context);
        }
        return innerHaps.map((innerHap) => munge(outerHap, innerHap));
      }
      const result = flatten(haps.map(flatHap));
      // remove undefineds
      return result.filter((x) => x);
    }
    return new Pattern(query);
  }

  squeezeBind(func) {
    return this.fmap(func).squeezeJoin();
  }

  polyJoin = function () {
    const pp = this;
    return pp.fmap((p) => p.extend(pp._steps.div(p._steps))).outerJoin();
  };

  polyBind(func) {
    return this.fmap(func).polyJoin();
  }

  //////////////////////////////////////////////////////////////////////
  // Utility methods mainly for internal use

  /**
   * Query haps inside the given time span.
   *
   * @tags internals
   * @param {Fraction | number} begin from time
   * @param {Fraction | number} end to time
   * @returns Hap[]
   * @example
   * const pattern = sequence('a', ['b', 'c'])
   * const haps = pattern.queryArc(0, 1)
   * console.log(haps)
   * silence
   * @noAutocomplete
   */
  queryArc(begin, end, controls = {}) {
    try {
      return this.query(new State(new TimeSpan(begin, end), controls));
    } catch (err) {
      errorLogger(err, 'query');
      return [];
    }
  }

  /**
   * Returns a new pattern, with queries split at cycle boundaries. This makes
   * some calculations easier to express, as all haps are then constrained to
   * happen within a cycle.
   * @tags internals
   * @returns Pattern
   * @noAutocomplete
   */
  splitQueries() {
    const pat = this;
    const q = (state) => {
      return flatten(state.span.spanCycles.map((subspan) => pat.query(state.setSpan(subspan))));
    };
    return new Pattern(q);
  }

  /**
   * Returns a new pattern, where the given function is applied to the query
   * timespan before passing it to the original pattern.
   * @tags internals
   * @param {Function} func the function to apply
   * @returns Pattern
   * @noAutocomplete
   */
  withQuerySpan(func) {
    return new Pattern((state) => this.query(state.withSpan(func)));
  }

  withQuerySpanMaybe(func) {
    const pat = this;
    return new Pattern((state) => {
      const newState = state.withSpan(func);
      if (!newState.span) {
        return [];
      }
      return pat.query(newState);
    });
  }

  /**
   * As with `withQuerySpan`, but the function is applied to both the
   * begin and end time of the query timespan.
   * @tags internals
   * @param {Function} func the function to apply
   * @returns Pattern
   * @noAutocomplete
   */
  withQueryTime(func) {
    return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
  }

  /**
   * Similar to `withQuerySpan`, but the function is applied to the timespans
   * of all haps returned by pattern queries (both `part` timespans, and where
   * present, `whole` timespans).
   * @tags internals
   * @param {Function} func
   * @returns Pattern
   * @noAutocomplete
   */
  withHapSpan(func) {
    return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
  }

  /**
   * As with `withHapSpan`, but the function is applied to both the
   * begin and end time of the hap timespans.
   * @tags internals
   * @param {Function} func the function to apply
   * @returns Pattern
   * @noAutocomplete
   */
  withHapTime(func) {
    return this.withHapSpan((span) => span.withTime(func));
  }

  /**
   * Returns a new pattern with the given function applied to the list of haps returned by every query.
   * @tags internals
   * @param {Function} func
   * @returns Pattern
   * @noAutocomplete
   */
  withHaps(func) {
    const result = new Pattern((state) => func(this.query(state), state));
    result._steps = this._steps;
    return result;
  }

  /**
   * As with `withHaps`, but applies the function to every hap, rather than every list of haps.
   * @tags internals
   * @param {Function} func
   * @returns Pattern
   * @noAutocomplete
   */
  withHap(func) {
    return this.withHaps((haps) => haps.map(func));
  }

  /**
   * Returns a new pattern with the context field set to every hap set to the given value.
   * @tags internals
   * @param {*} context
   * @returns Pattern
   * @noAutocomplete
   */
  setContext(context) {
    return this.withHap((hap) => hap.setContext(context));
  }

  /**
   * Returns a new pattern with the given function applied to the context field of every hap.
   * @tags internals
   * @param {Function} func
   * @returns Pattern
   * @noAutocomplete
   */
  withContext(func) {
    const result = this.withHap((hap) => hap.setContext(func(hap.context)));
    if (this.__pure !== undefined) {
      result.__pure = this.__pure;
      result.__pure_loc = this.__pure_loc;
    }
    return result;
  }

  /**
   * Returns a new pattern with the context field of every hap set to an empty object.
   * @tags internals
   * @returns Pattern
   * @noAutocomplete
   */
  stripContext() {
    return this.withHap((hap) => hap.setContext({}));
  }

  /**
   * Returns a new pattern with the given location information added to the
   * context of every hap.
   * @tags internals
   * @param {Number} start start offset
   * @param {Number} end end offset
   * @returns Pattern
   * @noAutocomplete
   */
  withLoc(start, end) {
    const location = {
      start,
      end,
    };
    const result = this.withContext((context) => {
      const locations = (context.locations || []).concat([location]);
      return { ...context, locations };
    });
    if (this.__pure) {
      result.__pure = this.__pure;
      result.__pure_loc = location;
    }
    return result;
  }

  /**
   * Returns a new Pattern, which only returns haps that meet the given test.
   * @tags internals
   * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
   * @returns Pattern
   * @example
   * s("bd*8").velocity(rand).filterHaps((h) => (h.whole.begin % 1) < h.value.velocity)
   */
  filterHaps(hap_test) {
    return new Pattern((state) => this.query(state).filter(hap_test));
  }

  /**
   * As with `filterHaps`, but the function is applied to values
   * inside haps.
   * @tags internals
   * @param {Function} value_test
   * @returns Pattern
   * @example
   * const drums = s("bd sd bd sd")
   * kick: drums.filterValues((v) => v.s === 'bd').duck(2)
   * snare: drums.filterValues((v) => v.s === 'sd')
   * bass: s("saw!4").note("G#1").lpf(80).lpenv(4).orbit(2)
   */
  filterValues(value_test) {
    return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value))).setSteps(this._steps);
  }

  /**
   * Returns a new pattern, with haps containing undefined values removed from
   * query results.
   * @tags internals
   * @returns Pattern
   * @noAutocomplete
   */
  removeUndefineds() {
    return this.filterValues((val) => val != undefined);
  }

  /**
   * Returns a new pattern, with all haps without onsets filtered out. A hap
   * with an onset is one with a `whole` timespan that begins at the same time
   * as its `part` timespan.
   * @tags internals
   * @returns Pattern
   * @noAutocomplete
   */
  onsetsOnly() {
    // Returns a new pattern that will only return haps where the start
    // of the 'whole' timespan matches the start of the 'part'
    // timespan, i.e. the haps that include their 'onset'.
    return this.filterHaps((hap) => hap.hasOnset());
  }

  /**
   * Returns a new pattern, with 'continuous' haps (those without 'whole'
   * timespans) removed from query results.
   * @tags internals
   * @returns Pattern
   * @noAutocomplete
   */
  discreteOnly() {
    // removes continuous haps that don't have a 'whole' timespan
    return this.filterHaps((hap) => hap.whole);
  }

  /**
   * Combines adjacent haps with the same value and whole.  Only
   * intended for use in tests.
   * @tags internals
   * @noAutocomplete
   */
  defragmentHaps() {
    // remove continuous haps
    const pat = this.discreteOnly();

    return pat.withHaps((haps) => {
      const result = [];
      for (var i = 0; i < haps.length; ++i) {
        var searching = true;
        var a = haps[i];
        while (searching) {
          const a_value = JSON.stringify(haps[i].value);
          var found = false;

          for (var j = i + 1; j < haps.length; j++) {
            const b = haps[j];

            if (a.whole.equals(b.whole)) {
              if (a.part.begin.eq(b.part.end)) {
                if (a_value === JSON.stringify(b.value)) {
                  // eat the matching hap into 'a'
                  a = new Hap(a.whole, new TimeSpan(b.part.begin, a.part.end), a.value);
                  haps.splice(j, 1);
                  // restart the search
                  found = true;
                  break;
                }
              } else if (b.part.begin.eq(a.part.end)) {
                if (a_value == JSON.stringify(b.value)) {
                  // eat the matching hap into 'a'
                  a = new Hap(a.whole, new TimeSpan(a.part.begin, b.part.end), a.value);
                  haps.splice(j, 1);
                  // restart the search
                  found = true;
                  break;
                }
              }
            }
          }

          searching = found;
        }
        result.push(a);
      }
      return result;
    });
  }

  /**
   * Queries the pattern for the first cycle, returning Haps. Mainly of use when
   * debugging a pattern.
   * @tags internals
   * @param {Boolean} with_context - set to true, otherwise the context field
   * will be stripped from the resulting haps.
   * @returns [Hap]
   * @noAutocomplete
   */
  firstCycle(with_context = false) {
    var self = this;
    if (!with_context) {
      self = self.stripContext();
    }
    return self.query(new State(new TimeSpan(Fraction(0), Fraction(1))));
  }

  /**
   * Accessor for a list of values returned by querying the first cycle.
   * @tags internals
   * @noAutocomplete
   */
  get firstCycleValues() {
    return this.firstCycle().map((hap) => hap.value);
  }

  /**
   * More human-readable version of the `firstCycleValues` accessor.
   * @tags internals
   * @noAutocomplete
   */
  get showFirstCycle() {
    return this.firstCycle().map(
      (hap) => `${hap.value}: ${hap.whole.begin.toFraction()} - ${hap.whole.end.toFraction()}`,
    );
  }

  /**
   * Returns a new pattern, which returns haps sorted in temporal order. Mainly
   * of use when comparing two patterns for equality, in tests.
   * @tags internals
   * @returns Pattern
   * @noAutocomplete
   */
  sortHapsByPart() {
    return this.withHaps((haps) =>
      haps.sort((a, b) =>
        a.part.begin
          .sub(b.part.begin)
          .or(a.part.end.sub(b.part.end))
          .or(a.whole.begin.sub(b.whole.begin).or(a.whole.end.sub(b.whole.end))),
      ),
    );
  }

  /**
   * Returns a new pattern with all values parsed as numerals.
   * @tags internals
   */
  asNumber() {
    return this.fmap(parseNumeral);
  }

  //////////////////////////////////////////////////////////////////////
  // Operators - see 'make composers' later..

  _opIn(other, func) {
    return this.fmap(func).appLeft(reify(other));
  }
  _opOut(other, func) {
    return this.fmap(func).appRight(reify(other));
  }
  _opMix(other, func) {
    return this.fmap(func).appBoth(reify(other));
  }
  _opSqueeze(other, func) {
    const otherPat = reify(other);
    return this.fmap((a) => otherPat.fmap((b) => func(a)(b))).squeezeJoin();
  }
  _opSqueezeOut(other, func) {
    const thisPat = this;
    const otherPat = reify(other);
    return otherPat.fmap((a) => thisPat.fmap((b) => func(b)(a))).squeezeJoin();
  }
  _opReset(other, func) {
    const otherPat = reify(other);
    return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).resetJoin();
  }
  _opRestart(other, func) {
    const otherPat = reify(other);
    return otherPat.fmap((b) => this.fmap((a) => func(a)(b))).restartJoin();
  }
  _opPoly(other, func) {
    const otherPat = reify(other);
    return this.fmap((b) => otherPat.fmap((a) => func(a)(b))).polyJoin();
  }

  //////////////////////////////////////////////////////////////////////
  // End-user methods.
  // Those beginning with an underscore (_) are 'patternified',
  // i.e. versions are created without the underscore, that are
  // magically transformed to accept patterns for all their arguments.

  //////////////////////////////////////////////////////////////////////
  // Methods without corresponding toplevel functions

  /**
   * Layers the result of the given function(s). Like `superimpose`, but without the original pattern:
   * @name layer
   * @tags combiners
   * @memberof Pattern
   * @returns Pattern
   * @example
   * "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
   *   .layer(x=>x.add("0,2"))
   *   .scale('C minor').note()
   */
  layer(...funcs) {
    return stack(...funcs.map((func) => func(this)));
  }

  /**
   * Superimposes the result of the given function(s) on top of the original pattern:
   * @name superimpose
   * @tags combiners
   * @memberof Pattern
   * @returns Pattern
   * @example
   * "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
   *   .superimpose(x=>x.add(2))
   *   .scale('C minor').note()
   */
  superimpose(...funcs) {
    return this.stack(...funcs.map((func) => func(this)));
  }

  //////////////////////////////////////////////////////////////////////
  // Multi-pattern functions

  stack(...pats) {
    return stack(this, ...pats);
  }

  sequence(...pats) {
    return sequence(this, ...pats);
  }

  seq(...pats) {
    return sequence(this, ...pats);
  }
  cat(...pats) {
    return cat(this, ...pats);
  }

  fastcat(...pats) {
    return fastcat(this, ...pats);
  }

  slowcat(...pats) {
    return slowcat(this, ...pats);
  }

  //////////////////////////////////////////////////////////////////////
  // Context methods - ones that deal with metadata

  onTrigger(onTrigger, dominant = true) {
    return this.withHap((hap) =>
      hap.setContext({
        ...hap.context,
        onTrigger: (...args) => {
          // run previously set trigger, if it exists
          hap.context.onTrigger?.(...args);
          onTrigger(...args);
        },
        // if dominantTrigger is set to true, the default output (webaudio) will be disabled
        // when using multiple triggers, you cannot flip this flag to false again!
        // example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
        dominantTrigger: hap.context.dominantTrigger || dominant,
      }),
    );
  }

  /**
   * Writes the content of the current event to the console (visible in the side menu).
   * @tags visualization
   * @name log
   * @memberof Pattern
   * @example
   * s("bd sd").log()
   */
  log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
    return this.onTrigger((...args) => {
      logger(func(...args), undefined, getData(...args));
    }, false);
  }

  /**
   * A simplified version of `log` which writes all "values" (various configurable parameters)
   * within the event to the console (visible in the side menu).
   * @tags visualization
   * @name logValues
   * @memberof Pattern
   * @example
   * s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
   */
  logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
    return this.log((hap) => func(hap.value));
  }

  //////////////////////////////////////////////////////////////////////
  // Visualisation

  drawLine() {
    console.log(drawLine(this));
    return this;
  }

  //////////////////////////////////////////////////////////////////////
  // methods relating to breaking patterns into subcycles

  // Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
  unjoin(pieces, func = id) {
    return pieces.withHap((hap) =>
      hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)),
    );
  }

  /**
   * Breaks a pattern into pieces according to the structure of a given pattern.
   * True values in the given pattern cause the corresponding subcycle of the
   * source pattern to be looped, and for an (optional) given function to be
   * applied. False values result in the corresponding part of the source pattern
   * to be played unchanged.
   * @tags temporal
   * @name into
   * @memberof Pattern
   * @example
   * sound("bd sd ht lt").into("1 0", hurry(2))
   */
  into(pieces, func) {
    return this.unjoin(pieces, func).innerJoin();
  }
}

//////////////////////////////////////////////////////////////////////
// functions relating to chords/patterns of lists/lists of patterns

// returns Array<Hap[]> where each list of haps satisfies eq
function groupHapsBy(eq, haps) {
  let groups = [];
  haps.forEach((hap) => {
    const match = groups.findIndex(([other]) => eq(hap, other));
    if (match === -1) {
      groups.push([hap]);
    } else {
      groups[match].push(hap);
    }
  });
  return groups;
}

// congruent haps = haps with equal spans
const congruent = (a, b) => a.spanEquals(b);
// Pattern<Hap<T>> -> Pattern<Hap<T[]>>
// returned pattern contains arrays of congruent haps
Pattern.prototype.collect = function () {
  return this.withHaps((haps) =>
    groupHapsBy(congruent, haps).map((_haps) => new Hap(_haps[0].whole, _haps[0].part, _haps, {})),
  );
};

/**
 * Selects indices in in stacked notes.
 * @tags temporal
 * @example
 * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
 * .arpWith(haps => haps[2])
 * */
export const arpWith = register('arpWith', (func, pat) => {
  return pat
    .collect()
    .fmap((v) => reify(func(v)))
    .innerJoin()
    .withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
});

/**
 * Selects indices in in stacked notes.
 * @tags temporal
 * @example
 * note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
 * .arp("0 [0,2] 1 [0,2]")
 * */
export const arp = register(
  'arp',
  (indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
  false,
);

/*
 * Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
 * distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
 * @name weave
 * @memberof Pattern
 * @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
 * @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))

addToPrototype('weave', function (t, ...pats) {
  return this.weaveWith(t, ...pats.map((x) => set.out(x)));
});

*/
/*
 * Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
 * @name weaveWith
 * @memberof Pattern

addToPrototype('weaveWith', function (t, ...funcs) {
  const pat = this;
  const l = funcs.length;
  t = Fraction(t);
  if (l == 0) {
    return silence;
  }
  return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
});
*/

//////////////////////////////////////////////////////////////////////
// compose matrix functions

function _nonArrayObject(x) {
  return !Array.isArray(x) && typeof x === 'object' && !isFraction(x);
}
function _composeOp(a, b, func) {
  if (_nonArrayObject(a) || _nonArrayObject(b)) {
    if (!_nonArrayObject(a)) {
      a = { value: a };
    }
    if (!_nonArrayObject(b)) {
      b = { value: b };
    }
    return unionWithObj(a, b, func);
  }
  return func(a, b);
}

// pattern composers
const COMPOSERS = {
  /**
   * When called on a pattern `a`, with a input pattern `b` (`a.set(b)`),
   * combines `a` and `b` such that anything defined in `b`
   * and anything defined in `a` that is *not* defined in `b`
   * will be in the resulting pattern.
   *
   * The structure is maintained from `a`,
   * because the default pattern alignment is `in`,
   * see the section on `Pattern Alignment`
   * in the technical manual in the docs
   *
   * This is the inverse of `keep`
   *
   * See examples below
   * @name set
   * @param {Pattern} pat
   * @returns {Pattern}
   * @memberof Pattern
   * @tags internal, combiners
   * @example
   * // because input pattern has `s` set,
   * // it overrides the "sine" declared earlier
   * note("c a f e").s("sine").set(s("triangle"))
   */
  set: [(a, b) => b],
  /**
   * When called on a pattern `a`, with a input pattern `b` (`a.keep(b)`),
   * combines `a` and `b` such that anything defined in `a`,
   * and anything defined in `b` that is *not* defined in `a`
   * will be in the resulting pattern
   *
   * The structure is maintained from `a`,
   * because the default pattern alignment is `in`,
   * see the section on `Pattern Alignment`
   * in the technical manual in the docs
   *
   * This is the inverse of `set`
   *
   * See examples below
   * @name keep
   * @param {Pattern} pat
   * @memberof Pattern
   * @returns {Pattern}
   * @tags internal, combiners
   * @example
   * // notes, already defined, will stay "c a f e",
   * // while "s", not defined, will be set to "piano"
   * note("c a f e").keep(note("e f a c").s("piano"))
   */
  keep: [(a) => a],
  keepif: [(a, b) => (b ? a : undefined)],

  // numerical functions
  /**
   *
   * Assumes a pattern of numbers. Adds the given number to each item in the pattern.
   * @name add
   * @memberof Pattern
   * @tags math
   * @example
   * // Here, the triad 0, 2, 4 is shifted by different amounts
   * n("0 2 4".add("<0 3 4 0>")).scale("C:major")
   * // Without add, the equivalent would be:
   * // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
   * @example
   * // You can also use add with notes:
   * note("c3 e3 g3".add("<0 5 7 0>"))
   * // Behind the scenes, the notes are converted to midi numbers:
   * // note("48 52 55".add("<0 5 7 0>"))
   */
  add: [numeralArgs((a, b) => a + b)], // support string concatenation
  /**
   *
   * Like add, but the given numbers are subtracted.
   * @name sub
   * @memberof Pattern
   * @tags math
   * @example
   * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
   * // See add for more information.
   */
  sub: [numeralArgs((a, b) => a - b)],
  /**
   *
   * Multiplies each number by the given factor.
   * @name mul
   * @memberof Pattern
   * @tags math
   * @example
   * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
   */
  mul: [numeralArgs((a, b) => a * b)],
  /**
   *
   * Divides each number by the given factor.
   * @name div
   * @memberof Pattern
   * @tags math
   */
  div: [numeralArgs((a, b) => a / b)],
  mod: [numeralArgs(_mod)],
  pow: [numeralArgs(Math.pow)],
  band: [numeralArgs((a, b) => a & b)],
  bor: [numeralArgs((a, b) => a | b)],
  bxor: [numeralArgs((a, b) => a ^ b)],
  blshift: [numeralArgs((a, b) => a << b)],
  brshift: [numeralArgs((a, b) => a >> b)],

  // TODO - force numerical comparison if both look like numbers?
  lt: [(a, b) => a < b],
  gt: [(a, b) => a > b],
  lte: [(a, b) => a <= b],
  gte: [(a, b) => a >= b],
  eq: [(a, b) => a == b],
  eqt: [(a, b) => a === b],
  ne: [(a, b) => a != b],
  net: [(a, b) => a !== b],
  and: [(a, b) => a && b],
  or: [(a, b) => a || b],

  //  bitwise ops
  func: [(a, b) => b(a)],
};

const _setupAlignments = () => {
  // generate methods to do what and how
  for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) {
    // make plain version, e.g. pat._add(value) adds that plain value
    // to all the values in pat
    Pattern.prototype['_' + what] = function (value) {
      return this.fmap((x) => op(x, value));
    };

    // make patternified monster version
    Object.defineProperty(Pattern.prototype, what, {
      // Set to configurable so we can update if the default alignment changes
      configurable: true,
      // a getter that returns a function, so 'pat' can be
      // accessed by closures that are methods of that function..
      get: function () {
        const pat = this;

        // wrap the 'in' function as default behaviour
        const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other);

        // add methods to that function for each behaviour
        for (const how of ALIGNMENTS) {
          wrapper[how.toLowerCase()] = function (...other) {
            var howpat = pat;
            other = sequence(other);
            if (preprocess) {
              howpat = preprocess(howpat);
              other = preprocess(other);
            }
            var result;
            // hack to remove undefs when doing 'keepif'
            if (what === 'keepif') {
              // avoid union, as we want to throw away the value of 'b' completely
              result = howpat['_op' + how](other, (a) => (b) => op(a, b));
              result = result.removeUndefineds();
            } else {
              result = howpat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
            }
            return result;
          };
        }
        wrapper.squeezein = wrapper.squeeze;

        return wrapper;
      },
    });
  }
};

let DEFAULT_ALIGNMENT = 'in';
const ALIGNMENTS = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase());

// Make composers
(function () {
  _setupAlignments();

  // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
  for (const how of ALIGNMENTS) {
    Pattern.prototype[how.toLowerCase()] = function (...args) {
      return this.set[how.toLowerCase()](args);
    };
  }
  // binary composers
  /**
   * Applies the given structure to the pattern:
   *
   * @tags temporal
   * @example
   * note("c,eb,g")
   *   .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
   *   .slow(2)
   */
  Pattern.prototype.struct = function (...args) {
    return this.keepif.out(...args);
  };
  Pattern.prototype.structAll = function (...args) {
    return this.keep.out(...args);
  };
  /**
   * Returns silence when mask is 0 or "~"
   *
   * @tags temporal
   * @example
   * note("c [eb,g] d [eb,g]").mask("<1 [0 1]>")
   */
  Pattern.prototype.mask = function (...args) {
    return this.keepif.in(...args);
  };
  Pattern.prototype.maskAll = function (...args) {
    return this.keep.in(...args);
  };
  /**
   * Resets the pattern to the start of the cycle for each onset of the reset pattern.
   *
   * @tags temporal
   * @example
   * s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
   */
  Pattern.prototype.reset = function (...args) {
    return this.keepif.reset(...args);
  };
  Pattern.prototype.resetAll = function (...args) {
    return this.keep.reset(...args);
  };
  /**
   * Restarts the pattern for each onset of the restart pattern.
   * While reset will only reset the current cycle, restart will start from cycle 0.
   *
   * @tags temporal
   * @example
   * s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
   */
  Pattern.prototype.restart = function (...args) {
    return this.keepif.restart(...args);
  };
  Pattern.prototype.restartAll = function (...args) {
    return this.keep.restart(...args);
  };
})();

/**
 * Sets the default method of combining events from two patterns (aka [alignment](https://strudel.cc/technical-manual/alignment/)) in Strudel.
 * The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right.
 * By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings.
 *
 * Note that we say the _default_ method, because alignments can also be set explicitly with calls like
 * 'add.mix', 'set.squeeze', etc.
 *
 * @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly'
 * @tags combiners
 * @example
 * setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc.
 * s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3")
 */
export const setDefaultJoin = (alignment) => {
  alignment = alignment?.toLowerCase();
  if (DEFAULT_ALIGNMENT !== alignment && ALIGNMENT_KEYS.includes(alignment)) {
    DEFAULT_ALIGNMENT = alignment;
    _setupAlignments();
  }
};

// aliases
export const polyrhythm = stack;
export const pr = stack;

export const pm = polymeter;

// methods that create patterns, which are added to patternified Pattern methods
// TODO: remove? this is only used in old transpiler (shapeshifter)
// Pattern.prototype.factories = {
//   pure,
//   stack,
//   slowcat,
//   fastcat,
//   cat,
//   timecat,
//   sequence,
//   seq,
//   polymeter,
//   pm,
//   polyrhythm,
//   pr,
// };
// the magic happens in Pattern constructor. Keeping this in prototype enables adding methods from the outside (e.g. see tonal.ts)

// Elemental patterns

/**
 * Does absolutely nothing, but with a given metrical 'steps'
 * @name gap
 * @tags generators
 * @param  {number} steps
 * @example
 * gap(3) // "~@3"
 */
export const gap = (steps) => new Pattern(() => [], steps);

/**
 * Does absolutely nothing..
 * @name silence
 * @tags generators
 * @example
 * silence // "~"
 */
export const silence = gap(1);

/* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0);

/**
 * A discrete value that repeats once per cycle.
 *
 * @tags generators
 * @returns {Pattern}
 * @example
 * pure('e4') // "e4"
 * @noAutocomplete
 */
export function pure(value) {
  function query(state) {
    return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
  }
  const result = new Pattern(query, 1);
  result.__pure = value;
  return result;
}

export function isPattern(thing) {
  // thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
  const is = thing instanceof Pattern || thing?._Pattern;
  // TODO: find out how to check wrong core dependency. below will never work !thing === 'undefined'
  // wrapping it in (..) will result other checks to log that warning (e.g. isPattern('kalimba'))
  /* if (!thing instanceof Pattern) {
    console.warn(
      `Found Pattern that fails "instanceof Pattern" check.
      This may happen if you are using multiple versions of @strudel/core.
      Please check by running "npm ls @strudel/core".`,
    );
    console.log(thing);
  } */
  return is;
}

export function reify(thing) {
  // Turns something into a pattern, unless it's already a pattern
  if (isPattern(thing)) {
    return thing;
  }
  if (stringParser && typeof thing === 'string') {
    return stringParser(thing);
  }
  return pure(thing);
}

/**
 * Takes a list of patterns, and returns a pattern of lists.
 *
 * @tags temporal
 */
export function sequenceP(pats) {
  let result = pure([]);
  for (const pat of pats) {
    result = result.bind((list) => pat.fmap((v) => list.concat([v])));
  }
  return result;
}

/**
 * The given items are played at the same time at the same length.
 *
 * @tags temporal
 * @return {Pattern}
 * @synonyms polyrhythm, pr
 * @example
 * stack("g3", "b3", ["e4", "d4"]).note()
 * // "g3,b3,[e4 d4]".note()
 *
 * @example
 * // As a chained function:
 * s("hh*4").stack(
 *   note("c4(5,8)")
 * )
 */
export function stack(...pats) {
  // Array test here is to avoid infinite recursions..
  pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
  const query = (state) => flatten(pats.map((pat) => pat.query(state)));
  const result = new Pattern(query);
  if (__steps) {
    result._steps = lcm(...pats.map((pat) => pat._steps));
  }
  return result;
}

function _stackWith(func, pats) {
  pats = pats.map((pat) => (Array.isArray(pat) ? sequence(...pat) : reify(pat)));
  if (pats.length === 0) {
    return silence;
  }
  if (pats.length === 1) {
    return pats[0];
  }
  const [left, ...right] = pats.map((pat) => pat._steps);
  const steps = __steps ? left.maximum(...right) : undefined;
  return stack(...func(steps, pats));
}

export function stackLeft(...pats) {
  return _stackWith(
    (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(pat, gap(steps.sub(pat._steps))))),
    pats,
  );
}

export function stackRight(...pats) {
  return _stackWith(
    (steps, pats) => pats.map((pat) => (pat._steps.eq(steps) ? pat : stepcat(gap(steps.sub(pat._steps)), pat))),
    pats,
  );
}

export function stackCentre(...pats) {
  return _stackWith(
    (steps, pats) =>
      pats.map((pat) => {
        if (pat._steps.eq(steps)) {
          return pat;
        }
        const g = gap(steps.sub(pat._steps).div(2));
        return stepcat(g, pat, g);
      }),
    pats,
  );
}

export function stackBy(by, ...pats) {
  const [left, ...right] = pats.map((pat) => pat._steps);
  const steps = left.maximum(...right);
  const lookup = {
    centre: stackCentre,
    left: stackLeft,
    right: stackRight,
    expand: stack,
    repeat: (...args) => polymeter(...args).steps(steps),
  };
  return by
    .inhabit(lookup)
    .fmap((func) => func(...pats))
    .innerJoin()
    .setSteps(steps);
}

/**
 * Concatenation: combines a list of patterns, switching between them successively, one per cycle.
 *
 * @tags combiners
 * @return {Pattern}
 * @synonyms cat
 * @example
 * slowcat("e5", "b4", ["d5", "c5"])
 *
 */
export function slowcat(...pats) {
  // Array test here is to avoid infinite recursions..
  pats = pats.map((pat) => (Array.isArray(pat) ? fastcat(...pat) : reify(pat)));

  if (pats.length == 1) {
    return pats[0];
  }

  const query = function (state) {
    const span = state.span;
    const pat_n = _mod(span.begin.sam(), pats.length);
    const pat = pats[pat_n];
    if (!pat) {
      // pat_n can be negative, if the span is in the past..
      return [];
    }
    // A bit of maths to make sure that cycles from constituent patterns aren't skipped.
    // For example if three patterns are slowcat-ed, the fourth cycle of the result should
    // be the second (rather than fourth) cycle from the first pattern.
    const offset = span.begin.floor().sub(span.begin.div(pats.length).floor());
    return pat.withHapTime((t) => t.add(offset)).query(state.setSpan(span.withTime((t) => t.sub(offset))));
  };
  const steps = __steps ? lcm(...pats.map((x) => x._steps)) : undefined;
  return new Pattern(query).splitQueries().setSteps(steps);
}

/** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles.
 * @tags combiners
 * @param {...any} items - The items to concatenate
 * @return {Pattern}
 */
export function slowcatPrime(...pats) {
  pats = pats.map(reify);
  const query = function (state) {
    const pat_n = Math.floor(state.span.begin) % pats.length;
    const pat = pats[pat_n]; // can be undefined for same cases e.g. /#cHVyZSg0MikKICAuZXZlcnkoMyxhZGQoNykpCiAgLmxhdGUoLjUp
    return pat?.query(state) || [];
  };
  return new Pattern(query).splitQueries();
}

/** The given items are con**cat**enated, where each one takes one cycle.
 *
 * @tags combiners
 * @param {...any} items - The items to concatenate
 * @synonyms slowcat
 * @return {Pattern}
 * @example
 * cat("e5", "b4", ["d5", "c5"]).note()
 * // "<e5 b4 [d5 c5]>".note()
 *
 * @example
 * // As a chained function:
 * s("hh*4").cat(
 *    note("c4(5,8)")
 * )
 */
export function cat(...pats) {
  return slowcat(...pats);
}

/**
 * Allows to arrange multiple patterns together over multiple cycles.
 * Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
 *
 * @tags combiners
 * @return {Pattern}
 * @example
 * arrange(
 *   [4, "<c a f e>(3,8)"],
 *   [2, "<g a>(5,8)"]
 * ).note()
 */
export function arrange(...sections) {
  const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
  sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
  return stepcat(...sections).slow(total);
}

/**
 * Similarly to `arrange`, allows you to arrange multiple patterns together over multiple cycles.
 * Unlike `arrange`, you specify a start and stop time for each pattern rather than duration, which
 * means that patterns can overlap.
 * @tags combiners
 * @return {Pattern}
 * @example
seqPLoop(
  [0, 2, "bd(3,8)"],
  [1, 3, "cp(3,8)"]
).sound()
 */
export function seqPLoop(...parts) {
  let total = Fraction(0);
  const pats = [];
  for (let part of parts) {
    if (part.length == 2) {
      part.unshift(total);
    }
    total = part[1];
  }

  return stack(
    ...parts.map(([start, stop, pat]) =>
      pure(reify(pat)).compress(Fraction(start).div(total), Fraction(stop).div(total)),
    ),
  )
    .slow(total)
    .innerJoin(); // or resetJoin or restartJoin ??
}

export function fastcat(...pats) {
  let result = slowcat(...pats);
  if (pats.length > 1) {
    result = result._fast(pats.length);
    result._steps = pats.length;
  }
  if (pats.length == 1 && pats[0].__steps_source) {
    pats._steps = pats[0]._steps;
  }
  return result;
}

/** See `fastcat`
 * @name sequence
 * @tags combiners
 */
export function sequence(...pats) {
  return fastcat(...pats);
}

/** Like **cat**, but the items are crammed into one cycle.
 * @tags combiners
 * @synonyms fastcat
 * @example
 * seq("e5", "b4", ["d5", "c5"]).note()
 * // "e5 b4 [d5 c5]".note()
 *
 * @example
 * // As a chained function:
 * s("hh*4").seq(
 *   note("c4(5,8)")
 * )
 */

export function seq(...pats) {
  return fastcat(...pats);
}

function _sequenceCount(x) {
  if (Array.isArray(x)) {
    if (x.length == 0) {
      return [silence, 0];
    }
    if (x.length == 1) {
      return _sequenceCount(x[0]);
    }
    return [fastcat(...x.map((a) => _sequenceCount(a)[0])), x.length];
  }
  return [reify(x), 1];
}

export const mask = curry((a, b) => reify(b).mask(a));
export const struct = curry((a, b) => reify(b).struct(a));
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
export const withValue = curry((a, b) => reify(b).withValue(a));

export const bind = curry((a, b) => reify(b).bind(a));
export const innerBind = curry((a, b) => reify(b).innerBind(a));
export const outerBind = curry((a, b) => reify(b).outerBind(a));
export const squeezeBind = curry((a, b) => reify(b).squeezeBind(a));
export const stepBind = curry((a, b) => reify(b).stepBind(a));
export const polyBind = curry((a, b) => reify(b).polyBind(a));

// operators
export const set = curry((a, b) => reify(b).set(a));
export const keep = curry((a, b) => reify(b).keep(a));
export const keepif = curry((a, b) => reify(b).keepif(a));
export const add = curry((a, b) => reify(b).add(a));
export const sub = curry((a, b) => reify(b).sub(a));
export const mul = curry((a, b) => reify(b).mul(a));
export const div = curry((a, b) => reify(b).div(a));
export const mod = curry((a, b) => reify(b).mod(a));
export const pow = curry((a, b) => reify(b).pow(a));
export const band = curry((a, b) => reify(b).band(a));
export const bor = curry((a, b) => reify(b).bor(a));
export const bxor = curry((a, b) => reify(b).bxor(a));
export const blshift = curry((a, b) => reify(b).blshift(a));
export const brshift = curry((a, b) => reify(b).brshift(a));
export const lt = curry((a, b) => reify(b).lt(a));
export const gt = curry((a, b) => reify(b).gt(a));
export const lte = curry((a, b) => reify(b).lte(a));
export const gte = curry((a, b) => reify(b).gte(a));
export const eq = curry((a, b) => reify(b).eq(a));
export const eqt = curry((a, b) => reify(b).eqt(a));
export const ne = curry((a, b) => reify(b).ne(a));
export const net = curry((a, b) => reify(b).net(a));
export const and = curry((a, b) => reify(b).and(a));
export const or = curry((a, b) => reify(b).or(a));
export const func = curry((a, b) => reify(b).func(a));

/**
 * Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
 *
 * @tags functional
 * @param {string | string[]} name name of the function, or an array of names to be used as synonyms
 * @param {function} func function with 1 or more params, where last is the current pattern
 * @param {bool} patternify defaults to true; if set to false, you will have more control over the arguments to `func` as they will be
 *   in their raw form and it will be up to you to patternify them and/or query them for values
 * @example
 * const vlpf = register('vlpf', (freq, pat) => {
 *   return pat.fmap((v) => ({...v, cutoff: freq * (v.velocity ?? 1) }));
 * })
 * s("saw").seg(8).velocity(rand).vlpf(800)
 *
 */
export function register(name, func, patternify = true, preserveSteps = false, join = (x) => x.innerJoin()) {
  if (isPattern(name)) {
    throw new Error(
      'Name argument for register is a pattern, try using single quotes (\'name\') instead of double quotes ("name")',
    );
  }

  if (Array.isArray(name)) {
    const result = {};
    for (const name_item of name) {
      result[name_item] = register(name_item, func, patternify, preserveSteps, join);
    }
    return result;
  }
  const arity = func.length;
  var pfunc; // the patternified function

  if (patternify) {
    pfunc = function (...args) {
      args = args.map(reify);
      const pat = args[args.length - 1];
      let result;

      if (arity === 1) {
        result = func(pat);
      } else {
        const firstArgs = args.slice(0, -1);

        if (firstArgs.every((arg) => arg.__pure != undefined)) {
          const pureArgs = firstArgs.map((arg) => arg.__pure);
          const pureLocs = firstArgs.filter((arg) => arg.__pure_loc).map((arg) => arg.__pure_loc);
          result = func(...pureArgs, pat);
          result = result.withContext((context) => {
            const locations = (context.locations || []).concat(pureLocs);
            return { ...context, locations };
          });
        } else {
          const [left, ...right] = firstArgs;

          let mapFn = (...args) => {
            return func(...args, pat);
          };
          mapFn = curry(mapFn, null, arity - 1);
          result = join(right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)));
        }
      }
      if (preserveSteps) {
        result._steps = pat._steps;
      }
      return result;
    };
  } else {
    pfunc = function (...args) {
      args = args.map(reify);
      const result = func(...args);
      if (preserveSteps) {
        result._steps = args[args.length - 1]._steps;
      }
      return result;
    };
  }

  Pattern.prototype[name] = function (...args) {
    // For methods that take a single argument (plus 'this'), allow
    // multiple arguments but sequence them
    if (arity === 2 && args.length !== 1) {
      args = [sequence(...args)];
    } else if (arity !== args.length + 1) {
      throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
    }
    args = args.map(reify);
    return pfunc(...args, this);
  };

  if (arity > 1) {
    // There are patternified args, so lets make an unpatternified
    // version, prefixed by '_'
    Pattern.prototype['_' + name] = function (...args) {
      const result = func(...args, this);
      if (preserveSteps) {
        result.setSteps(this._steps);
      }
      return result;
    };
  }

  // toplevel functions get curried as well as patternified
  // because pfunc uses spread args, we need to state the arity explicitly!
  const curried = curry(pfunc, null, arity);
  strudelScope[name] = curried;
  return curried;
}

// Like register, but defaults to stepJoin
function stepRegister(name, func, patternify = true, preserveSteps = false, join = (x) => x.stepJoin()) {
  return register(name, func, patternify, preserveSteps, join);
}

//////////////////////////////////////////////////////////////////////
// Numerical transformations

/**
 * Assumes a numerical pattern. Returns a new pattern with all values rounded
 * to the nearest integer.
 * @name round
 * @tags math
 * @memberof Pattern
 * @returns Pattern
 * @example
 * n("0.5 1.5 2.5".round()).scale("C:major")
 */
export const round = register('round', function (pat) {
  return pat.asNumber().fmap((v) => Math.round(v));
});
/**
 * Assumes a numerical pattern. Returns a new pattern with all values set to
 * their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2`
 * replaced with `-5`.
 * @name floor
 * @memberof Pattern
 * @tags math
 * @returns Pattern
 * @example
 * note("42 42.1 42.5 43".floor())
 */
export const floor = register('floor', function (pat) {
  return pat.asNumber().fmap((v) => Math.floor(v));
});

export const log2 = register('log2', (pat) => pat.asNumber().fmap((v) => Math.log2(v)));

/**
 * Assumes a numerical pattern. Returns a new pattern with all values set to
 * their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2`
 * replaced with `-4`.
 * @name ceil
 * @memberof Pattern
 * @tags math
 * @returns Pattern
 * @example
 * note("42 42.1 42.5 43".ceil())
 */
export const ceil = register('ceil', function (pat) {
  return pat.asNumber().fmap((v) => Math.ceil(v));
});
/**
 * Assumes a numerical pattern, containing unipolar values in the range 0 ..
 * 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1
 * @tags math
 * @returns Pattern
 * @noAutocomplete
 */
export const toBipolar = register('toBipolar', function (pat) {
  return pat.fmap((x) => x * 2 - 1);
});

/**
 * Assumes a numerical pattern, containing bipolar values in the range -1 .. 1
 * Returns a new pattern with values scaled to the unipolar range 0 .. 1
 * @tags math
 * @returns Pattern
 * @noAutocomplete
 */
export const fromBipolar = register('fromBipolar', function (pat) {
  return pat.fmap((x) => (x + 1) / 2);
});

/**
 * Assumes a numerical pattern, containing unipolar values in the range 0 .. 1.
 * Returns a new pattern with values scaled to the given min/max range.
 * Most useful in combination with continuous patterns.
 * @name range
 * @memberof Pattern
 * @tags math
 * @returns Pattern
 * @example
 * s("[bd sd]*2,hh*8")
 * .cutoff(sine.range(500,4000))
 */
export const range = register('range', function (min, max, pat) {
  return pat.mul(max - min).add(min);
});

/**
 * Assumes a numerical pattern, containing unipolar values in the range 0 .. 1
 * Returns a new pattern with values scaled to the given min/max range,
 * following an exponential curve.
 * @name rangex
 * @memberof Pattern
 * @tags math
 * @returns Pattern
 * @example
 * s("[bd sd]*2,hh*8")
 * .cutoff(sine.rangex(500,4000))
 */
export const rangex = register('rangex', function (min, max, pat) {
  return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
});

/**
 * Assumes a numerical pattern, containing bipolar values in the range -1 .. 1
 * Returns a new pattern with values scaled to the given min/max range.
 * @name range2
 * @memberof Pattern
 * @tags math
 * @returns Pattern
 * @example
 * s("[bd sd]*2,hh*8")
 * .cutoff(sine2.range2(500,4000))
 */
export const range2 = register('range2', function (min, max, pat) {
  return pat.fromBipolar()._range(min, max);
});

/**
 * Allows dividing numbers via list notation using ":".
 * Returns a new pattern with just numbers.
 * @name ratio
 * @memberof Pattern
 * @tags math
 * @returns Pattern
 * @example
 * ratio("1, 5:4, 3:2").mul(110)
 * .freq().s("piano")
 */
export const ratio = register('ratio', (pat) =>
  pat.fmap((v) => {
    if (!Array.isArray(v)) {
      return v;
    }
    return v.slice(1).reduce((acc, n) => acc / n, v[0]);
  }),
);

//////////////////////////////////////////////////////////////////////
// Structural and temporal transformations

/** Compress each cycle into the given timespan, leaving a gap
 * @tags temporal
 * @example
 * cat(
 *   s("bd sd").compress(.25,.75),
 *   s("~ bd sd ~")
 * )
 */
export const compress = register('compress', function (b, e, pat) {
  b = Fraction(b);
  e = Fraction(e);
  if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
    return silence;
  }
  return pat._fastGap(Fraction(1).div(e.sub(b)))._late(b);
});

export const { compressSpan, compressspan } = register(['compressSpan', 'compressspan'], function (span, pat) {
  return pat._compress(span.begin, span.end);
});

/**
 * speeds up a pattern like fast, but rather than it playing multiple times as fast would it instead leaves a gap in the remaining space of the cycle. For example, the following will play the sound pattern "bd sn" only once but compressed into the first half of the cycle, i.e. twice as fast.
 * @tags temporal
 * @name fastGap
 * @synonyms fastgap
 * @example
 * s("bd sd").fastGap(2)
 */
export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (factor, pat) {
  // A bit fiddly, to drop zero-width queries at the start of the next cycle
  const qf = function (span) {
    const cycle = span.begin.sam();
    const bpos = span.begin.sub(cycle).mul(factor).min(1);
    const epos = span.end.sub(cycle).mul(factor).min(1);
    if (bpos >= 1) {
      return undefined;
    }
    return new TimeSpan(cycle.add(bpos), cycle.add(epos));
  };
  // Also fiddly, to maintain the right 'whole' relative to the part
  const ef = function (hap) {
    const begin = hap.part.begin;
    const end = hap.part.end;
    const cycle = begin.sam();
    const beginPos = begin.sub(cycle).div(factor).min(1);
    const endPos = end.sub(cycle).div(factor).min(1);
    const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos));
    const newWhole = !hap.whole
      ? undefined
      : new TimeSpan(
          newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)),
          newPart.end.add(hap.whole.end.sub(end).div(factor)),
        );
    return new Hap(newWhole, newPart, hap.value, hap.context);
  };
  return pat.withQuerySpanMaybe(qf).withHap(ef).splitQueries();
});

/**
 * Similar to `compress`, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
 * @tags temporal
 * @example
 * s("bd hh sd hh").focus(1/4, 3/4)
 */
export const focus = register('focus', function (b, e, pat) {
  b = Fraction(b);
  e = Fraction(e);
  return pat
    ._early(b.sam())
    ._fast(Fraction(1).div(e.sub(b)))
    ._late(b);
});

export const { focusSpan, focusspan } = register(['focusSpan', 'focusspan'], function (span, pat) {
  return pat._focus(span.begin, span.end);
});

/** The ply function repeats each event the given number of times.
 * @tags temporal
 * @example
 * s("bd ~ sd cp").ply("<1 2 3>")
 */
export const ply = register('ply', function (factor, pat) {
  const result = pat.fmap((x) => pure(x)._fast(factor)).squeezeJoin();
  if (__steps) {
    result._steps = Fraction(factor).mulmaybe(pat._steps);
  }
  return result;
});

/**
 * Speed up a pattern by the given factor. Used by "*" in mini notation.
 *
 * @tags temporal
 * @name fast
 * @synonyms density
 * @memberof Pattern
 * @param {number | Pattern} factor speed up factor
 * @returns Pattern
 * @example
 * s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
 */
export const { fast, density } = register(
  ['fast', 'density'],
  function (factor, pat) {
    if (factor === 0) {
      return silence;
    }
    factor = Fraction(factor);
    const fastQuery = pat.withQueryTime((t) => t.mul(factor));
    return fastQuery.withHapTime((t) => t.div(factor)).setSteps(pat._steps);
  },
  true,
  true,
);

/**
 * Both speeds up the pattern (like 'fast') and the sample playback (like 'speed').
 * @tags temporal
 * @example
 * s("bd sd:2").hurry("<1 2 4 3>").slow(1.5)
 */
export const hurry = register('hurry', function (r, pat) {
  return pat._fast(r).mul(pure({ speed: r }));
});

/**
 * Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation.
 *
 * @tags temporal
 * @name slow
 * @synonyms sparsity
 * @memberof Pattern
 * @param {number | Pattern} factor slow down factor
 * @returns Pattern
 * @example
 * s("bd hh sd hh").slow(2) // s("[bd hh sd hh]/2")
 */
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
  if (factor === 0) {
    return silence;
  }
  return pat._fast(Fraction(1).div(factor));
});

/**
 * Carries out an operation 'inside' a cycle.
 * @tags temporal
 * @example
 * "0 1 2 3 4 3 2 1".inside(4, rev).scale('C major').note()
 * // "0 1 2 3 4 3 2 1".slow(4).rev().fast(4).scale('C major').note()
 */
export const inside = register('inside', function (factor, f, pat) {
  return f(pat._slow(factor))._fast(factor);
});

/**
 * Carries out an operation 'outside' a cycle.
 * @tags temporal
 * @example
 * "<[0 1] 2 [3 4] 5>".outside(4, rev).scale('C major').note()
 * // "<[0 1] 2 [3 4] 5>".fast(4).rev().slow(4).scale('C major').note()
 */
export const outside = register('outside', function (factor, f, pat) {
  return f(pat._fast(factor))._slow(factor);
});

/**
 * Applies the given function every n cycles, starting from the last cycle.
 * @tags temporal
 * @name lastOf
 * @memberof Pattern
 * @param {number} n how many cycles
 * @param {function} func function to apply
 * @returns Pattern
 * @example
 * note("c3 d3 e3 g3").lastOf(4, x=>x.rev())
 */
export const lastOf = register('lastOf', function (n, func, pat) {
  const pats = Array(n - 1).fill(pat);
  pats.push(func(pat));
  return slowcatPrime(...pats);
});

/**
 * Applies the given function every n cycles, starting from the first cycle.
 * @tags temporal
 * @name firstOf
 * @memberof Pattern
 * @param {number} n how many cycles
 * @param {function} func function to apply
 * @returns Pattern
 * @example
 * note("c3 d3 e3 g3").firstOf(4, x=>x.rev())
 */

/**
 * An alias for `firstOf`
 * @tags temporal
 * @name every
 * @memberof Pattern
 * @param {number} n how many cycles
 * @param {function} func function to apply
 * @returns Pattern
 * @example
 * note("c3 d3 e3 g3").every(4, x=>x.rev())
 */
export const { firstOf, every } = register(['firstOf', 'every'], function (n, func, pat) {
  const pats = Array(n - 1).fill(pat);
  pats.unshift(func(pat));
  return slowcatPrime(...pats);
});

/**
 * Applies the given function to the pattern. Like layer, but with a single function:
 * @tags combiners
 * @name apply
 * @example
 * "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
 */
export const apply = register('apply', function (func, pat) {
  return func(pat);
});

/**
 * Plays the pattern at the given cycles per minute.
 * @tags temporal
 * @deprecated
 * @example
 * s("<bd sd>,hh*2").cpm(90) // = 90 bpm
 */
// this is redefined in repl.mjs, using the current cps as divisor
export const cpm = register('cpm', function (cpm, pat) {
  return pat._fast(cpm / 60 / 1);
});

/**
 * Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator
 *
 * @tags temporal
 * @name early
 * @memberof Pattern
 * @param {number | Pattern} cycles number of cycles to nudge left
 * @returns Pattern
 * @example
 * "bd ~".stack("hh ~".early(.1)).s()
 */
export const early = register(
  'early',
  function (offset, pat) {
    offset = Fraction(offset);
    return pat.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
  },
  true,
  true,
);

/**
 * Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
 *
 * @tags temporal
 * @name late
 * @memberof Pattern
 * @param {number | Pattern} cycles number of cycles to nudge right
 * @returns Pattern
 * @example
 * "bd ~".stack("hh ~".late(.1)).s()
 */
export const late = register(
  'late',
  function (offset, pat) {
    offset = Fraction(offset);
    return pat._early(Fraction(0).sub(offset));
  },
  true,
  true,
);

/**
 * Plays a portion of a pattern, specified by the beginning and end of a time span. The new resulting pattern is played over the time period of the original pattern:
 *
 * @tags temporal
 * @example
 * s("bd*2 hh*3 [sd bd]*2 perc").zoom(0.25, 0.75)
 * // s("hh*3 [sd bd]*2") // equivalent
 */
export const zoom = register('zoom', function (s, e, pat) {
  e = Fraction(e);
  s = Fraction(s);
  if (s.gte(e)) {
    return nothing;
  }
  const d = e.sub(s);
  const steps = __steps ? pat._steps?.mulmaybe(d) : undefined;
  return pat
    .withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
    .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
    .splitQueries()
    .setSteps(steps);
});

export const { zoomArc, zoomarc } = register(['zoomArc', 'zoomarc'], function (a, pat) {
  return pat.zoom(a.begin, a.end);
});

/**
 * Splits a pattern into the given number of slices, and plays them according to a pattern of slice numbers.
 * Similar to `slice`, but slices up patterns rather than sound samples.
 * @tags temporal
 * @param {number} number of slices
 * @param {number} slices to play
 * @example
 * note("0 1 2 3 4 5 6 7".scale('c:mixolydian'))
 *.bite(4, "3 2 1 0")
 * @example
 * sound("bd - bd bd*2, - sd:6 - sd:5 sd:1 - [- sd:2] -, hh [- cp:7]")
  .bank("RolandTR909").speed(1.2)
  .bite(4, "0 0 [1 2] <3 2> 0 0 [2 1] 3")
 */
export const bite = register(
  'bite',
  (npat, ipat, pat) => {
    return ipat
      .fmap((i) => (n) => {
        const a = Fraction(i).div(n).mod(1);
        const b = a.add(Fraction(1).div(n));
        return pat.zoom(a, b);
      })
      .appLeft(npat)
      .squeezeJoin();
  },
  false,
);

/**
 * Selects the given fraction of the pattern and repeats that part to fill the remainder of the cycle.
 * @tags temporal
 * @param {number} fraction fraction to select
 * @example
 * s("lt ht mt cp, [hh oh]*2").linger("<1 .5 .25 .125>")
 */
export const linger = register(
  'linger',
  function (t, pat) {
    if (t == 0) {
      return silence;
    } else if (t < 0) {
      return pat._zoom(t.add(1), 1)._slow(t);
    }
    return pat._zoom(0, t)._slow(t);
  },
  true,
  true,
);

/**
 * Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
 * @tags temporal
 * @name segment
 * @synonyms seg
 * @param {number} segments number of segments per cycle
 * @example
 * note(saw.range(40,52).segment(24))
 */
export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) {
  return pat.struct(pure(true)._fast(rate)).setSteps(rate);
});

/**
 * The function `swingBy x n` breaks each cycle into `n` slices, and then delays events in the second half of each slice by the amount `x`, which is relative to the size of the (half) slice. So if `x` is 0 it does nothing, `0.5` delays for half the note duration, and 1 will wrap around to doing nothing again. The end result is a shuffle or swing-like rhythm
 * @tags temporal
 * @param {number} subdivision
 * @param {number} offset
 * @example
 * s("hh*8").swingBy(1/3, 4)
 */
export const swingBy = register('swingBy', (swing, n, pat) => pat.inside(n, late(seq(0, swing / 2))));

/**
 * Shorthand for swingBy with 1/3:
 * @tags temporal
 * @param {number} subdivision
 * @example
 * s("hh*8").swing(4)
 * // s("hh*8").swingBy(1/3, 4)
 */
export const swing = register('swing', (n, pat) => pat.swingBy(1 / 3, n));

/**
 * Swaps 1s and 0s in a binary pattern.
 * @tags temporal
 * @name invert
 * @synonyms inv
 * @example
 * s("bd").struct("1 0 0 1 0 0 1 0".lastOf(4, invert))
 */
export const { invert, inv } = register(
  ['invert', 'inv'],
  function (pat) {
    // Swap true/false in a binary pattern
    return pat.fmap((x) => !x);
  },
  true,
  true,
);

/**
 * Applies the given function whenever the given pattern is in a true state.
 * @tags temporal
 * @name when
 * @memberof Pattern
 * @param {Pattern} binary_pat
 * @param {function} func
 * @returns Pattern
 * @example
 * "c3 eb3 g3".when("<0 1>/2", x=>x.sub("5")).note()
 */
export const when = register('when', function (on, func, pat) {
  return on ? func(pat) : pat;
});

/**
 * Superimposes the function result on top of the original pattern, delayed by the given time.
 * @tags temporal
 * @name off
 * @memberof Pattern
 * @param {Pattern | number} time offset time
 * @param {function} func function to apply
 * @returns Pattern
 * @example
 * "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
 */
export const off = register('off', function (time_pat, func, pat) {
  return stack(pat, func(pat.late(time_pat)));
});

/**
 * Returns a new pattern where every other cycle is played once, twice as
 * fast, and offset in time by one quarter of a cycle. Creates a kind of
 * breakbeat feel.
 * @tags temporal
 * @returns Pattern
 */
export const brak = register('brak', function (pat) {
  return pat.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
});

/**
 * Reverse all cycles in a pattern. See also `revv` for reversing a whole pattern.
 *
 * @tags temporal
 * @name rev
 * @memberof Pattern
 * @returns Pattern
 * @example
 * note("c d e g").rev()
 */
export const rev = register(
  'rev',
  function (pat) {
    const query = function (state) {
      const span = state.span;
      const cycle = span.begin.sam();
      const next_cycle = span.begin.nextSam();
      const reflect = function (to_reflect) {
        const reflected = to_reflect.withTime((time) => cycle.add(next_cycle.sub(time)));
        // [reflected.begin, reflected.end] = [reflected.end, reflected.begin] -- didn't work
        const tmp = reflected.begin;
        reflected.begin = reflected.end;
        reflected.end = tmp;
        return reflected;
      };
      const haps = pat.query(state.setSpan(reflect(span)));
      return haps.map((hap) => hap.withSpan(reflect));
    };
    return new Pattern(query).splitQueries();
  },
  false,
  true,
);

/**
 * Reverse a whole pattern. See also `rev` for reversing each cycle.
 *
 * @name revv
 * @tags temporal
 * @memberof Pattern
 * @returns Pattern
 * @example
 * // This is the same as `<[g e] [d c]>`. If `rev()` is used, you get
 * // the same as `<[d c] [g e]>`, where each cycle reverses, but the order of
 * // cycles stays the same.
 * note("<[c d] [e g]>").revv()
 */
export const revv = register('revv', function (pat) {
  const negateSpan = (span) => new TimeSpan(Fraction(0).sub(span.end), Fraction(0).sub(span.begin));
  return pat.withQuerySpan(negateSpan).withHapSpan(negateSpan);
});

/** Like press, but allows you to specify the amount by which each
 * event is shifted. pressBy(0.5) is the same as press, while
 * pressBy(1/3) shifts each event by a third of its timespan.
 * @tags temporal
 * @example
 * stack(s("hh*4"),
 *       s("bd mt sd ht").pressBy("<0 0.5 0.25>")
 *      ).slow(2)
 */
export const pressBy = register('pressBy', function (r, pat) {
  return pat.fmap((x) => pure(x).compress(r, 1)).squeezeJoin();
});

/**
 * Syncopates a rhythm, by shifting each event halfway into its timespan.
 * @tags temporal
 * @example
 * stack(s("hh*4"),
 *       s("bd mt sd ht").every(4, press)
 *      ).slow(2)
 */
export const press = register('press', function (pat) {
  return pat._pressBy(0.5);
});

/**
 * Silences a pattern.
 * @tags temporal
 * @example
 * stack(
 *   s("bd").hush(),
 *   s("hh*3")
 * )
 */
Pattern.prototype.hush = function () {
  return silence;
};

/**
 * Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
 * @tags temporal
 * @example
 * note("c d e g").palindrome()
 */
export const palindrome = register(
  'palindrome',
  function (pat) {
    return pat.lastOf(2, rev);
  },
  true,
  true,
);

/**
 * Jux with adjustable stereo width. 0 = mono, 1 = full stereo.
 * @tags temporal
 * @name juxBy
 * @synonyms juxby
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)
 */
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
  by /= 2;
  const elem_or = function (dict, key, dflt) {
    if (key in dict) {
      return dict[key];
    }
    return dflt;
  };
  const left = pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
  const right = func(pat.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by })));

  return stack(left, right).setSteps(__steps ? lcm(left._steps, right._steps) : undefined);
});

/**
 * Like juxBy, except it flips the ears each cycle.
 * @name juxFlipBy
 * @synonyms juxflipby, fluxBy, fluxby
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").juxFlipBy(".8", rev)
 */
export const { juxFlipBy, juxflipby, fluxBy, fluxby } = register(
  ['juxFlipBy', 'juxflipby', 'fluxBy', 'fluxby'],
  function (by, func, pat) {
    return pat.juxBy(slowcat(by, -by), func);
  },
);

/**
 * The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
 * @tags temporal, superdough
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").jux(press)
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").jux(iter(4))
 */
export const jux = register('jux', function (func, pat) {
  return pat._juxBy(1, func, pat);
});

/**
 * Like jux, but flips the ears each cycle.
 * @name juxFlip
 * @synonyms juxflip, flux
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(rev)
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(press)
 * @example
 * s("bd lt [~ ht] mt cp ~ bd hh").juxFlip(iter(4))
 */
export const { juxFlip, flux } = register(['juxFlip', 'juxflip', 'flux'], function (func, pat) {
  return pat._juxFlipBy(1, func, pat);
});

/**
 * Superimpose and offset multiple times, applying the given function each time.
 * @tags temporal, functional
 * @name echoWith
 * @synonyms echowith, stutWith, stutwith
 * @param {number} times how many times to repeat
 * @param {number} time cycle offset between iterations
 * @param {function} func function to apply, given the pattern and the iteration index
 * @example
 * "<0 [2 4]>"
 * .echoWith(4, 1/8, (p,n) => p.add(n*2))
 * .scale("C:minor").note()
 */
export const { echoWith, echowith, stutWith, stutwith } = register(
  ['echoWith', 'echowith', 'stutWith', 'stutwith'],
  function (times, time, func, pat) {
    return stack(...listRange(0, times - 1).map((i) => func(pat.late(Fraction(time).mul(i)), i)));
  },
);

/**
 * Superimpose and offset multiple times, gradually decreasing the velocity
 * @tags temporal
 * @name echo
 * @memberof Pattern
 * @returns Pattern
 * @param {number} times how many times to repeat
 * @param {number} time cycle offset between iterations
 * @param {number} feedback velocity multiplicator for each iteration
 * @example
 * s("bd sd").echo(3, 1/6, .8)
 */
export const echo = register('echo', function (times, time, feedback, pat) {
  return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});

/**
 * Deprecated. Like echo, but the last 2 parameters are flipped.
 * @tags temporal
 * @name stut
 * @param {number} times how many times to repeat
 * @param {number} feedback velocity multiplicator for each iteration
 * @param {number} time cycle offset between iterations
 * @example
 * s("bd sd").stut(3, .8, 1/6)
 */
export const stut = register('stut', function (times, feedback, time, pat) {
  return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
});

export const applyN = register('applyN', function (n, func, p) {
  let result = p;
  for (let i = 0; i < n; i++) {
    result = func(result);
  }
  return result;
});

/**
 * The plyWith function repeats each event the given number of times, applying the given function to each event.\n
 * @tags temporal
 * @name plyWith
 * @synonyms plywith
 * @param {number} factor how many times to repeat
 * @param {function} func function to apply, given the pattern
 * @example
 * "<0 [2 4]>"
 * .plyWith(4, (p) => p.add(2))
 * .scale("C:minor").note()
 */
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
  const result = pat
    .fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
    .squeezeJoin();
  if (__steps) {
    result._steps = Fraction(factor).mulmaybe(pat._steps);
  }
  return result;
});

/**
 * The plyForEach function repeats each event the given number of times, applying the given function to each event.
 * This version of ply uses the iteration index as an argument to the function, similar to echoWith.
 * @tags temporal
 * @name plyForEach
 * @synonyms plyforeach
 * @param {number} factor how many times to repeat
 * @param {function} func function to apply, given the pattern and the iteration index
 * @example
 * "<0 [2 4]>"
 * .plyForEach(4, (p,n) => p.add(n*2))
 * .scale("C:minor").note()
 */
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
  const result = pat
    .fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
    .squeezeJoin();
  if (__steps) {
    result._steps = Fraction(factor).mulmaybe(pat._steps);
  }
  return result;
});

/**
 * Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
 * @tags temporal
 * @name iter
 * @memberof Pattern
 * @returns Pattern
 * @example
 * note("0 1 2 3".scale('A minor')).iter(4)
 */

const _iter = function (times, pat, back = false) {
  times = Fraction(times);
  return slowcat(
    ...listRange(0, times.sub(1)).map((i) =>
      back ? pat.late(Fraction(i).div(times)) : pat.early(Fraction(i).div(times)),
    ),
  );
};

export const iter = register(
  'iter',
  function (times, pat) {
    return _iter(times, pat, false);
  },
  true,
  true,
);

/**
 * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
 * @tags temporal
 * @name iterBack
 * @synonyms iterback
 * @memberof Pattern
 * @returns Pattern
 * @example
 * note("0 1 2 3".scale('A minor')).iterBack(4)
 */
export const { iterBack, iterback } = register(
  ['iterBack', 'iterback'],
  function (times, pat) {
    return _iter(times, pat, true);
  },
  true,
  true,
);

/**
 * Repeats each cycle the given number of times.
 * @tags temporal
 * @name repeatCycles
 * @memberof Pattern
 * @returns Pattern
 * @example
 * note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
 */
export const { repeatCycles } = register(
  'repeatCycles',
  function (n, pat) {
    return new Pattern(function (state) {
      const cycle = state.span.begin.sam();
      const source_cycle = cycle.div(n).sam();
      const delta = cycle.sub(source_cycle);
      state = state.withSpan((span) => span.withTime((spant) => spant.sub(delta)));
      return pat.query(state).map((hap) => hap.withSpan((span) => span.withTime((spant) => spant.add(delta))));
    }).splitQueries();
  },
  true,
  true,
);

/**
 * Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
 * @tags temporal, functional
 * @name chunk
 * @synonyms slowChunk, slowchunk
 * @memberof Pattern
 * @returns Pattern
 * @example
 * "0 1 2 3".chunk(4, x=>x.add(7))
 * .scale("A:minor").note()
 */
const _chunk = function (n, func, pat, back = false, fast = false) {
  const binary = Array(n - 1).fill(false);
  binary.unshift(true);
  // Invert the 'back' because we want to shift the pattern forwards,
  // and so time backwards
  const binary_pat = _iter(n, sequence(...binary), !back);
  if (!fast) {
    pat = pat.repeatCycles(n);
  }
  return pat.when(binary_pat, func);
};

export const { chunk, slowchunk, slowChunk } = register(
  ['chunk', 'slowchunk', 'slowChunk'],
  function (n, func, pat) {
    return _chunk(n, func, pat, false, false);
  },
  true,
  true,
);

/**
 * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
 * @tags temporal
 * @name chunkBack
 * @synonyms chunkback
 * @memberof Pattern
 * @returns Pattern
 * @example
 * "0 1 2 3".chunkBack(4, x=>x.add(7))
 * .scale("A:minor").note()
 */
export const { chunkBack, chunkback } = register(
  ['chunkBack', 'chunkback'],
  function (n, func, pat) {
    return _chunk(n, func, pat, true);
  },
  true,
  true,
);

/**
 * Like `chunk`, but the cycles of the source pattern aren't repeated
 * for each set of chunks.
 * @tags temporal
 * @name fastChunk
 * @synonyms fastchunk
 * @memberof Pattern
 * @returns Pattern
 * @example
 * "<0 8> 1 2 3 4 5 6 7"
 * .scale("C2:major").note()
 * .fastChunk(4, x => x.color('red')).slow(2)
 */
export const { fastchunk, fastChunk } = register(
  ['fastchunk', 'fastChunk'],
  function (n, func, pat) {
    return _chunk(n, func, pat, false, true);
  },
  true,
  true,
);

/**
 * Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
 * @tags temporal
 * @name chunkInto
 * @synonyms chunkinto
 * @memberof Pattern
 * @example
 * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
 *   .bank("tr909")
 */
export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) {
  return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func);
});

/**
 * Like `chunkInto`, but moves backwards through the chunks.
 * @tags temporal
 * @name chunkBackInto
 * @synonyms chunkbackinto
 * @memberof Pattern
 * @example
 * sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
 *   .bank("tr909")
 */
export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) {
  return pat.into(
    fastcat(true, ...Array(n - 1).fill(false))
      ._iter(n)
      ._early(1),
    func,
  );
});

// TODO - redefine elsewhere in terms of mask
export const bypass = register(
  'bypass',
  function (on, pat) {
    on = Boolean(parseInt(on));
    return on ? silence : pat;
  },
  true,
  true,
);

/**
 * Loops the pattern inside an `offset` for `cycles`.
 * If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
 * @tags temporal
 * @name ribbon
 * @synonyms rib
 * @param {number} offset start point of loop in cycles
 * @param {number} cycles loop length in cycles
 * @example
 * note("<c d e f>").ribbon(1, 2)
 * @example
 * // Looping a portion of randomness
 * n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2)
 * @example
 * // rhythm generator
 * s("bd!16?").ribbon(29,.5)
 */
export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) =>
  pat.early(offset).restart(pure(1).slow(cycles)),
);

export const hsla = register('hsla', (h, s, l, a, pat) => {
  return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
});

export const hsl = register('hsl', (h, s, l, pat) => {
  return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`);
});

/**
 * Tags each Hap with an identifier. Good for filtering. The function populates Hap.context.tags (Array).
 * @name tag
 * @tags temporal
 * @param {string} tag anything unique
 * @example
 * s("saw!16").note("F1")
 *   .lpf(tri.range(40, 80).slow(4)).lpenv(5).lpq(4).lpd(0.15)
 *   .when(rand.late(0.1).gte(0.5), x => x.transpose("12").tag('altered'))
 *   .when(rand.late(0.2).gte(0.5), x => x.s("square").tag('altered'))
 *   .when("<0 1>", x => x.filter((hap) => hap.hasTag('altered')))
 */
Pattern.prototype.tag = function (tag) {
  return this.withContext((ctx) => ({ ...ctx, tags: (ctx.tags || []).concat([tag]) }));
};

/**
 * Filters haps using the given function
 * @name filter
 * @tags temporal, functional
 * @param {Function} test function to test Hap
 * @example
 * s("hh!7 oh").filter(hap => hap.value.s === 'hh')
 */
export const filter = register('filter', (test, pat) => pat.withHaps((haps) => haps.filter(test)));

/**
 * Filters haps by their begin time
 * @name filterWhen
 * @tags temporal, functional
 * @param {Function} test function to test Hap.whole.begin
 * @example
 * oneCycle: s("bd*4").filterWhen((t) => t < 1)
 */
export const filterWhen = register('filterWhen', (test, pat) => pat.filter((h) => test(h.whole.begin)));

/**
 * Use within to apply a function to only a part of a pattern.
 * @name within
 * @tags temporal, functional
 * @param {number} start start within cycle (0 - 1)
 * @param {number} end end within cycle (0 - 1). Must be > start
 * @param {Function} func function to be applied to the sub-pattern
 */
export const within = register('within', (a, b, fn, pat) =>
  stack(
    fn(pat.filterWhen((t) => t.cyclePos() >= a && t.cyclePos() <= b)),
    pat.filterWhen((t) => t.cyclePos() < a || t.cyclePos() > b),
  ),
);

//////////////////////////////////////////////////////////////////////
// Stepwise functions

Pattern.prototype.stepJoin = function () {
  const pp = this;
  const first_t = stepcat(..._retime(_slices(pp.queryArc(0, 1))))._steps;
  const q = function (state) {
    const shifted = pp.early(state.span.begin.sam());
    const haps = shifted.query(state.setSpan(new TimeSpan(Fraction(0), Fraction(1))));
    const pat = stepcat(..._retime(_slices(haps)));
    return pat.query(state);
  };
  return new Pattern(q, first_t);
};

Pattern.prototype.stepBind = function (func) {
  return this.fmap(func).stepJoin();
};

export function _retime(timedHaps) {
  const occupied_perc = timedHaps.filter((t, pat) => pat.hasSteps).reduce((a, b) => a.add(b), Fraction(0));
  const occupied_steps = removeUndefineds(timedHaps.map((t, pat) => pat._steps)).reduce(
    (a, b) => a.add(b),
    Fraction(0),
  );
  const total_steps = occupied_perc.eq(0) ? undefined : occupied_steps.div(occupied_perc);
  function adjust(dur, pat) {
    if (pat._steps === undefined) {
      return [dur.mulmaybe(total_steps), pat];
    }
    return [pat._steps, pat];
  }
  return timedHaps.map((x) => adjust(...x));
}

export function _slices(haps) {
  // slices evs = map (\s -> ((snd s - fst s), stack $ map value $ fit s evs))
  // $ pairs $ sort $ nubOrd $ 0:1:concatMap (\ev -> start (part ev):stop (part ev):[]) evs
  const breakpoints = flatten(haps.map((hap) => [hap.part.begin, hap.part.end]));
  const unique = uniqsortr([Fraction(0), Fraction(1), ...breakpoints]);
  const slicespans = pairs(unique);
  return slicespans.map((s) => [
    s[1].sub(s[0]),
    stack(..._fitslice(new TimeSpan(...s), haps).map((x) => x.value.withHap((h) => h.setContext(h.combineContext(x))))),
  ]);
}

export function _fitslice(span, haps) {
  return removeUndefineds(haps.map((hap) => _match(span, hap)));
}

export function _match(span, hap_p) {
  const subspan = span.intersection(hap_p.part);
  if (subspan == undefined) {
    return undefined;
  }
  return new Hap(hap_p.whole, subspan, hap_p.value, hap_p.context);
}

/**
 * *Experimental*
 *
 * Speeds a pattern up or down, to fit to the given number of steps per cycle.
 * @tags stepwise
 * @example
 * sound("bd sd cp").pace(4)
 * // The same as sound("{bd sd cp}%4") or sound("<bd sd cp>*4")
 */
export const pace = register('pace', function (targetSteps, pat) {
  if (pat._steps === undefined) {
    return pat;
  }
  if (pat._steps.eq(Fraction(0))) {
    // avoid divide by zero..
    return nothing;
  }
  return pat._fast(Fraction(targetSteps).div(pat._steps)).setSteps(targetSteps);
});

export function _polymeterListSteps(steps, ...args) {
  const seqs = args.map((a) => _sequenceCount(a));
  if (seqs.length == 0) {
    return silence;
  }
  if (steps == 0) {
    steps = seqs[0][1];
  }
  const pats = [];
  for (const seq of seqs) {
    if (seq[1] == 0) {
      continue;
    }
    if (steps == seq[1]) {
      pats.push(seq[0]);
    } else {
      pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1]))));
    }
  }
  return stack(...pats);
}

/**
 * *Experimental*
 *
 * Aligns the steps of the patterns, creating polymeters. The patterns are repeated until they all fit the cycle. For example, in the below the first pattern is repeated twice, and the second is repeated three times, to fit the lowest common multiple of six steps.
 * @tags stepwise
 * @synonyms pm
 * @example
 * // The same as note("{c eb g, c2 g2}%6")
 * polymeter("c eb g", "c2 g2").note()
 *
 */
export function polymeter(...args) {
  if (Array.isArray(args[0])) {
    // Support old behaviour
    return _polymeterListSteps(0, ...args);
  }

  // TODO currently ignoring arguments without steps...
  args = args.filter((arg) => arg.hasSteps);

  if (args.length == 0) {
    return silence;
  }
  const steps = lcm(...args.map((x) => x._steps));
  if (steps.eq(Fraction(0))) {
    return nothing;
  }

  const result = stack(...args.map((x) => x.pace(steps)));
  result._steps = steps;
  return result;
}

/** 'Concatenates' patterns like `fastcat`, but proportional to a number of steps per cycle.
 * The steps can either be inferred from the pattern, or provided as a [length, pattern] pair.
 * Has the alias `timecat`.
 * @name stepcat
 * @tags stepwise
 * @synonyms timeCat, timecat
 * @return {Pattern}
 * @example
 * stepcat([3,"e3"],[1, "g3"]).note()
 * // the same as "e3@3 g3".note()
 * @example
 * stepcat("bd sd cp","hh hh").sound()
 * // the same as "bd sd cp hh hh".sound()
 */
export function stepcat(...timepats) {
  if (timepats.length === 0) {
    return nothing;
  }
  const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]);
  timepats = timepats.map(findsteps);
  if (timepats.find((x) => x[0] === undefined)) {
    const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
    if (times.length === 0) {
      return fastcat(...timepats.map((x) => x[1]));
    }
    if (times.length === timepats.length) {
      return nothing;
    }
    const avg = times.reduce((a, b) => a.add(b), Fraction(0)).div(times.length);
    for (let timepat of timepats) {
      if (timepat[0] === undefined) {
        timepat[0] = avg;
      }
    }
  }
  if (timepats.length == 1) {
    const result = reify(timepats[0][1]);
    return result.withSteps((_) => timepats[0][0]);
  }

  const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
  let begin = Fraction(0);
  const pats = [];
  for (const [time, pat] of timepats) {
    if (Fraction(time).eq(0)) {
      continue;
    }
    const end = begin.add(time);
    pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
    begin = end;
  }
  const result = stack(...pats);
  result._steps = total;
  return result;
}

/**
 * *Experimental*
 *
 * Concatenates patterns stepwise, according to an inferred 'steps per cycle'.
 * Similar to `stepcat`, but if an argument is a list, the whole pattern will alternate between the elements in the list.
 *
 * @tags stepwise
 * @return {Pattern}
 * @example
 * stepalt(["bd cp", "mt"], "bd").sound()
 * // The same as "bd cp bd mt bd".sound()
 */
export function stepalt(...groups) {
  groups = groups.map((a) => (Array.isArray(a) ? a.map(reify) : [reify(a)]));

  const cycles = lcm(...groups.map((x) => Fraction(x.length)));

  let result = [];
  for (let cycle = 0; cycle < cycles; ++cycle) {
    result.push(...groups.map((x) => (x.length == 0 ? silence : x[cycle % x.length])));
  }
  result = result.filter((x) => x.hasSteps && x._steps > 0);
  const steps = result.reduce((a, b) => a.add(b._steps), Fraction(0));
  result = stepcat(...result);
  result._steps = steps;
  return result;
}

/**
 * *Experimental*
 *
 * Takes the given number of steps from a pattern (dropping the rest).
 * A positive number will take steps from the start of a pattern, and a negative number from the end.
 * @tags stepwise
 * @return {Pattern}
 * @example
 * "bd cp ht mt".take("2").sound()
 * // The same as "bd cp".sound()
 * @example
 * "bd cp ht mt".take("1 2 3").sound()
 * // The same as "bd bd cp bd cp ht".sound()
 * @example
 * "bd cp ht mt".take("-1 -2 -3").sound()
 * // The same as "mt ht mt cp ht mt".sound()
 */
export const take = stepRegister('take', function (i, pat) {
  if (!pat.hasSteps) {
    return nothing;
  }
  if (pat._steps.lte(0)) {
    return nothing;
  }
  i = Fraction(i);
  if (i.eq(0)) {
    return nothing;
  }
  const flip = i < 0;
  if (flip) {
    i = i.abs();
  }
  const frac = i.div(pat._steps);
  if (frac.lte(0)) {
    return nothing;
  }
  if (frac.gte(1)) {
    return pat;
  }
  if (flip) {
    return pat.zoom(Fraction(1).sub(frac), 1);
  }
  return pat.zoom(0, frac);
});

/**
 * *Experimental*
 *
 * Drops the given number of steps from a pattern.
 * A positive number will drop steps from the start of a pattern, and a negative number from the end.
 * @tags stepwise
 * @return {Pattern}
 * @example
 * "tha dhi thom nam".drop("1").sound().bank("mridangam")
 * @example
 * "tha dhi thom nam".drop("-1").sound().bank("mridangam")
 * @example
 * "tha dhi thom nam".drop("0 1 2 3").sound().bank("mridangam")
 * @example
 * "tha dhi thom nam".drop("0 -1 -2 -3").sound().bank("mridangam")
 */
export const drop = stepRegister('drop', function (i, pat) {
  if (!pat.hasSteps) {
    return nothing;
  }

  i = Fraction(i);
  if (i.lt(0)) {
    return pat.take(pat._steps.add(i));
  }
  return pat.take(Fraction(0).sub(pat._steps.sub(i)));
});

/**
 * *Experimental*
 *
 * `extend` is similar to `fast` in that it increases its density, but it also increases the step count
 * accordingly. So `stepcat("a b".extend(2), "c d")` would be the same as `"a b a b c d"`, whereas
 * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
 * @tags stepwise
 * @example
 * stepcat(
 *   sound("bd bd - cp").extend(2),
 *   sound("bd - sd -")
 * ).pace(8)
 */
export const extend = stepRegister('extend', function (factor, pat) {
  return pat.fast(factor).expand(factor);
});

/**
 * *Experimental*
 *
 * `replicate` is similar to `fast` in that it increases its density, but it also increases the step count
 * accordingly. So `stepcat("a b".replicate(2), "c d")` would be the same as `"a b a b c d"`, whereas
 * `stepcat("a b".fast(2), "c d")` would be the same as `"[a b] [a b] c d"`.
 *
 * TODO: find out how this function differs from extend
 * @tags stepwise
 * @example
 * stepcat(
 *   sound("bd bd - cp").replicate(2),
 *   sound("bd - sd -")
 * ).pace(8)
 */
export const replicate = stepRegister('replicate', function (factor, pat) {
  return pat.repeatCycles(factor).fast(factor).expand(factor);
});

/**
 * *Experimental*
 *
 * Expands the step size of the pattern by the given factor.
 * @tags stepwise
 * @example
 * sound("tha dhi thom nam").bank("mridangam").expand("3 2 1 1 2 3").pace(8)
 */
export const expand = stepRegister('expand', function (factor, pat) {
  return pat.withSteps((t) => t.mul(Fraction(factor)));
});

/**
 * *Experimental*
 *
 * Contracts the step size of the pattern by the given factor. See also `expand`.
 * @tags stepwise
 * @example
 * sound("tha dhi thom nam").bank("mridangam").contract("3 2 1 1 2 3").pace(8)
 */
export const contract = stepRegister('contract', function (factor, pat) {
  return pat.withSteps((t) => t.div(Fraction(factor)));
});

Pattern.prototype.shrinklist = function (amount) {
  const pat = this;

  if (!pat.hasSteps) {
    return [pat];
  }

  let [amountv, times] = Array.isArray(amount) ? amount : [amount, pat._steps];
  amountv = Fraction(amountv);

  if (times === 0 || amountv === 0) {
    return [pat];
  }

  const fromstart = amountv > 0;
  const ranges = [];
  if (fromstart) {
    const seg = Fraction(1).div(pat._steps).mul(amountv);
    for (let i = 0; i < times; ++i) {
      const s = seg.mul(i);
      if (s.gt(1)) {
        break;
      }
      ranges.push([s, 1]);
    }
  } else {
    amountv = Fraction(0).sub(amountv);
    const seg = Fraction(1).div(pat._steps).mul(amountv);
    for (let i = 0; i < times; ++i) {
      const e = Fraction(1).sub(seg.mul(i));
      if (e.lt(0)) {
        break;
      }
      ranges.push([Fraction(0), e]);
    }
  }
  return ranges.map((x) => pat.zoom(...x));
};

export const shrinklist = (amount, pat) => pat.shrinklist(amount);

/**
 * *Experimental*
 *
 * Progressively shrinks the pattern by 'n' steps until there's nothing left, or if a second value is given (using mininotation list syntax with `:`),
 * that number of times.
 * A positive number will progressively drop steps from the start of a pattern, and a negative number from the end.
 * @tags stepwise
 * @return {Pattern}
 * @example
 * "tha dhi thom nam".shrink("1").sound()
 * .bank("mridangam")
 * @example
 * "tha dhi thom nam".shrink("-1").sound()
 * .bank("mridangam")
 * @example
 * "tha dhi thom nam".shrink("1 -1").sound().bank("mridangam").pace(4)
 * @example
 * note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp")
   .shrink("1 -1").pace(8)

 */

export const shrink = register(
  'shrink',
  function (amount, pat) {
    if (!pat.hasSteps) {
      return nothing;
    }

    const list = pat.shrinklist(amount);
    const result = stepcat(...list);
    // TODO is this calculation needed?
    result._steps = list.reduce((a, b) => a.add(b._steps), Fraction(0));
    return result;
  },
  true,
  false,
  (x) => x.stepJoin(),
);

/**
 * *Experimental*
 *
 * Progressively grows the pattern by 'n' steps until the full pattern is played, or if a second value is given (using mininotation list syntax with `:`),
 * that number of times.
 * A positive number will progressively grow steps from the start of a pattern, and a negative number from the end.
 * @tags stepwise
 * @return {Pattern}
 * @example
 * "tha dhi thom nam".grow("1").sound()
 * .bank("mridangam")
 * @example
 * "tha dhi thom nam".grow("-1").sound()
 * .bank("mridangam")
 * @example
 * "tha dhi thom nam".grow("1 -1").sound().bank("mridangam").pace(4)
 * @example
 * note("0 1 2 3 4 5 6 7".scale("C:ritusen")).sound("folkharp")
   .grow("1 -1").pace(8)
 */

export const grow = register(
  'grow',
  function (amount, pat) {
    if (!pat.hasSteps) {
      return nothing;
    }

    const list = pat.shrinklist(Fraction(0).sub(amount));
    list.reverse();
    const result = stepcat(...list);
    // TODO is this calculation needed?
    result._steps = list.reduce((a, b) => a.add(b._steps), Fraction(0));
    return result;
  },
  true,
  false,
  (x) => x.stepJoin(),
);

/**
 * *Experimental*
 * 
 * Inserts a pattern into a list of patterns. On the first repetition it will be inserted at the end of the list, then moved backwards through the list 
 * on successive repetitions. The patterns are added together stepwise, with all repetitions taking place over a single cycle. Using `pace` to set the 
 * number of steps per cycle is therefore usually recommended.
 *
 * @tags stepwise
 * @return {Pattern}
 * @example
 * "[c g]".tour("e f", "e f g", "g f e c").note()
   .sound("folkharp")
   .pace(8)
 */
export const tour = function (pat, ...many) {
  return pat.tour(...many);
};

Pattern.prototype.tour = function (...many) {
  return stepcat(
    ...[].concat(
      ...many.map((x, i) => [...many.slice(0, many.length - i), this, ...many.slice(many.length - i)]),
      this,
      ...many,
    ),
  );
};

/**
 * *Experimental*
 * 
 * 'zips' together the steps of the provided patterns. This can create a long repetition, taking place over a single, dense cycle. 
 * Using `pace` to set the number of steps per cycle is therefore usually recommended.
 * 
 * @tags stepwise
 * @returns {Pattern}
 * @example
 * zip("e f", "e f g", "g [f e] a f4 c").note()
   .sound("folkharp")
   .pace(8)
 */
export const zip = function (...pats) {
  pats = pats.filter((pat) => pat.hasSteps);
  const zipped = slowcat(...pats.map((pat) => pat._slow(pat._steps)));
  const steps = lcm(...pats.map((x) => x._steps));
  return zipped._fast(steps).setSteps(steps);
};

/** Aliases for `stepcat` */
export const timecat = stepcat;
export const timeCat = stepcat;

// Deprecated stepwise aliases
export const s_cat = stepcat;
export const s_alt = stepalt;
export const s_polymeter = polymeter;
Pattern.prototype.s_polymeter = Pattern.prototype.polymeter;
export const s_taper = shrink;
Pattern.prototype.s_taper = Pattern.prototype.shrink;
export const s_taperlist = shrinklist;
Pattern.prototype.s_taperlist = Pattern.prototype.shrinklist;
export const s_add = take;
Pattern.prototype.s_add = Pattern.prototype.take;
export const s_sub = drop;
Pattern.prototype.s_sub = Pattern.prototype.drop;
export const s_expand = expand;
Pattern.prototype.s_expand = Pattern.prototype.expand;
export const s_extend = extend;
Pattern.prototype.s_extend = Pattern.prototype.extend;
export const s_contract = contract;
Pattern.prototype.s_contract = Pattern.prototype.contract;
export const s_tour = tour;
Pattern.prototype.s_tour = Pattern.prototype.tour;
export const s_zip = zip;
Pattern.prototype.s_zip = Pattern.prototype.zip;
export const steps = pace;
Pattern.prototype.steps = Pattern.prototype.pace;

//////////////////////////////////////////////////////////////////////
// Control-related functions, i.e. ones that manipulate patterns of
// objects

/**
 * Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'.
 * It turns a pattern of samples into a pattern of parts of samples.
 * @name chop
 * @tags samples
 * @memberof Pattern
 * @returns Pattern
 * @example
 * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
 * s("rhodes")
 *  .chop(4)
 *  .rev() // reverse order of chops
 *  .loopAt(2) // fit sample into 2 cycles
 *
 */
export const chop = register('chop', function (n, pat) {
  const slices = Array.from({ length: n }, (x, i) => i);
  const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
  const merge = function (a, b) {
    if ('begin' in a && 'end' in a && a.begin !== undefined && a.end !== undefined) {
      const d = a.end - a.begin;
      b = { begin: a.begin + b.begin * d, end: a.begin + b.end * d };
    }
    // return a;
    return Object.assign({}, a, b);
  };
  const func = function (o) {
    return sequence(slice_objects.map((slice_o) => merge(o, slice_o)));
  };
  return pat.squeezeBind(func).setSteps(__steps ? Fraction(n).mulmaybe(pat._steps) : undefined);
});

/**
 * Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
 * @name striate
 * @tags samples
 * @memberof Pattern
 * @returns Pattern
 * @example
 * s("numbers:0 numbers:1 numbers:2").striate(6).slow(3)
 */
export const striate = register('striate', function (n, pat) {
  const slices = Array.from({ length: n }, (x, i) => i);
  const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
  const slicePat = slowcat(...slice_objects);
  return pat
    .set(slicePat)
    ._fast(n)
    .setSteps(__steps ? Fraction(n).mulmaybe(pat._steps) : undefined);
});

/**
 * Makes the sample fit the given number of cycles by changing the speed.
 * @name loopAt
 * @tags samples, pitch
 * @memberof Pattern
 * @returns Pattern
 * @example
 * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
 * s("rhodes").loopAt(2)
 */
const _loopAt = function (factor, pat, cps = 0.5) {
  return pat
    .speed((1 / factor) * cps)
    .unit('c')
    .slow(factor);
};

export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
  const steps = pat._steps ? pat._steps.div(factor) : undefined;
  return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state), steps);
});

/**
 * Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
 * Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
 * @name slice
 * @tags samples
 * @memberof Pattern
 * @returns Pattern
 * @example
 * samples('github:tidalcycles/dirt-samples')
 * s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75)
 * @example
 * samples('github:tidalcycles/dirt-samples')
 * s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
 */

export const slice = register(
  'slice',
  function (npat, ipat, opat) {
    return npat
      .innerBind((n) =>
        ipat.outerBind((i) =>
          opat.outerBind((o) => {
            // If it's not an object, assume it's a string and make it a 's' control parameter
            o = o instanceof Object ? o : { s: o };
            const begin = Array.isArray(n) ? n[i] : i / n;
            const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
            return pure({ begin, end, _slices: n, ...o });
          }),
        ),
      )
      .setSteps(ipat._steps);
  },
  false, // turns off auto-patternification
);

/**
 *
 * make something happen on event time
 * uses browser timeout which is innacurate for audio tasks
 * @name onTriggerTime
 * @tags external_io
 * @memberof Pattern
 *  @returns Pattern
 * @example
 * s("bd!8").onTriggerTime((hap) => {console.log(hap)})
 */
Pattern.prototype.onTriggerTime = function (func) {
  return this.onTrigger((hap, currentTime, _cps, targetTime) => {
    const diff = targetTime - currentTime;
    window.setTimeout(() => {
      func(hap);
    }, diff * 1000);
  }, false);
};

/**
 * Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
 * @name splice
 * @tags samples, pitch
 * @example
 * samples('github:tidalcycles/dirt-samples')
 * s("breaks165")
 * .splice(8,  "0 1 [2 3 0]@2 3 0@2 7")
 */

export const splice = register(
  'splice',
  function (npat, ipat, opat) {
    const sliced = slice(npat, ipat, opat);
    return new Pattern((state) => {
      // TODO - default cps to 0.5
      const cps = state.controls._cps || 1;
      const haps = sliced.query(state);
      return haps.map((hap) =>
        hap.withValue((v) => ({
          ...{
            speed: (cps / v._slices / hap.whole.duration) * (v.speed || 1),
            unit: 'c',
          },
          ...v,
        })),
      );
    }).setSteps(ipat._steps);
  },
  false, // turns off auto-patternification
);

/**
 * Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
 * Similar to `loopAt`.
 * @name fit
 * @tags samples, pitch
 * @example
 * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
 * s("rhodes/2").fit()
 */
export const fit = register('fit', (pat) =>
  pat.withHaps((haps, state) =>
    haps.map((hap) =>
      hap.withValue((v) => {
        const slicedur = ('end' in v ? v.end : 1) - ('begin' in v ? v.begin : 0);
        return {
          ...v,
          speed: ((state.controls._cps || 1) / hap.whole.duration) * slicedur,
          unit: 'c',
        };
      }),
    ),
  ),
);

/**
 * Makes the sample fit the given number of cycles and cps value, by
 * changing the speed. deprecated: use loopAt or fit instead, together with setCps / setCpm.
 * @name loopAtCps
 * @tags samples, pitch
 * @memberof Pattern
 * @deprecated
 * @returns Pattern
 * @example
 * samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
 * s("rhodes").loopAtCps(4,1.5).cps(1.5)
 */
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
  return _loopAt(factor, pat, cps);
});

/** exposes a custom value at query time. basically allows mutating state without evaluation
 * @tags internals
 */
export const ref = (accessor) =>
  pure(1)
    .withValue(() => reify(accessor()))
    .innerJoin();

let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);

/**
 * Cross-fades between left and right from 0 to 1:
 * - 0 = (full left, no right)
 * - .5 = (both equal)
 * - 1 = (no left, full right)
 *
 * @name xfade
 * @tags amplitude
 * @example
 * xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
 */
export let xfade = (a, pos, b) => {
  pos = reify(pos);
  a = reify(a);
  b = reify(b);
  let gaina = pos.fmap((v) => ({ gain: fadeGain(v) }));
  let gainb = pos.fmap((v) => ({ gain: fadeGain(1 - v) }));
  return stack(a.mul(gaina), b.mul(gainb));
};

// the prototype version is actually flipped so left/right makes sense
Pattern.prototype.xfade = function (pos, b) {
  return xfade(this, pos, b);
};

/**
 * creates a structure pattern from divisions of a cycle
 * especially useful for creating rhythms
 * @name beat
 * @tags temporal
 * @example
 * s("bd").beat("0,7,10", 16)
 * @example
 * s("sd").beat("4,12", 16)
 */
const __beat = (join) => (t, div, pat) => {
  t = Fraction(t).mod(div);
  div = Fraction(div);
  const b = t.div(div);
  const e = t.add(1).div(div);
  return join(pat.fmap((x) => pure(x)._compress(b, e)));
};

export const { beat } = register(
  ['beat'],
  __beat((x) => x.innerJoin()),
);

export const _morph = (from, to, by) => {
  by = Fraction(by);
  const dur = Fraction(1).div(from.length);
  const positions = (list) => {
    const result = [];
    for (const [pos, value] of list.entries()) {
      if (value) {
        result.push([Fraction(pos).div(list.length), value]);
      }
    }
    return result;
  };
  const arcs = zipWith(
    ([posa, valuea], [posb, valueb]) => {
      const b = by.mul(posb - posa).add(posa);
      const e = b.add(dur);
      return new TimeSpan(b, e);
    },
    positions(from),
    positions(to),
  );
  function query(state) {
    const cycle = state.span.begin.sam();
    const cycleArc = state.span.cycleArc();
    const result = [];
    for (const whole of arcs) {
      const part = whole.intersection(cycleArc);
      if (part !== undefined) {
        result.push(
          new Hap(
            whole.withTime((x) => x.add(cycle)),
            part.withTime((x) => x.add(cycle)),
            true,
          ),
        );
      }
    }
    return result;
  }
  return new Pattern(query).splitQueries();
};

/**
 * Takes two binary rhythms represented as lists of 1s and 0s, and a number
 * between 0 and 1 that morphs between them. The two lists should contain the same
 * number of true values.
 * @example
 * sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
 *                          [1,1,0,1,0,1,0], // wonky rhythm
 *                          0.25 // creates a slightly wonky rhythm
 *                         )
 *                   )
 * @example
 * sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
 *                          "1:1:0:1:0:1:0", // wonky rhythm
 *                          sine.slow(8) // slowly morph between the rhythms
 *                         )
 *                   )
 * @tags temporal
 */
export const morph = (frompat, topat, bypat) => {
  frompat = reify(frompat);
  topat = reify(topat);
  bypat = reify(bypat);
  return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
};

const _distortWithAlg = function (name) {
  const func = function (args, pat) {
    const argsPat = reify(args).fmap((v) => (Array.isArray(v) ? [...v, name] : [v, 1, name]));
    if (!pat) {
      return pure({}).distort(argsPat);
    }
    return pat.distort(argsPat);
  };
  Pattern.prototype[name] = function (args) {
    return func(args, this);
  };
  return func;
};

/**
 * Soft-clipping distortion
 *
 * @name soft
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const soft = _distortWithAlg('soft');

/**
 * Hard-clipping distortion
 *
 * @name hard
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const hard = _distortWithAlg('hard');

/**
 * Cubic polynomial distortion
 *
 * @name cubic
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const cubic = _distortWithAlg('cubic');

/**
 * Diode-emulating distortion
 *
 * @name diode
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const diode = _distortWithAlg('diode');

/**
 * Asymmetrical diode distortion
 *
 * @name asym
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const asym = _distortWithAlg('asym');

/**
 * Wavefolding distortion
 *
 * @name fold
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const fold = _distortWithAlg('fold');

/**
 * Wavefolding distortion composed with sinusoid
 *
 * @name sinefold
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const sinefold = _distortWithAlg('sinefold');

/**
 * Distortion via Chebyshev polynomials
 *
 * @name chebyshev
 * @tags distortion, superdough
 * @param {number | Pattern} distortion amount of distortion to apply
 * @param {number | Pattern} volume linear postgain of the distortion
 *
 */
export const chebyshev = _distortWithAlg('chebyshev');

/**
 * Turns a list of patterns into a single pattern which outputs list-values
 *
 * @name parray
 * @tags combiners
 * @returns Pattern
 */
export const parray = (pats) => {
  const pack = (...xs) => xs;
  let acc = pure(curry(pack, null, pats.length));
  for (const p of pats) acc = acc.appBoth(reify(p));
  return acc;
};

const _ensureListPattern = (list) => {
  if (Array.isArray(list)) {
    return parray(list);
  }
  return reify(list);
};

/**
 * Scale the magnitude of the harmonics of one of the core synths ('sine', 'tri', 'saw', ..)
 *
 * Can also be used to create a new synth via `s('user').partials(...)`
 *
 * @name partials
 * @tags superdough
 * @param {number[] | Pattern} magnitudes List of [0, 1] magnitudes for partials. 0th entry is the fundamental harmonic (i.e. DC offset is skipped)
 * @example
 * s("user").seg(16).n(irand(8)).scale("A:major")
 *   .partials([1, 0, 1, 0, 0, 1])
 * @example
 * s("saw").seg(8).n(irand(12)).scale("G#:minor")
 *   .partials(binaryL(irand(256).add("1")))
 */
Pattern.prototype.partials = function (list) {
  return this.withValue((v) => (l) => ({ ...v, partials: l })).appLeft(_ensureListPattern(list));
};

// Also create a top-level function
export const partials = (list) => {
  return _ensureListPattern(list).as('partials');
};

/**
 * Rotates the harmonics of one of the core synths ('sine', 'tri', 'saw', 'user', ..) by a list of phases
 *
 * @name phases
 * @tags superdough
 * @param {number[] | Pattern} phases List of [0, 1) phases for partials. 0th entry is the fundamental phase (i.e. DC offset is skipped)
 * @example
 * // Phase cancellation
 * s("saw").seg(8).n(irand(12)).scale("G#1:minor")
 *   .partials(partials([1, 1, 1]))
 *   .superimpose(x => x.phases([0.5, 0.5, 0.5]))
 */
Pattern.prototype.phases = function (list) {
  return this.withValue((v) => (l) => ({ ...v, phases: l })).appLeft(_ensureListPattern(list));
};

// Also create a top-level function
export const phases = (list) => {
  return _ensureListPattern(list).as('phases');
};

/**
 * Establishes an FX chain. Can be called by chaining .FX(fx1).FX(fx2)..
 * calls and/or in a single .FX(fx1, fx2, ..) call. The fx1, .. are _patterns_ which
 * establish the controls of the given effect. See examples.
 * @name FX
 * @tags superdough
 * @memberof Pattern
 * @returns Pattern
 * @example
 * $: s("[sbd <hh [bd | lt | oh]>]*4").dec(.4)
 *   .FX(
 *     phaser(0.5).gain(2),
 *     bpf(800),
 *     distort(1.3),
 *     room(0.2),
 *     delay(0.5).gain(1.25),
 *     distort(0.3),
 *   ).fxr(1.7) // sets release time of effects (like delay)
 * @example
 * $: s("saw").fm(0.5)
 *   .delay(0.3) // outer effects are applied *last*
 *   .FX(coarse(4)) // first coarse
 *   .FX(lpf(500).lpe(4).lpa(1).lpd(2)) // then lpf
 *   .FX(distort(1)) // then distort
 */
Pattern.prototype.FX = function (...effects) {
  effects = effects.map(reify);
  return this.withValue((v) => (vEff) => {
    const currFX = v.FX ?? [];
    return { ...v, FX: currFX.concat(vEff) };
  }).appLeft(parray(effects));
};

const _asArrayPattern = (pats) => {
  const pack = (...xs) => xs;
  let acc = pure(curry(pack, null, pats.length));
  for (const p of pats) acc = acc.appLeft(p);
  return acc;
};

/**
 * Produces a [Kabelsalat](https://kabel.salat.dev/) modular sound engine.
 * This can be used as either an effect (by including `audioin()` at the beginning
 * of your kabel expression) or as a sound source (via any expression which doesn't
 * start with `audioin()`).
 *
 * Some helpers you have available to you:
 *   * Strudel mini notation works fine in K(..) via "" or ``
 *   * More complex Strudel expressions (like "0 1 2".fast(4) or irand(24)) can be
 *     written by wrapping them in `S(..)` inside your Kabel code
 *   * We expose Strudel's note frequency under `sFreq` and Strudel's gate
 *     information under `sGate`
 *   * You can use more complex multi-line expressions (like `let x = a; let y = b; x.lpf(y);`)
 *     by wrapping them inside a function in K (see example).
 *
 * @name K
 * @tags generators, superdough
 * @param {KabelsalatExpression | Function} expr Kabelsalat graph definition
 * @memberof Pattern
 * @returns Pattern
 *
 * @example
 * note("A c e".fast(4)).transpose("<0 2 4 6 8>")
 *   .scale("F:minor").transpose("12")
 *   .s("saw")
 *   .K(
 *     // audioin().mul(sGate.adsr(0.001, 0.3, 0, 0.2)) // as effect
 *     saw(saw(sFreq / "2!3 16").mul(8).add(sFreq).lag("0!3 0.1")).mul(0.3) // as source
 *     .mul(sGate.adsr(0, 0.15, 0.5, "0.1!3 1"))
 *     .lpf(sGate.adsr(0, 0.2, 0.3, 0.2).mul(1).add(0))
 *     .add(x => x.delay(S("0.3 0.2".fast(2))).mul(0.7))
 *     .add(x => x.delay("0.03 [0.08 0.01] 0.01 0.013").mul(0.77)).mul(0.7)
 *     .add(x => x.delay(.13).mul(0.7))
 *     .out()
 *   )
 *
 * @example
 * n("<0 1 <2 3 2 4>>*16")
 *   .scale("G#2:minor").sometimes(x => x.transpose("12 | 24"))
 *   .K(() => {
 *     const att = S(rand.range(0, 0.05))
 *     const dec = S(rand.range(0.05, 0.2))
 *     let f = n(sFreq);
 *     const mod = sine(f).mul("0.1 | 0.2 | 0.3")
 *       .add("[[1.5 1] | 1 | 2 | 4 | [6 4@3]]*2")
 *     saw(f.mul(mod))
 *     .mul(sGate.ad(att, dec))
 *     .add(x => x.delay(0.4).mul(0.3))
 *     .out()
 *   }).fxr(1).room(0.3)
 */
/**
 * Creates a worklet effect. Typically derived by writing K(...) in the REPL which will parse
 * Kabelsalat code.
 *
 * @name worklet
 * @param {string} src Source code of the worklet update function
 * @param {...number | ...Pattern} inputs Worklet inputs
 * @memberof Pattern
 * @returns Pattern
 * @noAutocomplete
 */
Pattern.prototype.worklet = function (src, ...inputs) {
  inputs = inputs.map(reify);
  return this.outerBind((v) => {
    return _asArrayPattern(inputs).withValue((vInput) => {
      const currInputs = v.workletInputs ?? [];
      return { ...v, workletSrc: src, workletInputs: currInputs.concat(vInput) };
    });
  });
};

export const worklet = (...args) => pure({}).worklet(...args);
