/*
pattern.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/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 from './fraction.mjs';
import Hap from './hap.mjs';
import State from './state.mjs';
import { unionWithObj } from './value.mjs';

import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
import drawLine from './drawLine.mjs';

/** @class Class representing a pattern. */
export class Pattern {
  _Pattern = true; // this property is used to detect if a pattern that fails instanceof Pattern is an instance of another 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 {@link State} to an array of {@link Hap}.
   */
  constructor(query) {
    this.query = query;
  }

  /**
   * query haps insude the tiven time span
   *
   * @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);
   */
  queryArc(begin, end) {
    return this.query(new State(new TimeSpan(begin, end)));
  }

  /**
   * 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.
   * @returns Pattern
   */
  _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.
   * @param {Function} func the function to apply
   * @returns Pattern
   */
  withQuerySpan(func) {
    return new Pattern((state) => this.query(state.withSpan(func)));
  }

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

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

  /**
   * As with {@link Pattern#withHapSpan}, but the function is applied to both the
   * begin and end time of the hap timespans.
   * @param {Function} func the function to apply
   * @returns Pattern
   */
  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.
   * @param {Function} func
   * @returns Pattern
   */
  _withHaps(func) {
    return new Pattern((state) => func(this.query(state)));
  }

  /**
   * As with {@link Pattern#_withHaps}, but applies the function to every hap, rather than every list of haps.
   * @param {Function} func
   * @returns Pattern
   */
  _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.
   * @param {*} context
   * @returns Pattern
   */
  _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.
   * @param {Function} func
   * @returns Pattern
   */
  _withContext(func) {
    return this._withHap((hap) => hap.setContext(func(hap.context)));
  }

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

  /**
   * Returns a new pattern with the given location information added to the
   * context of every hap.
   * @param {Number} start
   * @param {Number} end
   * @returns Pattern
   */
  withLocation(start, end) {
    const location = {
      start: { line: start[0], column: start[1], offset: start[2] },
      end: { line: end[0], column: end[1], offset: end[2] },
    };
    return this._withContext((context) => {
      const locations = (context.locations || []).concat([location]);
      return { ...context, locations };
    });
  }

  withMiniLocation(start, end) {
    const offset = {
      start: { line: start[0], column: start[1], offset: start[2] },
      end: { line: end[0], column: end[1], offset: end[2] },
    };
    return this._withContext((context) => {
      let locations = context.locations || [];
      locations = locations.map(({ start, end }) => {
        const colOffset = start.line === 1 ? offset.start.column : 0;
        return {
          start: {
            ...start,
            line: start.line - 1 + (offset.start.line - 1) + 1,
            column: start.column - 1 + colOffset,
          },
          end: {
            ...end,
            line: end.line - 1 + (offset.start.line - 1) + 1,
            column: end.column - 1 + colOffset,
          },
        };
      });
      return { ...context, locations };
    });
  }

  /**
   * Returns a new pattern, with the function applied to the value of
   * each hap. It has the alias {@link Pattern#fmap}.
   * @param {Function} func
   * @returns Pattern
   */
  withValue(func) {
    return new Pattern((state) => this.query(state).map((hap) => hap.withValue(func)));
  }

  /**
   * see {@link Pattern#withValue}
   */
  fmap(func) {
    return this.withValue(func);
  }

  /**
   * Returns a new Pattern, which only returns haps that meet the given test.
   * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
   * @returns Pattern
   */
  _filterHaps(hap_test) {
    return new Pattern((state) => this.query(state).filter(hap_test));
  }

  /**
   * As with {@link Pattern#_filterHaps}, but the function is applied to values
   * inside haps.
   * @param {Function} value_test
   * @returns Pattern
   */
  _filterValues(value_test) {
    return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
  }

