/*
mini.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.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 * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core';

/* var _seedState = 0;
const randOffset = 0.0002;

function _nextSeed() {
  return _seedState++;
} */

const applyOptions = (parent, code) => (pat, i) => {
  const ast = parent.source_[i];
  const options = ast.options_;
  const ops = options?.ops;
  if (ops) {
    for (const op of ops) {
      switch (op.type_) {
        case 'stretch': {
          const legalTypes = ['fast', 'slow'];
          const { type, amount } = op.arguments_;
          if (!legalTypes.includes(type)) {
            throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
          }
          pat = strudel.reify(pat)[type](patternifyAST(amount, code));
          break;
        }
        case 'bjorklund': {
          if (op.arguments_.rotation) {
            pat = pat.euclidRot(
              patternifyAST(op.arguments_.pulse, code),
              patternifyAST(op.arguments_.step, code),
              patternifyAST(op.arguments_.rotation, code),
            );
          } else {
            pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
          }
          break;
        }
        case 'degradeBy': {
          // TODO: find out what is right here
          // example:
          /*
           stack(
             s("hh*8").degrade(),
             s("[ht*8]?")
           )
        */
          // above example will only be in sync when _degradeBy is used...
          // it also seems that the nextSeed will create undeterministic behaviour
          // as it uses a global _seedState. This is probably the reason for
          // https://github.com/tidalcycles/strudel/issues/245

          // this is how it was:
          /* 
        return strudel.reify(pat)._degradeByWith(
          strudel.rand.early(randOffset * _nextSeed()).segment(1),
          op.arguments_.amount ?? 0.5,
        ); 
        */
          pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
          break;
        }
        default: {
          console.warn(`operator "${op.type_}" not implemented`);
        }
      }
    }
  }

  return pat;
};

function resolveReplications(ast) {
  ast.source_ = strudel.flatten(
    ast.source_.map((child) => {
      const { reps } = child.options_ || {};
      if (!reps) {
        return [child];
      }
      delete child.options_.reps;
      return Array(reps).fill(child);
    }),
  );
}

export function patternifyAST(ast, code) {
  switch (ast.type_) {
    case 'pattern': {
      resolveReplications(ast);
      const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
      const alignment = ast.arguments_.alignment;
      if (alignment === 'stack') {
        return strudel.stack(...children);
      }
      if (alignment === 'polymeter') {
        // polymeter
        const stepsPerCycle = ast.arguments_.stepsPerCycle
          ? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
          : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));

        const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
        return strudel.stack(...aligned);
      }
      if (alignment === 'rand') {
        // https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
        // return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
        return strudel.chooseCycles(...children);
      }
      const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
      if (!weightedChildren && alignment === 'slowcat') {
        return strudel.slowcat(...children);
      }
      if (weightedChildren) {
        const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
        const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
        if (alignment === 'slowcat') {
          return pat._slow(weightSum); // timecat + slow
        }
        pat.__weight = weightSum;
        return pat;
      }
      const pat = strudel.sequence(...children);
      pat.__weight = children.length;
      return pat;
    }
    case 'element': {
      return patternifyAST(ast.source_, code);
    }
    case 'atom': {
      if (ast.source_ === '~') {
        return strudel.silence;
      }
      if (!ast.location_) {
        console.warn('no location for', ast);
        return ast.source_;
      }
      const { start, end } = ast.location_;
      const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
      // the following line expects the shapeshifter append .withMiniLocation
      // because location_ is only relative to the mini string, but we need it relative to whole code
      // make sure whitespaces are not part of the highlight:
      const actual = code?.split('').slice(start.offset, end.offset).join('');
      const [offsetStart = 0, offsetEnd = 0] = actual
        ? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
        : [];
      return strudel
        .pure(value)
        .withLocation(
          [start.line, start.column + offsetStart, start.offset + offsetStart],
          [start.line, end.column - offsetEnd, end.offset - offsetEnd],
        );
    }
    case 'stretch':
      return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
    /* case 'scale':
      let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
      const intervals = Scale.get(scale).intervals;
      const pattern = patternifyAST(ast.source_);
      tonic = tonic || 'C4';
      // console.log('scale', ast, pattern, tonic, scale);
      console.log('tonic', tonic);
      return pattern.fmap((step: any) => {
        step = Number(step);
        if (isNaN(step)) {
          console.warn(`scale step "${step}" not a number`);
          return step;
        }
        const octaves = Math.floor(step / intervals.length);
        const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
        const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2
        const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12));
        return Note.transpose(tonic, interval || '1P');
      }); */
    /* case 'struct':
      // TODO:
      return strudel.silence; */
    default:
      console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
      return strudel.silence;
  }
}

// mini notation only (wraps in "")
export const mini = (...strings) => {
  const pats = strings.map((str) => {
    const code = `"${str}"`;
    const ast = krill.parse(code);
    return patternifyAST(ast, code);
  });
  return strudel.sequence(...pats);
};

// includes haskell style (raw krill parsing)
export const h = (string) => {
  const ast = krill.parse(string);
  // console.log('ast', ast);
  return patternifyAST(ast, string);
};

export function minify(thing) {
  if (typeof thing === 'string') {
    return mini(thing);
  }
  return strudel.reify(thing);
}
