// ─── Pure conversion engine ─────────────────────────────────────────
// No DOM dependencies — just math. Used by the Calculator component.

import type { CalculatorDef } from './data/calculators';

export interface SolveResult {
  val1: string;
  val2: string;
  val3: string;
}

function fmt(n: number): string {
  if (!Number.isFinite(n)) return n.toString();
  if (n === 0) return '0';
  if (Math.abs(n) < 1e-6) {
    return n.toExponential(6);
  }
  const rounded = parseFloat(n.toFixed(6));
  return rounded.toString();
}

function gcd(a: number, b: number): number {
  a = Math.abs(Math.round(a));
  b = Math.abs(Math.round(b));
  return b ? gcd(b, a % b) : a;
}

/**
 * Run the conversion for a given calculator definition.
 * `source` indicates which field the user is typing in (1, 2, or 3).
 * Returns new string values for all fields.
 */
export function solve(
  calc: CalculatorDef,
  source: 1 | 2 | 3,
  rawVal1: string,
  rawVal2: string,
  rawVal3: string
): SolveResult {
  const v1 = parseFloat(rawVal1);
  const v2 = parseFloat(rawVal2);
  const v3 = parseFloat(rawVal3);
  const factor = calc.factor ?? 1;
  const offset = calc.offset ?? 0;

  let out: SolveResult = { val1: rawVal1, val2: rawVal2, val3: rawVal3 };

  switch (calc.type) {
    case 'standard':
      if (source === 1) {
        out.val2 = !isNaN(v1) ? fmt(v1 * factor + offset) : '';
      } else {
        out.val1 = !isNaN(v2) ? fmt((v2 - offset) / factor) : '';
      }
      break;

    case 'inverse':
      if (source === 1) {
        out.val2 = (!isNaN(v1) && v1 !== 0) ? fmt(factor / v1) : '';
      } else {
        out.val1 = (!isNaN(v2) && v2 !== 0) ? fmt(factor / v2) : '';
      }
      break;

    case '3col':
      if (source === 1 || source === 2) {
        out.val3 = (!isNaN(v1) && !isNaN(v2) && v2 !== 0) ? fmt(v1 / v2) : '';
      } else {
        out.val1 = (!isNaN(v3) && !isNaN(v2)) ? fmt(v3 * v2) : '';
      }
      break;

    case '3col-mul':
      if (source === 1 || source === 2) {
        out.val3 = (!isNaN(v1) && !isNaN(v2)) ? fmt(v1 * v2) : '';
      } else {
        out.val1 = (!isNaN(v3) && !isNaN(v2) && v2 !== 0) ? fmt(v3 / v2) : '';
      }
      break;

    case 'base': {
      const fromBase = calc.fromBase ?? 10;
      const toBase = calc.toBase ?? 2;
      if (source === 1) {
        const val = rawVal1.trim();
        if (!val) { out.val2 = ''; break; }
        const dec = parseInt(val, fromBase);
        out.val2 = !isNaN(dec) ? dec.toString(toBase).toUpperCase() : 'Invalid';
      } else {
        const val = rawVal2.trim();
        if (!val) { out.val1 = ''; break; }
        const dec = parseInt(val, toBase);
        out.val1 = !isNaN(dec) ? dec.toString(fromBase).toUpperCase() : 'Invalid';
      }
      break;
    }

    case 'text-bin':
      if (source === 1) {
        out.val2 = rawVal1.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' ');
      } else {
        try {
          out.val1 = rawVal2.split(' ').map(b => String.fromCharCode(parseInt(b, 2))).join('');
        } catch { out.val1 = 'Error'; }
      }
      break;

    case 'bin-text':
      if (source === 1) {
        try {
          out.val2 = rawVal1.split(' ').map(b => String.fromCharCode(parseInt(b, 2))).join('');
        } catch { out.val2 = 'Error'; }
      } else {
        out.val1 = rawVal2.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' ');
      }
      break;

    case 'dms-dd':
      if (source === 1) {
        if (!isNaN(v1)) {
          const d = Math.floor(v1);
          const md = (v1 - d) * 60;
          const m = Math.floor(md);
          const sec = ((md - m) * 60).toFixed(2);
          out.val2 = `${d}° ${m}' ${sec}"`;
        } else { out.val2 = ''; }
      } else {
        const match = rawVal2.match(/(?:([0-9.-]+)\s*°)?\s*(?:([0-9.-]+)\s*')?\s*(?:([0-9.-]+)\s*")?/);
        if (match && rawVal2.trim().length > 0) {
          const d = parseFloat(match[1]) || 0;
          const m = parseFloat(match[2]) || 0;
          const sec = parseFloat(match[3]) || 0;
          out.val1 = fmt(d + m / 60 + sec / 3600);
        } else { out.val1 = ''; }
      }
      break;

    case 'dec-frac': {
      // Two calculators share this type:
      //  - decimal -> fraction
      //  - fraction -> decimal
      // Detect which direction the left field represents via its label.
      const fractionFirst = calc.labels.in1.toLowerCase().includes('fraction');

      const decimalToFraction = (n: number) => {
        if (isNaN(n)) return '';
        const parts = n.toString().split('.');
        const len = parts[1] ? parts[1].length : 0;
        const den = Math.pow(10, len);
        const num = n * den;
        const div = gcd(num, den);
        return `${num / div}/${den / div}`;
      };

      const fractionToDecimal = (raw: string) => {
        const parts = raw.split('/');
        if (parts.length === 2 && !isNaN(Number(parts[0])) && !isNaN(Number(parts[1])) && Number(parts[1]) !== 0) {
          return fmt(Number(parts[0]) / Number(parts[1]));
        }
        const f = parseFloat(parts[0]);
        return !isNaN(f) ? f.toString() : '';
      };

      if (fractionFirst) {
        if (source === 1) {
          out.val2 = fractionToDecimal(rawVal1);
        } else {
          out.val1 = decimalToFraction(v2);
        }
      } else {
        if (source === 1) {
          out.val2 = decimalToFraction(v1);
        } else {
          out.val1 = fractionToDecimal(rawVal2);
        }
      }
      break;
    }

    case 'db-int':
      if (source === 1) {
        out.val2 = !isNaN(v1) ? (1e-12 * Math.pow(10, v1 / 10)).toExponential(6) : '';
      } else {
        out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(10 * Math.log10(v2 / 1e-12)) : '';
      }
      break;

    case 'db-spl':
      if (source === 1) {
        out.val2 = !isNaN(v1) ? fmt(20 * Math.pow(10, v1 / 20)) : '';
      } else {
        out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(20 * Math.log10(v2 / 20)) : '';
      }
      break;

    case 'db-v':
      if (source === 1) {
        out.val2 = !isNaN(v1) ? fmt(Math.pow(10, v1 / 20)) : '';
      } else {
        out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(20 * Math.log10(v2)) : '';
      }
      break;

    case 'db-w':
      if (source === 1) {
        out.val2 = !isNaN(v1) ? fmt(Math.pow(10, v1 / 10)) : '';
      } else {
        out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(10 * Math.log10(v2)) : '';
      }
      break;

    case 'awg': {
      const log92 = Math.log(92);
      const awgToDiameterMm = (g: number) => 0.127 * Math.pow(92, (36 - g) / 39);
      const diameterMmToAwg = (d: number) => 36 - 39 * Math.log(d / 0.127) / log92;
      const awgToCircularMils = (g: number) => 1000 * Math.pow(92, (36 - g) / 19.5);
      const circularMilsToAwg = (a: number) => 36 - 19.5 * Math.log(a / 1000) / log92;
      const awgToAreaMm2 = (g: number) => {
        const d = awgToDiameterMm(g);
        return Math.PI * Math.pow(d, 2) / 4;
      };
      const areaMm2ToAwg = (a: number) => {
        const d = Math.sqrt((4 * a) / Math.PI);
        return diameterMmToAwg(d);
      };

      const slug = calc.slug;
      const formatAwg = (g: number) => isFinite(g) ? fmt(g) : '';
      const awgIsInput = calc.labels.in1.toLowerCase().includes('awg');
      const isCircular = slug.includes('circular-mils');
      const isArea = slug.includes('square-millimeters');

      if (isCircular) {
        if (awgIsInput) {
          if (source === 1) out.val2 = !isNaN(v1) ? fmt(awgToCircularMils(v1)) : '';
          else out.val1 = (!isNaN(v2) && v2 > 0) ? formatAwg(circularMilsToAwg(v2)) : '';
        } else {
          if (source === 1) out.val2 = (!isNaN(v1) && v1 > 0) ? formatAwg(circularMilsToAwg(v1)) : '';
          else out.val1 = !isNaN(v2) ? fmt(awgToCircularMils(v2)) : '';
        }
      } else if (isArea) {
        if (awgIsInput) {
          if (source === 1) out.val2 = !isNaN(v1) ? fmt(awgToAreaMm2(v1)) : '';
          else out.val1 = (!isNaN(v2) && v2 > 0) ? formatAwg(areaMm2ToAwg(v2)) : '';
        } else {
          if (source === 1) out.val2 = (!isNaN(v1) && v1 > 0) ? formatAwg(areaMm2ToAwg(v1)) : '';
          else out.val1 = !isNaN(v2) ? fmt(awgToAreaMm2(v2)) : '';
        }
      } else {
        // diameter in millimeters
        if (awgIsInput) {
          if (source === 1) out.val2 = !isNaN(v1) ? fmt(awgToDiameterMm(v1)) : '';
          else out.val1 = (!isNaN(v2) && v2 > 0) ? formatAwg(diameterMmToAwg(v2)) : '';
        } else {
          if (source === 1) out.val2 = (!isNaN(v1) && v1 > 0) ? formatAwg(diameterMmToAwg(v1)) : '';
          else out.val1 = !isNaN(v2) ? fmt(awgToDiameterMm(v2)) : '';
        }
      }
      break;
    }

    case 'awg-swg': {
      const log92 = Math.log(92);
      const awgToDiameterMm = (g: number) => 0.127 * Math.pow(92, (36 - g) / 39);
      const diameterMmToAwg = (d: number) => 36 - 39 * Math.log(d / 0.127) / log92;
      const swgTable: Record<number, number> = {
        0: 8.23, 1: 7.62, 2: 7.01, 3: 6.4, 4: 5.89, 5: 5.385, 6: 4.877,
        7: 4.47, 8: 4.064, 9: 3.658, 10: 3.251, 11: 2.946, 12: 2.642, 13: 2.337,
        14: 2.032, 15: 1.829, 16: 1.626, 17: 1.422, 18: 1.219, 19: 1.016,
        20: 0.914, 21: 0.813, 22: 0.711, 23: 0.61, 24: 0.559, 25: 0.508,
        26: 0.457, 27: 0.417, 28: 0.376, 29: 0.345, 30: 0.315, 31: 0.294,
        32: 0.274, 33: 0.254, 34: 0.234, 35: 0.213, 36: 0.193,
        37: 0.173, 38: 0.152, 39: 0.132, 40: 0.122, 41: 0.112, 42: 0.102,
        43: 0.091, 44: 0.081, 45: 0.071, 46: 0.061, 47: 0.051, 48: 0.04,
        49: 0.03, 50: 0.025
      };

      const nearestSwg = (diamMm: number) => {
        let bestGauge = 0;
        let bestDiff = Number.POSITIVE_INFINITY;
        for (const [gStr, d] of Object.entries(swgTable)) {
          const diff = Math.abs(diamMm - d);
          if (diff < bestDiff) { bestDiff = diff; bestGauge = Number(gStr); }
        }
        return bestGauge;
      };

      if (source === 1) {
        // AWG -> SWG
        if (!isNaN(v1)) {
          const diam = awgToDiameterMm(v1);
          out.val2 = fmt(nearestSwg(diam));
        } else { out.val2 = ''; }
      } else {
        // SWG -> AWG
        if (!isNaN(v2)) {
          const diam = swgTable[Math.round(v2)];
          if (diam) out.val1 = fmt(diameterMmToAwg(diam));
          else out.val1 = '';
        } else { out.val1 = ''; }
      }
      break;
    }

    case 'ev-lux': {
      const isEvFirst = calc.labels.in1.toLowerCase().includes('ev');
      const toLux = (ev: number) => 2.5 * Math.pow(2, ev);
      const toEv = (lux: number) => lux > 0 ? Math.log(lux / 2.5) / Math.log(2) : NaN;
      if (isEvFirst) {
        if (source === 1) out.val2 = !isNaN(v1) ? fmt(toLux(v1)) : '';
        else out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(toEv(v2)) : '';
      } else {
        if (source === 1) out.val2 = (!isNaN(v1) && v1 > 0) ? fmt(toEv(v1)) : '';
        else out.val1 = !isNaN(v2) ? fmt(toLux(v2)) : '';
      }
      break;
    }

    case 'aov': {
      const sensorWidth = 36; // mm, full-frame horizontal
      const isFocalFirst = calc.labels.in1.toLowerCase().includes('focal');
      const toAov = (f: number) => f > 0 ? (2 * Math.atan(sensorWidth / (2 * f)) * 180 / Math.PI) : NaN;
      const toFocal = (angle: number) => {
        const radians = angle * Math.PI / 180;
        return Math.tan(radians / 2) !== 0 ? sensorWidth / (2 * Math.tan(radians / 2)) : NaN;
      };
      if (isFocalFirst) {
        if (source === 1) out.val2 = (!isNaN(v1) && v1 !== 0) ? fmt(toAov(v1)) : '';
        else out.val1 = (!isNaN(v2) && v2 !== 0) ? fmt(toFocal(v2)) : '';
      } else {
        if (source === 1) out.val2 = (!isNaN(v1) && v1 !== 0) ? fmt(toFocal(v1)) : '';
        else out.val1 = (!isNaN(v2) && v2 !== 0) ? fmt(toAov(v2)) : '';
      }
      break;
    }

    case 'brinell-rockwell': {
      // Approximate correlation for steels:
      // BHN = (1520000 - 4500 * HRC) / (100 - HRC)^2
      if (source === 1) {
        // Brinell to Rockwell C
        if (!isNaN(v1) && v1 > 0) {
          const a = v1;
          const disc = 4500 ** 2 + 4 * a * 1070000;
          const y = (4500 + Math.sqrt(disc)) / (2 * a);
          const hrc = 100 - y;
          out.val2 = fmt(hrc);
        } else {
          out.val2 = '';
        }
      } else {
        // Rockwell C to Brinell
        if (!isNaN(v2) && v2 < 100) {
          const h = v2;
          const bhn = (1520000 - 4500 * h) / Math.pow(100 - h, 2);
          out.val1 = fmt(bhn);
        } else {
          out.val1 = '';
        }
      }
      break;
    }

    case 'molarity': {
      const m = v1; // mol/L
      const gpl = v2; // grams/L
      const molarMass = v3; // g/mol

      if (source === 1) {
        out.val2 = (!isNaN(m) && !isNaN(molarMass)) ? fmt(m * molarMass) : '';
      } else if (source === 2) {
        out.val1 = (!isNaN(gpl) && !isNaN(molarMass) && molarMass !== 0) ? fmt(gpl / molarMass) : '';
      } else {
        if (!isNaN(m) && !isNaN(molarMass)) out.val2 = fmt(m * molarMass);
        else if (!isNaN(gpl) && !isNaN(molarMass) && molarMass !== 0) out.val1 = fmt(gpl / molarMass);
      }
      break;
    }

    case 'rockwell-vickers': {
      const hrcToBhn = (h: number) => (1520000 - 4500 * h) / Math.pow(100 - h, 2);
      const bhnToHrc = (b: number) => {
        const disc = 4500 ** 2 + 4 * b * 1070000;
        const y = (4500 + Math.sqrt(disc)) / (2 * b);
        return 100 - y;
      };
      const bhnToHv = (b: number) => b * 0.95;
      const hvToBhn = (hv: number) => hv / 0.95;

      if (source === 1) {
        const hrc = v1;
        if (!isNaN(hrc) && hrc < 100) {
          const hv = bhnToHv(hrcToBhn(hrc));
          out.val2 = fmt(hv);
        } else out.val2 = '';
      } else {
        const hv = v2;
        if (!isNaN(hv) && hv > 0) {
          const bhn = hvToBhn(hv);
          out.val1 = fmt(bhnToHrc(bhn));
        } else out.val1 = '';
      }
      break;
    }

    case 'sus-cst': {
      const susToCst = (sus: number) => {
        if (sus <= 0) return NaN;
        if (sus < 100) return 0.226 * sus - 195 / sus;
        return 0.22 * sus - 135 / sus;
      };
      const cstToSus = (cst: number) => {
        if (cst <= 0) return NaN;
        const low = (cst + Math.sqrt(cst * cst + 4 * 0.226 * 195)) / (2 * 0.226);
        const high = (cst + Math.sqrt(cst * cst + 4 * 0.22 * 135)) / (2 * 0.22);
        return low < 100 ? low : high;
      };

      if (source === 1) {
        out.val2 = !isNaN(v1) ? fmt(susToCst(v1)) : '';
      } else {
        out.val1 = !isNaN(v2) ? fmt(cstToSus(v2)) : '';
      }
      break;
    }

    case 'swg': {
      const swgTable: Record<number, number> = {
        0: 8.23, 1: 7.62, 2: 7.01, 3: 6.4, 4: 5.89, 5: 5.38, 6: 4.88, 7: 4.47,
        8: 4.06, 9: 3.66, 10: 3.25, 11: 2.95, 12: 2.64, 13: 2.34, 14: 2.03, 15: 1.83,
        16: 1.63, 17: 1.42, 18: 1.22, 19: 1.02, 20: 0.91, 21: 0.81, 22: 0.71, 23: 0.61,
        24: 0.56, 25: 0.51, 26: 0.46, 27: 0.42, 28: 0.38, 29: 0.35, 30: 0.32, 31: 0.29,
        32: 0.27, 33: 0.25, 34: 0.23, 35: 0.21, 36: 0.19, 37: 0.17, 38: 0.15, 39: 0.14,
        40: 0.12, 41: 0.11, 42: 0.1, 43: 0.09, 44: 0.08, 45: 0.07, 46: 0.064, 47: 0.058,
        48: 0.051, 49: 0.045, 50: 0.04
      };

      const gaugeToMm = (g: number) => swgTable[Math.round(g)];
      const mmToGauge = (mm: number) => {
        let best = -1, bestDiff = Infinity;
        for (const [gStr, diam] of Object.entries(swgTable)) {
          const diff = Math.abs(mm - diam);
          if (diff < bestDiff) { bestDiff = diff; best = parseInt(gStr, 10); }
        }
        return best;
      };

      if (calc.labels.in1.toLowerCase().includes('swg')) {
        if (source === 1) out.val2 = !isNaN(v1) ? fmt(gaugeToMm(v1) ?? NaN) : '';
        else out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(mmToGauge(v2)) : '';
      } else {
        if (source === 1) out.val2 = (!isNaN(v1) && v1 > 0) ? fmt(mmToGauge(v1)) : '';
        else out.val1 = !isNaN(v2) ? fmt(gaugeToMm(v2) ?? NaN) : '';
      }
      break;
    }

    case 'cmil-dia': {
      const cmilToMm = (c: number) => 0.0254 * Math.sqrt(c);
      const mmToCmil = (mm: number) => Math.pow(mm / 0.0254, 2);

      if (source === 1) {
        out.val2 = (!isNaN(v1) && v1 >= 0) ? fmt(cmilToMm(v1)) : '';
      } else {
        out.val1 = (!isNaN(v2) && v2 >= 0) ? fmt(mmToCmil(v2)) : '';
      }
      break;
    }

    case 'cmil-swg': {
      const swgTable: Record<number, number> = {
        0: 8.23, 1: 7.62, 2: 7.01, 3: 6.4, 4: 5.89, 5: 5.38, 6: 4.88, 7: 4.47,
        8: 4.06, 9: 3.66, 10: 3.25, 11: 2.95, 12: 2.64, 13: 2.34, 14: 2.03, 15: 1.83,
        16: 1.63, 17: 1.42, 18: 1.22, 19: 1.02, 20: 0.91, 21: 0.81, 22: 0.71, 23: 0.61,
        24: 0.56, 25: 0.51, 26: 0.46, 27: 0.42, 28: 0.38, 29: 0.35, 30: 0.32, 31: 0.29,
        32: 0.27, 33: 0.25, 34: 0.23, 35: 0.21, 36: 0.19, 37: 0.17, 38: 0.15, 39: 0.14,
        40: 0.12, 41: 0.11, 42: 0.1, 43: 0.09, 44: 0.08, 45: 0.07, 46: 0.064, 47: 0.058,
        48: 0.051, 49: 0.045, 50: 0.04
      };
      const mmToSwg = (mm: number) => {
        let best = -1, bestDiff = Infinity;
        for (const [gStr, diam] of Object.entries(swgTable)) {
          const diff = Math.abs(mm - diam);
          if (diff < bestDiff) { bestDiff = diff; best = parseInt(gStr, 10); }
        }
        return best;
      };
      const swgToMm = (g: number) => swgTable[Math.round(g)];
      const cmilToMm = (c: number) => 0.0254 * Math.sqrt(c);
      const mmToCmil = (mm: number) => Math.pow(mm / 0.0254, 2);

      if (source === 1) {
        const mm = (!isNaN(v1) && v1 >= 0) ? cmilToMm(v1) : NaN;
        out.val2 = isFinite(mm) ? fmt(mmToSwg(mm)) : '';
      } else {
        const mm = (!isNaN(v2) && v2 >= 0) ? swgToMm(v2) : NaN;
        out.val1 = isFinite(mm) ? fmt(mmToCmil(mm)) : '';
      }
      break;
    }
  }

  return out;
}