  /**
   * Returns a new pattern, with haps containing undefined values removed from
   * query results.
   * @returns Pattern
   */
  _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.
   * @returns Pattern
   */
  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.
   * @returns Pattern
   */
  discreteOnly() {
    // removes continuous haps that don't have a 'whole' timespan
    return this._filterHaps((hap) => hap.whole);
  }

  _appWhole(whole_func, pat_val) {
    // 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.
    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.
   * @param {Pattern} pat_val
   * @returns Pattern
   */
  appBoth(pat_val) {
    // 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);
    };
    return this._appWhole(whole_func, pat_val);
  }

  /**
   * As with {@link Pattern#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).
   * @param {Pattern} pat_val
   * @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;
    };
    return new Pattern(query);
  }

  /**
   * As with {@link Pattern#appLeft}, but `whole` timespans are instead taken from the
   * pattern of values, i.e. structure is preserved from the right hand/outer
   * pattern.
   * @param {Pattern} pat_val
   * @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;
    };
    return new Pattern(query);
  }

  /**
   * Queries the pattern for the first cycle, returning Haps. Mainly of use when
   * debugging a pattern.
   * @param {Boolean} with_context - set to true, otherwise the context field
   * will be stripped from the resulting haps.
   * @returns [Hap]
   */
  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.
   */
  get _firstCycleValues() {
    return this.firstCycle().map((hap) => hap.value);
  }

