/*
pick.mjs - methods that use one pattern to pick events from other patterns.
Copyright (C) 2024 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/signal.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 { Pattern, reify, silence, register } from './pattern.mjs';

import { _mod, clamp, objectMap } from './util.mjs';

const _pick = function (lookup, pat, modulo = true) {
  const array = Array.isArray(lookup);
  const len = Object.keys(lookup).length;

  lookup = objectMap(lookup, reify);

  if (len === 0) {
    return silence;
  }
  return pat.fmap((i) => {
    let key = i;
    if (array) {
      key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
    }
    return lookup[key];
  });
};

/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
 * Similar to `inhabit`, but maintains the structure of the original patterns.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 * @example
 * note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
 * @example
 * sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
 * @example
 * sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
 * @example
 * s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
 */

export const pick = function (lookup, pat) {
  // backward compatibility - the args used to be flipped
  if (Array.isArray(pat)) {
    [pat, lookup] = [lookup, pat];
  }
  return __pick(lookup, pat);
};

const __pick = register('pick', function (lookup, pat) {
  return _pick(lookup, pat, false).innerJoin();
});

/** * The same as `pick`, but if you pick a number greater than the size of the list,
 * it wraps around, rather than sticking at the maximum value.
 * For example, if you pick the fifth pattern of a list of three, you'll get the
 * second one.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */

export const pickmod = register('pickmod', function (lookup, pat) {
  return _pick(lookup, pat, true).innerJoin();
});

/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
 * @tags combiners, functional
 * @param {Pattern} pat
 * @param {Pattern} lookup a pattern of indices or names
 * @param {function[] | object} lookup the array or lookup object of functions from which to pull
 * @returns {Pattern}
 * @example
 * s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
 * @example
 * note("<c2 d2>(3,8)").s("square")
 * .pickF("<0 2> 1", [jux(rev), fast(2), x=>x.lpf(800)])
 * @example
 * note("<c2 d2>(3,8)").s("square")
 * .pickF("<jr l> f", { jr:jux(rev), f:fast(2), l:x=>x.lpf(800) })
 */
export const pickF = register('pickF', function (pickPattern, lookup, pat) {
  return pat.apply(pick(lookup, pickPattern));
});

/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
 * it wraps around, rather than sticking at the maximum value.
 * @tags combiners
 * @param {Pattern} pat
 * @param {Pattern} lookup a pattern of indices or names
 * @param {function[] | object} lookup the array or lookup object of functions from which to pull
 * @returns {Pattern}
 */
export const pickmodF = register('pickmodF', function (pickPattern, lookup, pat) {
  return pat.apply(pickmod(lookup, pickPattern));
});

/** * Similar to `pick`, but it applies an outerJoin instead of an innerJoin.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */
export const pickOut = register('pickOut', function (lookup, pat) {
  return _pick(lookup, pat, false).outerJoin();
});

/** * The same as `pickOut`, but if you pick a number greater than the size of the list,
 * it wraps around, rather than sticking at the maximum value.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */
export const pickmodOut = register('pickmodOut', function (lookup, pat) {
  return _pick(lookup, pat, true).outerJoin();
});

/** * Similar to `pick`, but the choosen pattern is restarted when its index is triggered.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */
export const pickRestart = register('pickRestart', function (lookup, pat) {
  return _pick(lookup, pat, false).restartJoin();
});

/** * The same as `pickRestart`, but if you pick a number greater than the size of the list,
   * it wraps around, rather than sticking at the maximum value.
   * @tags combiners
   * @param {Pattern} pat
   * @param {*} xs
   * @returns {Pattern}
   * @example
   * "<a@2 b@2 c@2 d@2>".pickRestart({
        a: n("0 1 2 0"),
        b: n("2 3 4 ~"),
        c: n("[4 5] [4 3] 2 0"),
        d: n("0 -3 0 ~")
      }).scale("C:major").s("piano")
   */
export const pickmodRestart = register('pickmodRestart', function (lookup, pat) {
  return _pick(lookup, pat, true).restartJoin();
});

/** * Similar to `pick`, but the choosen pattern is reset when its index is triggered.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */
export const pickReset = register('pickReset', function (lookup, pat) {
  return _pick(lookup, pat, false).resetJoin();
});

/** * The same as `pickReset`, but if you pick a number greater than the size of the list,
 * it wraps around, rather than sticking at the maximum value.
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */
export const pickmodReset = register('pickmodReset', function (lookup, pat) {
  return _pick(lookup, pat, true).resetJoin();
});

/** Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
 * Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
 * @name inhabit
 * @tags combiners
 * @synonyms pickSqueeze
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 * @example
 * let a = s("bd(3,8)")
 * let b = s("cp sd")
 * "<a b [a,b]>".inhabit({ a, b })
 * @example
 * s("a@2 [a b] a"
 * .inhabit({a: "bd(3,8)", b: "sd sd"}))
 * .slow(4)
 */
export const { inhabit, pickSqueeze } = register(['inhabit', 'pickSqueeze'], function (lookup, pat) {
  return _pick(lookup, pat, false).squeezeJoin();
});

/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
 * it wraps around, rather than sticking at the maximum value.
 * For example, if you pick the fifth pattern of a list of three, you'll get the
 * second one.
 * @name inhabitmod
 * @synonyms pickmodSqueeze
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 */

export const { inhabitmod, pickmodSqueeze } = register(['inhabitmod', 'pickmodSqueeze'], function (lookup, pat) {
  return _pick(lookup, pat, true).squeezeJoin();
});

/**
 * Pick from the list of values (or patterns of values) via the index using the given
 * pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
 * @tags combiners
 * @param {Pattern} pat
 * @param {*} xs
 * @returns {Pattern}
 * @example
 * note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
 */

export const squeeze = (pat, xs) => {
  xs = xs.map(reify);
  if (xs.length == 0) {
    return silence;
  }
  return pat
    .fmap((i) => {
      const key = _mod(Math.round(i), xs.length);
      return xs[key];
    })
    .squeezeJoin();
};
