const { extractNumber, extractOperator } = require('./parameter_extractors');
const { dynamicVariableMap, dynamicVariableName, materializeDynamicExpression } = require('./dynamic_context');

function mod(value, n) {
  return ((Math.trunc(Number(value) || 0) % n) + n) % n;
}

function realMod(value, n) {
  const number = Number(value) || 0;
  return ((number % n) + n) % n;
}

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

function wrap(expr) {
  return `(${expr})`;
}

function positiveModExpression(expression, modulusExpression) {
  return `(((${expression}) mod ${modulusExpression}) + ${modulusExpression}) mod ${modulusExpression}`;
}

function normalizeExpression(expression) {
  return positiveModExpression(`round(${expression})`, 'N');
}

function normalizeValue(value, N) {
  return mod(Math.round(value), N);
}

function asInt(value) {
  return Math.trunc(Number(value) || 0);
}

function divisor(number) {
  return Math.abs(asInt(number)) + 1;
}

function modulus(number) {
  return Math.abs(asInt(number)) + 2;
}

function coeff(number, max) {
  return mod(Math.abs(asInt(number)), max) + 1;
}

function axisForIndex(seedValue, index) {
  const value = mod(seedValue + index * 17, 5);
  if (value === 0) return 'both';
  return value % 2 === 0 ? 'u' : 'v';
}

function staticOperatorExpression(operatorId, expression, number) {
  const n = asInt(number);
  const d = divisor(n);
  const m = modulus(n);
  const c11 = coeff(n, 11);
  const c13 = coeff(n, 13);
  const c17 = coeff(n, 17);
  const c19 = coeff(n, 19);
  const c23 = coeff(n, 23);
  const c29 = coeff(n, 29);
  const c31 = coeff(n, 31);
  const c37 = coeff(n, 37);
  const z = wrap(expression);

  switch (operatorId) {
    case 'add':
      return `${z} + ${n}`;
    case 'subtract':
      return `${z} - ${n}`;
    case 'absolute':
      return `abs(${z} - ${n})`;
    case 'multiply':
      return `${z} * (${n} + if(${n} == 0, 1, 0))`;
    case 'divide':
      return `floor(${z} / ${d})`;
    case 'floor':
      return `floor((${z} + ${n}) / ${d})`;
    case 'ceil':
      return `ceil((${z} + ${n}) / ${d})`;
    case 'round':
      return `round((${z} + ${n}) / ${d})`;
    case 'mod':
      return positiveModExpression(z, m);
    case 'sqrt':
      return `sqrt(abs(${z}) + abs(${n}))`;
    case 'power':
      return `(${positiveModExpression(`abs(${z}) + ${positiveModExpression(n, 11)}`, 'N')}) ^ 2`;
    case 'sum':
      return `${z} + (${z} + ${n}) + (${z} + ${n * 2})`;
    case 'product':
      return `${z} * (${positiveModExpression(n, m)} + 1)`;
    case 'dot':
      return `${z} * ${c19} + x * ${c23} + y * ${c29}`;
    case 'norm':
      return `abs(${z}) + abs(x - ${n}) + abs(y - ${n})`;
    case 'matrix_add':
      return `${z} + ${c31}*x + ${c37}*y`;
    case 'transpose':
      return `${z} + ${c31}*y + ${c37}*x`;
    case 'trace':
      return `${z} + (${c31} * (x + y)) + ${n}`;
    case 'matrix_mul':
      return `${z} + ${c11}*x + ${c13}*y + ${c17}*x*y`;
    case 'determinant':
      return `${z} + (${c13}*x*x - ${c17}*y*y)`;
    case 'integral':
      return `${z} + floor((x*x + y*y + ${n}) / ${d})`;
    case 'derivative':
      return `${z} + ${c17} * if((x + y + ${n}) mod 2 == 0, x, y)`;
    case 'partial_derivative':
      return `${z} + ${c19} * if((x - y + ${n}) mod 2 == 0, y, x)`;
    case 'gradient':
      return `${z} + abs(${c17}*x - ${c19}*y) + ${n}`;
    case 'rank':
      return `${z} + if(((${c13}*x + ${c17}*y + ${n}) mod ${m}) == 0, 1, 2)`;
    case 'series':
      return `${z} + ${positiveModExpression('x', m)} + ${positiveModExpression(`x + y + ${n}`, m)} + ${positiveModExpression(`x + 2*y + ${n}`, m)}`;
    case 'laplace_transform':
      return `${z} + floor((${z} + ${n}) / ${d}) + ${positiveModExpression(`x + y + ${n}`, m)}`;
    case 'fourier_transform':
      return `${z} + table(x + y + ${n}, 0, 1, 0, -1, 0, 1, 0, -1)`;
    case 'inverse':
      return `${z} + floor(${m} / (abs(${z}) + 1))`;
    case 'eigenvalue':
      return `${z} + floor(((${c17}*x + ${c19}*y) + ${n}) / ${d})`;
    case 'laplacian_operator':
      return `${z} + abs((x + 1) + (x - 1) + (y + 1) + (y - 1) - 2*x - 2*y + ${n})`;
    case 'limit':
      return `${z} + floor((${z} + ${n}) / (${d} + 1))`;
    case 'cross':
      return `${z} + (${c17}*x - ${c19}*y) + ${n}`;
    default:
      return `${z} + ${n}`;
  }
}