  /**
   * More human-readable version of the {@link Pattern#_firstCycleValues} accessor.
   */
  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.
   * @returns Pattern
   */
  _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))),
      ),
    );
  }

  _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();
  }
  _opTrig(other, func) {
    const otherPat = reify(other);
    return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._trigJoin();
  }
  _opTrigzero(other, func) {
    const otherPat = reify(other);
    return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._TrigzeroJoin();
  }

  _asNumber(dropfails = false, softfail = false) {
    return this._withHap((hap) => {
      const asNumber = Number(hap.value);
      if (!isNaN(asNumber)) {
        return hap.withValue(() => asNumber);
      }
      const specialValue = {
        e: Math.E,
        pi: Math.PI,
      }[hap.value];
      if (typeof specialValue !== 'undefined') {
        return hap.withValue(() => specialValue);
      }
      if (isNote(hap.value)) {
        // set context type to midi to let the player know its meant as midi number and not as frequency
        return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
      }
      if (dropfails) {
        // return 'nothing'
        return undefined;
      }
      if (softfail) {
        // return original hap
        return hap;
      }
      throw new Error('cannot parse as number: "' + hap.value + '"');
      return hap;
    });
  }

  /**
   * Assumes a numerical pattern. Returns a new pattern with all values rounded
   * to the nearest integer.
   * @returns Pattern
   */
  round() {
    return this._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`.
   * @returns Pattern
   */
  floor() {
    return this._asNumber().fmap((v) => Math.floor(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`.
   * @returns Pattern
   */
  ceil() {
    return this._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
   * @returns Pattern
   */
  _toBipolar() {
    return this.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
   * @returns Pattern
   */
  _fromBipolar() {
    return this.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
   * @returns Pattern
   * @example
   * s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4)).out()
   */
  range(min, max) {
    return this.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.
   * @param {Number} min
   * @param {Number} max
   * @returns Pattern
   */
  rangex(min, max) {
    return this.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.
   * @param {Number} min
   * @param {Number} max
   * @returns Pattern
   */
  range2(min, max) {
    return this._fromBipolar().range(min, max);
  }

  _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);
  }

  outerJoin() {
    // Flattens a pattern of patterns into a pattern, where wholes are
    // taken from inner 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
  _trigJoin(cycleZero = 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
                // trig = align the inner pattern cycle start to outer pattern haps
                // Trigzero = align the inner pattern cycle zero to outer pattern haps
                .late(cycleZero ? 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()
      );
    });
  }

  _TrigzeroJoin() {
    return this._trigJoin(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();
  }

  _apply(func) {
    return func(this);
  }

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

  _patternify(func) {
    const pat = this;
    const patterned = function (...args) {
      // the problem here: args could a pattern that has been turned into an object to add location
      // to avoid object checking for every pattern method, we can remove it here...
      // in the future, patternified args should be marked as well + some better object handling
      args = args.map((arg) => (isPattern(arg) ? arg.fmap((value) => value.value || value) : arg));
      const pat_arg = sequence(...args);
      // arg.locations has to go somewhere..
      return pat_arg.fmap((arg) => func.call(pat, arg)).innerJoin();
    };
    return patterned;
  }

  _fastGap(factor) {
    // Maybe it's better without this fallback..
    // if (factor < 1) {
    //     // there is no gap.. so maybe revert to _fast?
    //     return this._fast(factor)
    // }
    const qf = function (span) {
      const cycle = span.begin.sam();
      const begin = cycle.add(span.begin.sub(cycle).mul(factor).min(1));
      const end = cycle.add(span.end.sub(cycle).mul(factor).min(1));
      return new TimeSpan(begin, end);
    };
    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 this.withQuerySpan(qf)._withHap(ef)._splitQueries();
  }

  // Compress each cycle into the given timespan, leaving a gap
  _compress(b, e) {
    if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
      return silence;
    }
    return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
  }

  _compressSpan(span) {
    return this._compress(span.begin, span.end);
  }

  // Similar to compress, but doesn't leave gaps, and the 'focus' can be
  // bigger than a cycle
  _focus(b, e) {
    return this._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
  }
    
  _focusSpan(span) {
    return this._focus(span.begin, span.end);
  }

  /**
   * Speed up a pattern by the given factor. Used by "*" in mini notation.
   *
   * @name fast
   * @memberof Pattern
   * @param {number | Pattern} factor speed up factor
   * @returns Pattern
   * @example
   * s("<bd sd> hh").fast(2).out() // s("[<bd sd> hh]*2").out()
   */
  _fast(factor) {
    const fastQuery = this.withQueryTime((t) => t.mul(factor));
    return fastQuery.withHapTime((t) => t.div(factor));
  }

  /**
   * Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation.
   *
   * @name slow
   * @memberof Pattern
   * @param {number | Pattern} factor slow down factor
   * @returns Pattern
   * @example
   * s("<bd sd> hh").slow(2).out() // s("[<bd sd> hh]/2").out()
   */
  _slow(factor) {
    return this._fast(Fraction(1).div(factor));
  }

  _inside(factor, f) {
    return f(this._slow(factor))._fast(factor);
  }

  _outside(factor, f) {
    return f(this._fast(factor))._slow(factor);
  }

  _ply(factor) {
    return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
  }

  _chop(n) {
    const slices = Array.from({ length: n }, (x, i) => i);
    const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
    const func = function (o) {
      return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
    };
    return this._squeezeBind(func);
  }

  _striate(n) {
    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 this.set(slicePat)._fast(n);
  }

  // cpm = cycles per minute
  _cpm(cpm) {
    return this._fast(cpm / 60);
  }

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

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

  _zoom(s, e) {
    e = Fraction(e);
    s = Fraction(s);
    const d = e.sub(s);
    return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
      .withHapSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
      ._splitQueries();
  }

  _zoomArc(a) {
    return this.zoom(a.begin, a.end);
  }

  _linger(t) {
    if (t == 0) {
      return silence;
    } else if (t < 0) {
      return this._zoom(t.add(1), 1)._slow(t);
    }
    return this._zoom(0, t)._slow(t);
  }

  /**
   * Applies the given structure to the pattern:
   *
   * @name struct
   * @memberof Pattern
   * @returns Pattern
   * @example
   * "c3,eb3,g3"
   *   .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
   *   .slow(4).note().out()
   */
  // struct(...binary_pats) {
  //   // Re structure the pattern according to a binary pattern (false values are dropped)
  //   const binary_pat = sequence(binary_pats);
  //   return binary_pat
  //     .fmap((b) => (val) => b ? val : undefined)
  //     .appLeft(this)
  //     ._removeUndefineds();
  // }

  // mask(...binary_pats) {
  //   // Only let through parts of pattern corresponding to true values in the given binary pattern
  //   const binary_pat = sequence(binary_pats);
  //   return binary_pat
  //     .fmap((b) => (val) => b ? val : undefined)
  //     .appRight(this)
  //     ._removeUndefineds();
  // }

  _color(color) {
    return this._withContext((context) => ({ ...context, color }));
  }

  log() {
    return this._withHap((e) => {
      return e.setContext({ ...e.context, logs: (e.context?.logs || []).concat([e.show()]) });
    });
  }

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

  _segment(rate) {
    return this.struct(pure(true)._fast(rate));
  }

  invert() {
    // Swap true/false in a binary pattern
    return this.fmap((x) => !x);
  }

  inv() {
    // alias for invert()
    return this.invert();
  }

  /**
   * Applies the given function whenever the given pattern is in a true state.
   * @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))
   */
  when(binary_pat, func) {
    //binary_pat = sequence(binary_pat)
    const true_pat = binary_pat._filterValues(id);
    const false_pat = binary_pat._filterValues((val) => !val);
    const with_pat = true_pat.fmap((_) => (y) => y).appRight(func(this));
    const without_pat = false_pat.fmap((_) => (y) => y).appRight(this);
    return stack(with_pat, without_pat);
  }

  /**
   * Superimposes the function result on top of the original pattern, delayed by the given time.
   * @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))
   */
  off(time_pat, func) {
    return stack(this, func(this.late(time_pat)));
  }

  /**
   * Applies the given function every n cycles.
   * @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()).out()
   */
  every(n, func) {
    const pat = this;
    const pats = Array(n - 1).fill(pat);
    // pats.unshift(func(pat));
    pats.push(func(pat));
    return slowcatPrime(...pats);
  }
  /**
   * Applies the given function every n cycles, starting from the first cycle.
   * @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()).out()
   */
  every(n, func) {
    const pat = this;
    const pats = Array(n - 1).fill(pat);
    pats.unshift(func(pat));
    return slowcatPrime(...pats);
  }

  /**
   * Applies the given function every n cycles, starting from the last cycle.
   * @name each
   * @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()).out()
   */
  each(n, func) {
    const pat = this;
    const pats = Array(n - 1).fill(pat);
    pats.push(func(pat));
    return slowcatPrime(...pats);
  }

  /**
   * 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.
   * @returns Pattern
   */
  brak() {
    return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
  }

  /**
   * Reverse all haps in a pattern
   *
   * @name rev
   * @memberof Pattern
   * @returns Pattern
   * @example
   * "c3 d3 e3 g3".rev()
   */
  rev() {
    const pat = this;
    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();
  }

  palindrome() {
    return this.every(2, rev);
  }

  juxBy(by, func) {
    by /= 2;
    const elem_or = function (dict, key, dflt) {
      if (key in dict) {
        return dict[key];
      }
      return dflt;
    };
    const left = this.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) - by }));
    const right = this.withValue((val) => Object.assign({}, val, { pan: elem_or(val, 'pan', 0.5) + by }));

    return stack(left, func(right));
  }

  _jux(func) {
    return this.juxBy(1, func);
  }

  /**
   * Stacks the given pattern(s) to the current pattern.
   * @name stack
   * @memberof Pattern
   * @example
   * s("hh*2").stack(
   *   n("c2(3,8)")
   * ).out()
   */
  stack(...pats) {
    return stack(this, ...pats);
  }

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

  /**
   * Appends the given pattern(s) to the current pattern. Synonyms: .sequence .fastcat
   * @name seq
   * @memberof Pattern
   * @example
   * s("hh*2").seq(
   *   n("c2(3,8)")
   * ).out()
   */
  seq(...pats) {
    return sequence(this, ...pats);
  }

  /**
   * Appends the given pattern(s) to the next cycle. Synonym: .slowcat
   * @name cat
   * @memberof Pattern
   * @example
   * s("hh*2").cat(
   *   n("c2(3,8)")
   * ).out()
   */
  cat(...pats) {
    return cat(this, ...pats);
  }

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

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

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

  stutWith(times, time, func) {
    return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
  }

  stut(times, feedback, time) {
    return this.stutWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
  }

  /**
   * Superimpose and offset multiple times, applying the given function each time.
   * @name echoWith
   * @memberof Pattern
   * @returns Pattern
   * @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().legato(.2).out()
   */
  _echoWith(times, time, func) {
    return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
  }

  /**
   * Superimpose and offset multiple times, gradually decreasing the velocity
   * @name echo
   * @memberof Pattern
   * @returns Pattern
   * @example
   * s("bd sd").echo(3, 1/6, .8).out()
   */
  _echo(times, time, feedback) {
    return this._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
  }

  /**
   * 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.
   * @name iter
   * @memberof Pattern
   * @returns Pattern
   * @example
   * note("0 1 2 3".scale('A minor')).iter(4).out()
   */
  iter(times, back = false) {
    return slowcat(...listRange(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
  }

  /**
   * Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
   * @name iterBack
   * @memberof Pattern
   * @returns Pattern
   * @example
   * note("0 1 2 3".scale('A minor')).iterBack(4).out()
   */
  iterBack(times) {
    return this.iter(times, 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).
   * @name chunk
   * @memberof Pattern
   * @returns Pattern
   * @example
   * "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note().out()
   */
  _chunk(n, func, back = false) {
    const binary = Array(n - 1).fill(false);
    binary.unshift(true);
    const binary_pat = sequence(...binary).iter(n, back);
    return this.when(binary_pat, func);
  }

  /**
   * Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
   * @name chunkBack
   * @memberof Pattern
   * @returns Pattern
   * @example
   * "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note().out()
   */
  _chunkBack(n, func) {
    return this._chunk(n, func, true);
  }

  _bypass(on) {
    on = Boolean(parseInt(on));
    return on ? silence : this;
  }

  hush() {
    return silence;
  }

  // sets absolute duration of haps
  _duration(value) {
    return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
  }

  // sets hap relative duration of haps
  _legato(value) {
    return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
  }

  _velocity(velocity) {
    return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
  }

  // move this to controls? (speed and unit are controls)
  _loopAt(factor, cps = 1) {
    return this.speed((1 / factor) * cps)
      .unit('c')
      .slow(factor);
  }
  onTrigger(onTrigger) {
    return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
  }
  log(func = id) {
    return this._withHap((hap) =>
      hap.setContext({
        ...hap.context,
        onTrigger: (...args) => {
          if (hap.context.onTrigger) {
            hap.context.onTrigger(...args);
          }
          console.log(func(...args));
        },
      }),
    );
  }
  logValues(func = id) {
    return this.log((_, hap) => func(hap.value));
  }
}

// TODO - adopt value.mjs fully..
function _composeOp(a, b, func) {
  function _nonFunctionObject(x) {
    return x instanceof Object && !(x instanceof Function);
  }
  if (_nonFunctionObject(a) || _nonFunctionObject(b)) {
    if (!_nonFunctionObject(a)) {
      a = { value: a };
    }
    if (!_nonFunctionObject(b)) {
      b = { value: b };
    }
    return unionWithObj(a, b, func);
  }
  return func(a, b);
}

// Make composers
(function () {
  const num = (pat) => pat._asNumber();
  const numOrString = (pat) => pat._asNumber(false, true);

  // pattern composers
  const composers = {
    set: [(a, b) => b],
    keep: [(a, b) => a],
    keepif: [(a, b) => (b ? a : undefined)],

    // numerical functions
    add: [(a, b) => a + b, numOrString], // support string concatenation
    sub: [(a, b) => a - b, num],
    mul: [(a, b) => a * b, num],
    div: [(a, b) => a / b, num],
    mod: [mod, num],
    pow: [Math.pow, num],
    _and: [(a, b) => a & b, num],
    _or: [(a, b) => a | b, num],
    _xor: [(a, b) => a ^ b, num],
    _lshift: [(a, b) => a << b, num],
    _rshift: [(a, b) => a >> b, num],

    // 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)],
  };

  // generate methods to do what and how
  for (const [what, [op, preprocess]] of Object.entries(composers)) {
    for (const how of ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Trig', 'Trigzero']) {
      Pattern.prototype[what + how] = function (...other) {
        var pat = this;
        other = sequence(other);
        if (preprocess) {
          pat = preprocess(pat);
          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 = pat['_op' + how](other, (a) => (b) => op(a, b));
          result = result._removeUndefineds();
        }
        else {
          result = pat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
        }
        return result;
      };
      if (how === 'Squeeze') {
        // support 'squeezeIn' longhand
        Pattern.prototype[what + 'SqueezeIn'] = Pattern.prototype[what + how];
      }
      if (how === 'In') {
        // default how to 'in', e.g. add == addIn
        Pattern.prototype[what] = Pattern.prototype[what + how];
      } else {
        // default what to 'set', e.g. squeeze = setSqueeze
        if (what === 'set') {
          Pattern.prototype[how.toLowerCase()] = Pattern.prototype[what + how];
        }
      }
    }
  }

  // binary composers
  Pattern.prototype.struct = Pattern.prototype.keepifOut;
  Pattern.prototype.structAll = Pattern.prototype.keepOut;
  Pattern.prototype.mask = Pattern.prototype.keepifIn;
  Pattern.prototype.maskAll = Pattern.prototype.keepIn;
  Pattern.prototype.reset = Pattern.prototype.keepifTrig;
  Pattern.prototype.resetAll = Pattern.prototype.keepTrig;
  Pattern.prototype.restart = Pattern.prototype.keepifTrigzero;
  Pattern.prototype.restartAll = Pattern.prototype.keepTrigzero;
})();

// methods of Pattern that get callable factories
Pattern.prototype.patternified = [
  'apply',
  'chop',
  'color',
  'cpm',
  'duration',
  'early',
  'fast',
  'jux',
  'late',
  'legato',
  'linger',
  'ply',
  'segment',
  'striate',
  'slow',
  'velocity',
];

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

// methods that create patterns, which are added to patternified Pattern methods
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

// Nothing
export const silence = new Pattern((_) => []);

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

export function isPattern(thing) {
  // thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
  const is = thing instanceof Pattern || thing._Pattern;
  if (!thing instanceof Pattern) {
    console.warn(
      `Found Pattern that fails "instanceof Pattern" check.
      This may happen if you are using multiple versions of @strudel.cycles/core. 
      Please check by running "npm ls @strudel.cycles/core".`,
    );
  }
  return is;
}

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

/** The given items are played at the same time at the same length.
 *
 * @return {Pattern}
 * @example
 * stack(g3, b3, [e4, d4]) // "g3,b3,[e4,d4]"
 */
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)));
  return new Pattern(query);
}

