const { Rng } = require('./rng');

const OPERATOR_POOL = [
  { id: 'add', symbol: '+', name: 'addition', group: 'basic_arithmetic', difficulty_score: 20 },
  { id: 'subtract', symbol: '-', name: 'subtraction', group: 'basic_arithmetic', difficulty_score: 25 },
  { id: 'absolute', symbol: 'abs(x)', name: 'absolute_value', group: 'basic_arithmetic', difficulty_score: 80 },
  { id: 'multiply', symbol: '*', name: 'multiplication', group: 'basic_arithmetic', difficulty_score: 100 },
  { id: 'divide', symbol: '/', name: 'division', group: 'basic_arithmetic', difficulty_score: 140 },
  { id: 'floor', symbol: 'floor(x)', name: 'floor', group: 'basic_arithmetic', difficulty_score: 170 },
  { id: 'ceil', symbol: 'ceil(x)', name: 'ceil', group: 'basic_arithmetic', difficulty_score: 180 },
  { id: 'round', symbol: 'round(x)', name: 'round', group: 'basic_arithmetic', difficulty_score: 190 },
  { id: 'mod', symbol: 'mod', name: 'modulo', group: 'basic_arithmetic', difficulty_score: 220 },
  { id: 'sqrt', symbol: 'sqrt(x)', name: 'square_root', group: 'basic_arithmetic', difficulty_score: 260 },
  { id: 'power', symbol: '^', name: 'power', group: 'basic_arithmetic', difficulty_score: 320 },
  { id: 'sum', symbol: 'sum', name: 'summation', group: 'advanced_math', difficulty_score: 360 },
  { id: 'product', symbol: 'product', name: 'product', group: 'advanced_math', difficulty_score: 400 },
  { id: 'dot', symbol: 'dot', name: 'dot_product', group: 'linear_algebra', difficulty_score: 430 },
  { id: 'norm', symbol: 'norm', name: 'vector_norm', group: 'linear_algebra', difficulty_score: 470 },
  { id: 'matrix_add', symbol: 'matrix_add', name: 'matrix_addition', group: 'linear_algebra', difficulty_score: 500 },
  { id: 'transpose', symbol: 'transpose', name: 'matrix_transpose', group: 'linear_algebra', difficulty_score: 530 },
  { id: 'trace', symbol: 'trace', name: 'matrix_trace', group: 'linear_algebra', difficulty_score: 560 },
  { id: 'matrix_mul', symbol: 'matrix_mul', name: 'matrix_multiplication', group: 'linear_algebra', difficulty_score: 600 },
  { id: 'determinant', symbol: 'det', name: 'determinant', group: 'linear_algebra', difficulty_score: 650 },
  { id: 'integral', symbol: 'integral', name: 'integral', group: 'advanced_math', difficulty_score: 700 },
  { id: 'derivative', symbol: 'd/dx', name: 'derivative', group: 'advanced_math', difficulty_score: 730 },
  { id: 'partial_derivative', symbol: 'partial', name: 'partial_derivative', group: 'advanced_math', difficulty_score: 760 },
  { id: 'gradient', symbol: 'gradient', name: 'gradient', group: 'advanced_math', difficulty_score: 790 },
  { id: 'rank', symbol: 'rank', name: 'matrix_rank', group: 'linear_algebra', difficulty_score: 810 },
  { id: 'series', symbol: 'series', name: 'series', group: 'advanced_math', difficulty_score: 840 },
  { id: 'laplace_transform', symbol: 'Laplace', name: 'laplace_transform', group: 'advanced_math', difficulty_score: 870 },
  { id: 'fourier_transform', symbol: 'Fourier', name: 'fourier_transform', group: 'advanced_math', difficulty_score: 900 },
  { id: 'inverse', symbol: 'inverse', name: 'matrix_inverse', group: 'linear_algebra', difficulty_score: 920 },
  { id: 'eigenvalue', symbol: 'eigenvalue', name: 'eigenvalue', group: 'linear_algebra', difficulty_score: 950 },
  { id: 'laplacian_operator', symbol: 'laplacian', name: 'laplacian_operator', group: 'advanced_math', difficulty_score: 970 },
  { id: 'limit', symbol: 'limit', name: 'limit', group: 'advanced_math', difficulty_score: 980 },
  { id: 'cross', symbol: 'cross', name: 'cross_product', group: 'linear_algebra', difficulty_score: 990 },
];

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function operatorCandidates({ minScore = 1, maxScore = 1000, groups = null } = {}) {
  const allowedGroups = Array.isArray(groups) && groups.length ? new Set(groups) : null;
  return OPERATOR_POOL.filter(
    (operator) =>
      operator.difficulty_score >= minScore &&
      operator.difficulty_score <= maxScore &&
      (!allowedGroups || allowedGroups.has(operator.group)),
  );
}

function extractOperator({ seed = 'operator', minScore = 1, maxScore = 1000, groups = null } = {}) {
  const candidates = operatorCandidates({ minScore, maxScore, groups });
  if (!candidates.length) throw new Error('No operator candidates for the requested score range.');
  const rng = new Rng(seed);
  return rng.pick(candidates);
}

function extractOperators({
  seed = 'operators',
  count = 1,
  minScore = 1,
  maxScore = 1000,
  groups = null,
  allowRepeat = false,
} = {}) {
  const candidates = operatorCandidates({ minScore, maxScore, groups });
  if (!candidates.length) throw new Error('No operator candidates for the requested score range.');
  const rng = new Rng(seed);
  const pool = [...candidates];
  const result = [];
  const total = Math.max(0, Math.trunc(count));

  for (let i = 0; i < total; i += 1) {
    if (!allowRepeat && !pool.length) break;
    const source = allowRepeat ? candidates : pool;
    const index = rng.int(0, source.length - 1);
    result.push(source[index]);
    if (!allowRepeat) pool.splice(index, 1);
  }

  return result;
}

function scoreNumber(value) {
  return clamp(Math.abs(Math.trunc(Number(value) || 0)), 1, 1000);
}

function extractNumber({ seed = 'number', min = 1, max = 1000, allowNegative = false } = {}) {
  const low = Math.trunc(Math.min(min, max));
  const high = Math.trunc(Math.max(min, max));
  const rng = new Rng(seed);
  let value = rng.int(low, high);
  if (allowNegative && rng.int(0, 1) === 1) value = -value;
  return {
    value,
    difficulty_score: scoreNumber(value),
  };
}

function extractNumbers({ seed = 'numbers', count = 1, min = 1, max = 1000, allowNegative = false } = {}) {
  const rng = new Rng(seed);
  const total = Math.max(0, Math.trunc(count));
  return Array.from({ length: total }, (_, index) => {
    const localSeed = `${seed}:${index}:${rng.int(0, 0x7fffffff)}`;
    return extractNumber({ seed: localSeed, min, max, allowNegative });
  });
}

module.exports = {
  OPERATOR_POOL,
  extractOperator,
  extractOperators,
  extractNumber,
  extractNumbers,
  operatorCandidates,
  scoreNumber,
};