function applyStaticOperator(value, operatorId, number, x, y, N) {
  const n = asInt(number);
  const d = divisor(n);
  const m = modulus(n);
  const c11 = coeff(n, 11);
  const c13 = coeff(n, 13);
  const c17 = coeff(n, 17);
  const c19 = coeff(n, 19);
  const c23 = coeff(n, 23);
  const c29 = coeff(n, 29);
  const c31 = coeff(n, 31);
  const c37 = coeff(n, 37);
  const z = Number(value) || 0;

  switch (operatorId) {
    case 'add':
      return z + n;
    case 'subtract':
      return z - n;
    case 'absolute':
      return Math.abs(z - n);
    case 'multiply':
      return z * (n + (n === 0 ? 1 : 0));
    case 'divide':
      return Math.floor(z / d);
    case 'floor':
      return Math.floor((z + n) / d);
    case 'ceil':
      return Math.ceil((z + n) / d);
    case 'round':
      return Math.round((z + n) / d);
    case 'mod':
      return realMod(z, m);
    case 'sqrt':
      return Math.sqrt(Math.abs(z) + Math.abs(n));
    case 'power':
      return realMod(Math.abs(z) + realMod(n, 11), N) ** 2;
    case 'sum':
      return z + (z + n) + (z + n * 2);
    case 'product':
      return z * (mod(n, m) + 1);
    case 'dot':
      return z * c19 + x * c23 + y * c29;
    case 'norm':
      return Math.abs(z) + Math.abs(x - n) + Math.abs(y - n);
    case 'matrix_add':
      return z + c31 * x + c37 * y;
    case 'transpose':
      return z + c31 * y + c37 * x;
    case 'trace':
      return z + c31 * (x + y) + n;
    case 'matrix_mul':
      return z + c11 * x + c13 * y + c17 * x * y;
    case 'determinant':
      return z + (c13 * x * x - c17 * y * y);
    case 'integral':
      return z + Math.floor((x * x + y * y + n) / d);
    case 'derivative':
      return z + c17 * (mod(x + y + n, 2) === 0 ? x : y);
    case 'partial_derivative':
      return z + c19 * (mod(x - y + n, 2) === 0 ? y : x);
    case 'gradient':
      return z + Math.abs(c17 * x - c19 * y) + n;
    case 'rank':
      return z + (mod(c13 * x + c17 * y + n, m) === 0 ? 1 : 2);
    case 'series':
      return z + mod(x, m) + mod(x + y + n, m) + mod(x + 2 * y + n, m);
    case 'laplace_transform':
      return z + Math.floor((z + n) / d) + mod(x + y + n, m);
    case 'fourier_transform': {
      const table = [0, 1, 0, -1, 0, 1, 0, -1];
      return z + table[mod(x + y + n, table.length)];
    }
    case 'inverse':
      return z + Math.floor(m / (Math.abs(z) + 1));
    case 'eigenvalue':
      return z + Math.floor((c17 * x + c19 * y + n) / d);
    case 'laplacian_operator':
      return z + Math.abs((x + 1) + (x - 1) + (y + 1) + (y - 1) - 2 * x - 2 * y + n);
    case 'limit':
      return z + Math.floor((z + n) / (d + 1));
    case 'cross':
      return z + (c17 * x - c19 * y) + n;
    default:
      return z + n;
  }
}