/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
 *
 * synonyms: {@link cat}
 *
 * @return {Pattern}
 * @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) ? sequence(...pat) : reify(pat)));

  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))));
  };
  return new Pattern(query)._splitQueries();
}

/** Concatenation: combines a list of patterns, switching between them successively, one per cycle. Unlike slowcat, this version will skip cycles.
 * @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();
}

/** Concatenation: as with {@link slowcat}, but squashes a cycle from each pattern into one cycle
 *
 * Synonyms: {@link seq}, {@link sequence}
 *
 * @param {...any} items - The items to concatenate
 * @return {Pattern}
 * @example
 * fastcat(e5, b4, [d5, c5])
 * // sequence(e5, b4, [d5, c5])
 * // seq(e5, b4, [d5, c5])
 */
export function fastcat(...pats) {
  return slowcat(...pats)._fast(pats.length);
}

/** The given items are con**cat**enated, where each one takes one cycle. Synonym: slowcat
 *
 * @param {...any} items - The items to concatenate
 * @return {Pattern}
 * @example
 * cat(e5, b4, [d5, c5]) // "<e5 b4 [d5 c5]>"
 *
 */
export function cat(...pats) {
  return slowcat(...pats);
}

/** Like {@link seq}, but each step has a length, relative to the whole.
 * @return {Pattern}
 * @example
 * timeCat([3,e3],[1, g3]) // "e3@3 g3"
 */