function dynamicExpression(param, expression) {
  const z = wrap(expression);
  if (param.family === 'CHAIN_STATE') {
    const seed = dynamicVariableName(param, 'seed');
    const modulusName = dynamicVariableName(param, 'modulus');
    const m = `(abs(${modulusName}) + 2)`;
    return positiveModExpression(`${z} + ${seed}`, m);
  }
  if (param.family === 'PERIODIC_CYCLE') {
    const phase = dynamicVariableName(param, 'phase');
    const value = dynamicVariableName(param, 'value');
    const amplitude = dynamicVariableName(param, 'amplitude');
    return `${z} + ${value} + ${amplitude} * ${phase}`;
  }
  if (param.family === 'TIME_CLOCK') {
    const bucket = dynamicVariableName(param, 'bucket');
    const window = dynamicVariableName(param, 'window');
    const scaled = dynamicVariableName(param, 'scaled');
    return `${z} + if(${window} == 0, ${scaled}, ${bucket})`;
  }
  if (param.family === 'HALF_LIFE_DECAY') {
    const decay = dynamicVariableName(param, 'decay');
    return `${z} + ${decay}`;
  }
  return expression;
}

function applyDynamicValue(value, param, variableMap, N) {
  if (param.family === 'CHAIN_STATE') {
    const seed = variableMap[dynamicVariableName(param, 'seed')] || 0;
    const modulusName = variableMap[dynamicVariableName(param, 'modulus')] || N;
    return realMod(value + seed, Math.abs(modulusName) + 2);
  }
  if (param.family === 'PERIODIC_CYCLE') {
    const phase = variableMap[dynamicVariableName(param, 'phase')] || 0;
    const dynamicValue = variableMap[dynamicVariableName(param, 'value')] || 0;
    const amplitude = variableMap[dynamicVariableName(param, 'amplitude')] || 0;
    return value + dynamicValue + amplitude * phase;
  }
  if (param.family === 'TIME_CLOCK') {
    const bucket = variableMap[dynamicVariableName(param, 'bucket')] || 0;
    const window = variableMap[dynamicVariableName(param, 'window')] || 0;
    const scaled = variableMap[dynamicVariableName(param, 'scaled')] || 0;
    return value + (window === 0 ? scaled : bucket);
  }
  if (param.family === 'HALF_LIFE_DECAY') {
    return value + (variableMap[dynamicVariableName(param, 'decay')] || 0);
  }
  return value;
}

function buildOutputPipelinePlan({
  parameterPlan = null,
  dynamicParameterPlan = null,
  seed = 'output-pipeline',
} = {}) {
  const parameterCount = Math.max(0, Math.trunc(Number(parameterPlan?.parameter_count || 0)));
  const axisSeed = Array.from(String(seed)).reduce((sum, ch) => sum + ch.charCodeAt(0), 0);
  const staticStages = [];

  for (let index = 0; index < parameterCount; index += 1) {
    const operator = extractOperator({
      seed: `${seed}:static:${index}:operator`,
      minScore: parameterPlan.operator_score_min,
      maxScore: parameterPlan.operator_score_max,
    });
    const number = extractNumber({
      seed: `${seed}:static:${index}:number`,
      min: parameterPlan.number_score_min,
      max: parameterPlan.number_score_max,
      allowNegative: true,
    });
    staticStages.push({
      axis: axisForIndex(axisSeed, index),
      operator_id: operator.id,
      operator_score: operator.difficulty_score,
      number: number.value,
      number_score: number.difficulty_score,
      stage_score: operator.difficulty_score + number.difficulty_score,
    });
  }

  const dynamicParameters = dynamicParameterPlan?.dynamic_parameters || [];
  const dynamicStages = dynamicParameters.map((param, index) => ({
    axis: axisForIndex(axisSeed + 11, index),
    parameter: param,
    stage_score: Number(param.difficulty_score || 0),
  }));

  const staticScore = staticStages.reduce((sum, stage) => sum + stage.stage_score, 0);
  const dynamicScore = dynamicStages.reduce((sum, stage) => sum + stage.stage_score, 0);
  return {
    version: 'output_pipeline_1',
    static_stages: staticStages,
    dynamic_stages: dynamicStages,
    static_parameter_score: staticScore,
    dynamic_parameter_score: dynamicScore,
    output_pipeline_score: staticScore + dynamicScore,
  };
}

function stageApplies(stage, axis) {
  return stage.axis === axis || stage.axis === 'both';
}

function applyOutputPipeline({ output, step, x, y, N, context = {} } = {}) {
  const pipeline = step.output_pipeline || {};
  let u = output.u;
  let v = output.v;
  for (const stage of pipeline.static_stages || []) {
    if (stageApplies(stage, 'u')) u = normalizeValue(applyStaticOperator(u, stage.operator_id, stage.number, x, y, N), N);
    if (stageApplies(stage, 'v')) v = normalizeValue(applyStaticOperator(v, stage.operator_id, stage.number, x, y, N), N);
  }

  const dynamicParams = (pipeline.dynamic_stages || []).map((stage) => stage.parameter).filter(Boolean);
  const variableMap = dynamicVariableMap(dynamicParams, context, N);
  for (const stage of pipeline.dynamic_stages || []) {
    if (stageApplies(stage, 'u')) u = normalizeValue(applyDynamicValue(u, stage.parameter, variableMap, N), N);
    if (stageApplies(stage, 'v')) v = normalizeValue(applyDynamicValue(v, stage.parameter, variableMap, N), N);
  }
  return { u: normalizeValue(u, N), v: normalizeValue(v, N) };
}

function applyOutputPipelineToExpressions({ expressions, step, context = {}, N = 40 } = {}) {
  const pipeline = step.output_pipeline || {};
  let u = normalizeExpression(expressions.u);
  let v = normalizeExpression(expressions.v);
  for (const stage of pipeline.static_stages || []) {
    if (stageApplies(stage, 'u')) u = normalizeExpression(staticOperatorExpression(stage.operator_id, u, stage.number));
    if (stageApplies(stage, 'v')) v = normalizeExpression(staticOperatorExpression(stage.operator_id, v, stage.number));
  }

  for (const stage of pipeline.dynamic_stages || []) {
    if (stageApplies(stage, 'u')) u = normalizeExpression(dynamicExpression(stage.parameter, u));
    if (stageApplies(stage, 'v')) v = normalizeExpression(dynamicExpression(stage.parameter, v));
  }

  const dynamicParams = (pipeline.dynamic_stages || []).map((stage) => stage.parameter).filter(Boolean);
  const variableMap = dynamicVariableMap(dynamicParams, context, N);
  return {
    u: `(${materializeDynamicExpression(u, variableMap)}) mod N`,
    v: `(${materializeDynamicExpression(v, variableMap)}) mod N`,
  };
}

function finalStepScore({ baseDifficultyScore = 0, formulaScore = 0, combinerScore = 0, outputPipelineScore = 0 } = {}) {
  return Math.round(
    Number(baseDifficultyScore || 0) +
      Number(formulaScore || 0) +
      Number(combinerScore || 0) +
      Number(outputPipelineScore || 0),
  );
}

module.exports = {
  applyOutputPipeline,
  applyOutputPipelineToExpressions,
  buildOutputPipelinePlan,
  finalStepScore,
  staticOperatorExpression,
};