export function timeCat(...timepats) {
  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) {
    const end = begin.add(time);
    pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
    begin = end;
  }
  return stack(...pats);
}

/** See {@link fastcat} */
export function sequence(...pats) {
  return fastcat(...pats);
}

/** Like **cat**, but the items are crammed into one cycle. Synonyms: fastcat, sequence
 * @example
 * seq(e5, b4, [d5, c5]) // "e5 b4 [d5 c5]"
 *
 */
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 function polymeterSteps(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) {
      next;
    }
    if (steps == seq[1]) {
      pats.push(seq[0]);
    } else {
      pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1]))));
    }
  }
  return stack(...pats);
}

export function polymeter(...args) {
  return polymeterSteps(0, ...args);
}

// alias
export function pm(...args) {
  polymeter(...args);
}

export const add = curry((a, pat) => pat.add(a));
export const chop = curry((a, pat) => pat.chop(a));
export const chunk = curry((a, pat) => pat.chunk(a));
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
export const div = curry((a, pat) => pat.div(a));
export const early = curry((a, pat) => pat.early(a));
export const echo = curry((a, b, c, pat) => pat.echo(a, b, c));
export const every = curry((i, f, pat) => pat.every(i, f));
export const fast = curry((a, pat) => pat.fast(a));
export const inv = (pat) => pat.inv();
export const invert = (pat) => pat.invert();
export const iter = curry((a, pat) => pat.iter(a));
export const iterBack = curry((a, pat) => pat.iter(a));
export const jux = curry((f, pat) => pat.jux(f));
export const juxBy = curry((by, f, pat) => pat.juxBy(by, f));
export const late = curry((a, pat) => pat.late(a));
export const linger = curry((a, pat) => pat.linger(a));
export const mask = curry((a, pat) => pat.mask(a));
export const mul = curry((a, pat) => pat.mul(a));
export const off = curry((t, f, pat) => pat.off(t, f));
export const ply = curry((a, pat) => pat.ply(a));
export const range = curry((a, b, pat) => pat.range(a, b));
export const range2 = curry((a, b, pat) => pat.range2(a, b));
export const rev = (pat) => pat.rev();
export const slow = curry((a, pat) => pat.slow(a));
export const struct = curry((a, pat) => pat.struct(a));
export const sub = curry((a, pat) => pat.sub(a));
export const superimpose = curry((array, pat) => pat.superimpose(...array));
export const set = curry((a, pat) => pat.set(a));
export const when = curry((binary, f, pat) => pat.when(binary, f));

// problem: curried functions with spread arguments must have pat at the beginning
// with this, we cannot keep the pattern open at the end.. solution for now: use array to keep using pat as last arg

// these are the core composable functions. they are extended with Pattern.prototype.define below
Pattern.prototype.composable = { fast, slow, early, late, superimpose };

// adds Pattern.prototype.composable to given function as child functions
// then you can do transpose(2).late(0.2) instead of x => x.transpose(2).late(0.2)
export function makeComposable(func) {
  Object.entries(Pattern.prototype.composable).forEach(([functionName, composable]) => {
    // compose with dot
    func[functionName] = (...args) => {
      // console.log(`called ${functionName}(${args.join(',')})`);
      const composition = compose(func, composable(...args));
      // the composition itself must be composable too :)
      // then you can do endless chaining transpose(2).late(0.2).fast(2) ...
      return makeComposable(composition);
    };
  });
  return func;
}

export const patternify2 = (f) => (pata, patb, pat) =>
  pata
    .fmap((a) => (b) => f.call(pat, a, b))
    .appLeft(patb)
    .innerJoin();
export const patternify3 = (f) => (pata, patb, patc, pat) =>
  pata
    .fmap((a) => (b) => (c) => f.call(pat, a, b, c))
    .appLeft(patb)
    .appLeft(patc)
    .innerJoin();
export const patternify4 = (f) => (pata, patb, patc, patd, pat) =>
  pata
    .fmap((a) => (b) => (c) => (d) => f.call(pat, a, b, c, d))
    .appLeft(patb)
    .appLeft(patc)
    .appLeft(patd)
    .innerJoin();

Pattern.prototype.echo = function (...args) {
  args = args.map(reify);
  return patternify3(Pattern.prototype._echo)(...args, this);
};
Pattern.prototype.echoWith = function (...args) {
  args = args.map(reify);
  return patternify3(Pattern.prototype._echoWith)(...args, this);
};
Pattern.prototype.chunk = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._chunk)(...args, this);
};
Pattern.prototype.chunkBack = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._chunkBack)(...args, this);
};
Pattern.prototype.loopAt = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._loopAt)(...args, this);
};
Pattern.prototype.zoom = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._zoom)(...args, this);
};
Pattern.prototype.compress = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._compress)(...args, this);
};
Pattern.prototype.outside = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._outside)(...args, this);
};
Pattern.prototype.inside = function (...args) {
  args = args.map(reify);
  return patternify2(Pattern.prototype._inside)(...args, this);
};

// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
Pattern.prototype.bootstrap = function () {
  // makeComposable(Pattern.prototype);
  const bootstrapped = Object.fromEntries(
    Object.entries(Pattern.prototype.composable).map(([functionName, composable]) => {
      if (Pattern.prototype[functionName]) {
        // without this, 'C^7'.m.chordBass.transpose(2) will throw "C^7".m.chordBass.transpose is not a function
        Pattern.prototype[functionName] = makeComposable(Pattern.prototype[functionName]); // is this needed?
      }
      return [functionName, curry(composable, makeComposable)];
    }),
  );
  // note: this === Pattern.prototypetgh6z
  this.patternified.forEach((prop) => {
    // the following will patternify all functions in Pattern.prototype.patternified
    Pattern.prototype[prop] = function (...args) {
      return this._patternify(Pattern.prototype['_' + prop])(...args);
    };
    // with the following, you can do, e.g. `stack(c3).fast.slowcat(1, 2, 4, 8)` instead of `stack(c3).fast(slowcat(1, 2, 4, 8))`
    // TODO: find a way to implement below outside of constructor (code only worked there)
    /* Object.assign(
      Pattern.prototype[prop],
      Object.fromEntries(
        Object.entries(Pattern.prototype.factories).map(([type, func]) => [
          type,
          function(...args) {
            console.log('this', this);
            return this[prop](func(...args)) 
          }
        ])
      )
    ); */
  });
  return bootstrapped;
};

// this will add func as name to list of composable / patternified functions.
// those lists will be used in bootstrap to curry and compose everything, to support various call patterns
Pattern.prototype.define = (name, func, options = {}) => {
  if (options.composable) {
    Pattern.prototype.composable[name] = func;
  }
  if (options.patternified) {
    Pattern.prototype.patternified = Pattern.prototype.patternified.concat([name]);
  }
  Pattern.prototype.bootstrap(); // automatically bootstrap after new definition
};

// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true });
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, composable: true });
