/**
 * Spec Reader - Parses QuickBASIC binary DAT spec files
 *
 * Reads model specification data from 4 product family DAT files:
 *   5BMAIN.DAT  (SCM5B family, 160 bytes/record)
 *   8BMAIN.DAT  (8B family, 163 bytes/record)
 *   DSCOUT.DAT  (DSCA family, 163 bytes/record)
 *   SCTMAIN.DAT (DSCT family, 121 bytes/record)
 *
 * These are QuickBASIC random-access files using TYPE (struct) records.
 * All values are little-endian: SINGLE = IEEE 754 float (4 bytes),
 * INTEGER = signed 16-bit (2 bytes), STRING * N = fixed-width ASCII.
 */

const fs = require('fs');
const path = require('path');

// Default spec data directory
const DEFAULT_SPEC_DIR = path.join(__dirname, '..', 'specdata');

// --------------------------------------------------------------------------
// Binary read helpers
// --------------------------------------------------------------------------

function readString(buf, offset, length) {
    return buf.toString('ascii', offset, offset + length).replace(/\0/g, '').trim();
}

function readSingle(buf, offset) {
    return buf.readFloatLE(offset);
}

function readInteger(buf, offset) {
    return buf.readInt16LE(offset);
}

// --------------------------------------------------------------------------
// TYPE definitions (field name, type, size)
// --------------------------------------------------------------------------

const FIELD_TYPES = {
    STRING17: { size: 17, read: (buf, off) => readString(buf, off, 17) },
    STRING9:  { size: 9,  read: (buf, off) => readString(buf, off, 9) },
    STRING15: { size: 15, read: (buf, off) => readString(buf, off, 15) },
    STRING14: { size: 14, read: (buf, off) => readString(buf, off, 14) },
    STRING13: { size: 13, read: (buf, off) => readString(buf, off, 13) },
    STRING7:  { size: 7,  read: (buf, off) => readString(buf, off, 7) },
    SINGLE:   { size: 4,  read: (buf, off) => readSingle(buf, off) },
    INTEGER:  { size: 2,  read: (buf, off) => readInteger(buf, off) },
};

const S15 = 'STRING15';
const S14 = 'STRING14';
const S13 = 'STRING13';
const S7  = 'STRING7';
const SNG = 'SINGLE';
const INT = 'INTEGER';

// SCM5B: 160 bytes/record
const SCM5B_FIELDS = [
    ['MODNAME',      S15], ['SENTYPE',      S7],
    ['ISMAXNEXCL',   SNG], ['ISMAXFEXCL',   SNG], ['OUTRES',       SNG],
    ['MININ',        SNG], ['MAXIN',        SNG], ['IEXC',         SNG],
    ['RCONV',        SNG], ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['OSCALPT',      SNG], ['GNCALPT',      SNG], ['CALTOL',       SNG],
    ['VEXC',         SNG], ['VEXCACC',      SNG], ['EXCLOAD',      SNG],
    ['EXCLOADREG',   SNG], ['EXCIMAX',      SNG], ['LINEAR',       SNG],
    ['ACCURACY',     SNG], ['TESTFREQ',     SNG], ['ATTEN',        SNG],
    ['ATTENTOL',     SNG], ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['PSS',          SNG], ['OUTNOISE',     SNG], ['INPUTRES',     SNG],
    ['VOPENINMIN',   SNG], ['VOPENINMAX',   SNG], ['LEADRERR',     SNG],
    ['LINEARIZED',   INT], ['OSCALIN',      SNG], ['GNCALIN',      SNG],
    ['BANDWIDTH',    SNG], ['IMATCHTOL',    SNG],
];

// 8B: 163 bytes/record (no OUTRES, has OUTSIGTYPE)
const B8_FIELDS = [
    ['MODNAME',      S15], ['SENTYPE',      S7],
    ['ISMAXNEXCL',   SNG], ['ISMAXFEXCL',   SNG],
    ['MININ',        SNG], ['MAXIN',        SNG], ['IEXC',         SNG],
    ['RCONV',        SNG], ['OUTSIGTYPE',   S7],
    ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['OSCALPT',      SNG], ['GNCALPT',      SNG], ['CALTOL',       SNG],
    ['VEXC',         SNG], ['VEXCACC',      SNG], ['EXCLOAD',      SNG],
    ['EXCLOADREG',   SNG], ['EXCIMAX',      SNG], ['LINEAR',       SNG],
    ['ACCURACY',     SNG], ['TESTFREQ',     SNG], ['ATTEN',        SNG],
    ['ATTENTOL',     SNG], ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['PSS',          SNG], ['OUTNOISE',     SNG], ['INPUTRES',     SNG],
    ['VOPENINMIN',   SNG], ['VOPENINMAX',   SNG], ['LEADRERR',     SNG],
    ['LINEARIZED',   INT], ['OSCALIN',      SNG], ['GNCALIN',      SNG],
    ['BANDWIDTH',    SNG], ['IMATCHTOL',    SNG],
];

// DSCA: 163 bytes/record
const DSCA_FIELDS = [
    ['MODNAME',      S13], ['SENTYPE',      S7],
    ['ISMAXNL',      SNG], ['ISMAXFL',      SNG],
    ['MININ',        SNG], ['MAXIN',        SNG], ['RCONV',        SNG],
    ['MINOUT',       SNG], ['MAXOUT',       SNG], ['OUTSIGTYPE',   S7],
    ['OSCALIN',      SNG], ['GNCALIN',      SNG], ['CALTOL',       SNG],
    ['LOAD1',        SNG], ['LINEAR1',      SNG], ['ACCURACY1',    SNG],
    ['LOAD2',        SNG], ['LINEAR2',      SNG], ['ACCURACY2',    SNG],
    ['LOAD3',        SNG], ['LINEAR3',      SNG], ['ACCURACY3',    SNG],
    ['BANDWIDTH',    SNG], ['TESTFREQ',     SNG], ['ATTEN',        SNG],
    ['ATTENTOL',     SNG], ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['PSS',          SNG], ['OUTNOISE',     SNG], ['INPUTRES',     SNG],
    ['COMPLIANCE',   SNG], ['MAXLOAD',      SNG], ['ILIMIT',       SNG],
    ['PERCOVER',     SNG], ['MINVS',        SNG], ['MAXVS',        SNG],
];

// DSCT: 121 bytes/record (uses INTEGER for some fields)
const DSCT_FIELDS = [
    ['MODNAME',      S14], ['SENTYPE',      S7],
    ['MININ',        SNG], ['MAXIN',        SNG],
    ['IEXCMFS',      SNG], ['IEXCPFS',     SNG],
    ['RCONV',        SNG], ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['OSCALPT',      SNG], ['GNCALPT',      SNG],
    ['LINEAR',       SNG], ['ACCURACY',     SNG],
    ['TESTFREQ',     INT], ['ATTEN',        INT], ['ATTENTOL',     INT],
    ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['OUTNOISE',     SNG], ['INPUTRES',     SNG],
    ['IOPENTC',      SNG], ['LEADRERR',     SNG],
    ['LINEARIZED',   INT], ['OSCALIN',      SNG], ['GNCALIN',      SNG],
    ['BANDWIDTH',    SNG], ['IMATCHTOL',    SNG],
    ['CALTOL',       SNG], ['VSEN',         SNG],
];

const S9  = 'STRING9';

// SCM5B45: 119 bytes/record (frequency/counter modules)
const SCM5B45_FIELDS = [
    ['MODNAME',      S9],
    ['MININ',        SNG], ['MAXIN',        SNG], ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['ZHYSAMPL',     SNG], ['ZHYSLIM',      SNG], ['TTLHYSAMPL',  SNG],
    ['TTLLIMHI',     SNG], ['TTLLIMLO',     SNG], ['MINPW',        SNG],
    ['OSCALIN',      SNG], ['GNCALIN',      SNG], ['CALTOL',       SNG],
    ['LINEAR',       SNG], ['ACCURACY',     SNG],
    ['TESTFREQ',     INT], ['ATTEN',        INT], ['ATTENTOL',     INT],
    ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['ISMAX',        SNG], ['PSS',          SNG],
    ['NOISEMFS',     SNG], ['NOISETESTPT',  SNG], ['NOISEPFS',     SNG],
    ['OUTRES',       SNG], ['EXCVOLT',      SNG],
    ['EXCTOLNL',     SNG], ['EXCTOLL',      SNG],
];

// SCM5B48: 264 bytes/record (multi-bandwidth modules)
const SCM5B48_FIELDS = [
    ['MODNAME',      S15], ['SENTYPE',      S7],
    ['ISMAXNEXCL',   SNG], ['ISMAXFEXCL',   SNG], ['OUTRES',       SNG],
    ['MININ',        SNG], ['MAXIN',        SNG],
    ['MININ1',       SNG], ['MAXIN1',       SNG],
    ['MININ2',       SNG], ['MAXIN2',       SNG],
    ['MININ3',       SNG], ['MAXIN3',       SNG],
    ['IEXC',         SNG], ['IEXC1',        SNG], ['IEXC2',        SNG],
    ['RCONV',        SNG], ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['OSCALPT',      SNG], ['GNCALPT',      SNG], ['CALTOL',       SNG],
    ['VEXC',         SNG], ['VEXCACC',      SNG], ['EXCLOAD',      SNG],
    ['EXCLOADREG',   SNG], ['EXCIMAX',      SNG],
    ['LINEAR',       SNG], ['ACCURACY',     SNG],
    ['TESTFREQ',     SNG], ['TESTFREQ1',    SNG], ['TESTFREQ2',    SNG], ['TESTFREQ3',    SNG], ['TESTFREQ4',    SNG],
    ['ATTEN',        SNG], ['ATTEN1',       SNG], ['ATTEN2',       SNG], ['ATTEN3',       SNG], ['ATTEN4',       SNG],
    ['ATTENTOL',     SNG],
    ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['PSS',          SNG], ['PSS1',         SNG], ['PSS2',         SNG], ['PSS3',         SNG],
    ['OUTNOISE',     SNG], ['OUTNOISE1',    SNG], ['OUTNOISE2',    SNG], ['OUTNOISE3',    SNG],
    ['INPUTRES',     SNG], ['VOPENINMIN',   SNG], ['VOPENINMAX',   SNG], ['LEADRERR',     SNG],
    ['LINEARIZED',   INT],
    ['OSCALIN',      SNG], ['GNCALIN',      SNG],
    ['BANDWIDTH',    SNG], ['BANDWIDTH1',   SNG], ['BANDWIDTH2',   SNG], ['BANDWIDTH3',   SNG], ['BANDWIDTH4',   SNG],
    ['IMATCHTOL',    SNG],
];

// SCM5B49: 93 bytes/record (sample & hold modules)
const SCM5B49_FIELDS = [
    ['MODNAME',      S9],
    ['MININ',        SNG], ['MAXIN',        SNG], ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['MAXSUPPLYNL',  SNG], ['MAXSUPPLYFL',  SNG], ['LIMITOUT',     SNG], ['POWERSEN',     SNG],
    ['TESTFREQ',     INT], ['ATTEN',        INT],
    ['LINEAR0MA',    SNG], ['LINEAR50MA',   SNG],
    ['ACCURACY0MA',  SNG], ['ACCURACY50MA', SNG],
    ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['NOISEOUT',     SNG], ['QINJECT',      SNG],
    ['INPUTRES',     SNG], ['ACQLIM',       SNG],
    ['DROOP',        SNG], ['PERCOVER',     SNG],
];

// DSCA (TSTDIN1B variant, for DSCMAIN4.DAT): 159 bytes/record
const DSCA_DIN_FIELDS = [
    ['MODNAME',      S13], ['SENTYPE',      S7],
    ['ISMAXNEXCL',   SNG], ['ISMAXFEXCL',   SNG],
    ['MININ',        SNG], ['MAXIN',        SNG],
    ['IEXCPFS',      SNG], ['IEXCMFS',      SNG],
    ['RCONV',        SNG], ['OUTSIGTYPE',   S7],
    ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['OSCALPT',      SNG], ['GNCALPT',      SNG], ['CALTOL',       SNG],
    ['VEXC',         SNG], ['VEXCACC',      SNG], ['EXCLOAD',      SNG],
    ['EXCLOADREG',   SNG], ['EXCIMAX',      SNG],
    ['LINEAR',       SNG], ['ACCURACY',     SNG],
    ['TESTFREQ',     INT], ['ATTEN',        INT], ['ATTENTOL',     INT],
    ['STEPRMIN',     SNG], ['STEPRMAX',     SNG],
    ['PSS',          SNG], ['OUTNOISE',     SNG], ['INPUTRES',     SNG],
    ['OPENTC',       SNG], ['LEADRERR',     SNG],
    ['LINEARIZED',   INT], ['OSCALIN',      SNG], ['GNCALIN',      SNG],
    ['BANDWIDTH',    SNG], ['MINVS',        SNG], ['MAXVS',        SNG],
];

// SCM7B: 170 bytes/record
const S17 = 'STRING17';
const SCM7B_FIELDS = [
    ['MODNAME',      S17], ['SENTYPE',      S7],
    ['MINVS',        SNG], ['NOMVS',        SNG], ['MAXVS',        SNG],
    ['VLIM',         SNG], ['ILIM',         SNG], ['PE',           SNG],
    ['ISMAXNEXCL',   SNG],
    ['MININ',        SNG], ['MAXIN',        SNG],
    ['MINOUT',       SNG], ['MAXOUT',       SNG],
    ['IEXC',         SNG], ['EXCIMIN',      SNG], ['EXCIMAX',      SNG],
    ['LEADRERR',     SNG], ['RCONV',        SNG],
    ['OSCALPT',      SNG], ['GNCALPT',      SNG], ['CALTOL',       SNG],
    ['ISMAXFEXCL',   SNG],
    ['VEXC',         SNG], ['VEXCLO',       SNG], ['VEXCHI',       SNG],
    ['LOOPIMAX',     SNG],
    ['LINEAR',       SNG], ['ACCURACY',     SNG], ['PSS',          SNG],
    ['TESTFREQ',     INT], ['ATTEN',        INT], ['ATTENTOL',     INT],
    ['STEPRESP',     SNG], ['STEPTOL',      SNG],
    ['OUTNOISERMS',  SNG], ['OUTNOISEVPK',  SNG],
    ['INPUTRES',     SNG], ['VOPENTC',      SNG],
    ['CJCACC',       SNG], ['IBIAS',        SNG],
];

// --------------------------------------------------------------------------
// Record size calculation
// --------------------------------------------------------------------------

function calcRecordSize(fields) {
    let size = 0;
    for (const [, type] of fields) {
        size += FIELD_TYPES[type].size;
    }
    return size;
}

// --------------------------------------------------------------------------
// Parse a single record from a buffer
// --------------------------------------------------------------------------

function parseRecord(buf, offset, fields) {
    const record = {};
    let pos = offset;
    for (const [name, type] of fields) {
        const ft = FIELD_TYPES[type];
        record[name] = ft.read(buf, pos);
        pos += ft.size;
    }
    return record;
}

// --------------------------------------------------------------------------
// Parse an entire DAT file into an array of records
// --------------------------------------------------------------------------

function parseDatFile(filePath, fields) {
    if (!fs.existsSync(filePath)) {
        console.error(`Spec file not found: ${filePath}`);
        return [];
    }

    const buf = fs.readFileSync(filePath);
    const recordSize = calcRecordSize(fields);
    const numRecords = Math.floor(buf.length / recordSize);
    const records = [];

    for (let i = 0; i < numRecords; i++) {
        const offset = i * recordSize;
        if (offset + recordSize > buf.length) break;

        const record = parseRecord(buf, offset, fields);

        // Skip records with empty, placeholder, or corrupted model names
        const modname = record.MODNAME;
        if (!modname || modname.length === 0) continue;
        // Skip if model name contains non-alphanumeric characters (except dash)
        if (!/^[A-Za-z0-9-]+$/.test(modname)) continue;
        // Skip placeholder entries
        if (/^[XYZ]+$/.test(modname) || /^ZZZZ/.test(modname)) continue;
        // Skip if MODNAME doesn't start with a known product prefix
        const upper = modname.toUpperCase();
        if (!upper.match(/^(SCM5B|5B|SCM7B|7B|8B|DSCA|DSCT|SCT|BOGUS)/)) continue;

        records.push(record);
    }

    return records;
}

// --------------------------------------------------------------------------
// Family configuration
// --------------------------------------------------------------------------

const FAMILIES = {
    SCM5B: {
        file: '5BMAIN.DAT',
        fields: SCM5B_FIELDS,
        family: 'SCM5B',
        logType: '5BLOG',
    },
    B8: {
        file: '8BMAIN.DAT',
        fields: B8_FIELDS,
        family: '8B',
        logType: '8BLOG',
    },
    DSCA: {
        file: 'DSCOUT.DAT',
        fields: DSCA_FIELDS,
        family: 'DSCA',
        logType: 'DSCLOG',
    },
    DSCT: {
        file: 'SCTMAIN.DAT',
        fields: DSCT_FIELDS,
        family: 'DSCT',
        logType: 'SCTLOG',
    },
    DSCA_DIN: {
        file: 'DSCMAIN4.DAT',
        fields: DSCA_DIN_FIELDS,
        family: 'DSCA',
        logType: 'DSCLOG',
    },
    SCM5B45: {
        file: '5B45DATA.DAT',
        fields: SCM5B45_FIELDS,
        family: 'SCM5B',
        logType: '5BLOG',
    },
    SCM5B48: {
        file: 'DB5B48.DAT',
        fields: SCM5B48_FIELDS,
        family: 'SCM5B',
        logType: '5BLOG',
    },
    SCM5B49: {
        file: '5B49_2.DAT',
        fields: SCM5B49_FIELDS,
        family: 'SCM5B',
        logType: '5BLOG',
    },
    SCM7B: {
        file: '7BMAIN.DAT',
        fields: SCM7B_FIELDS,
        family: 'SCM7B',
        logType: '7BLOG',
    },
};

// --------------------------------------------------------------------------
// Main API: load all specs into a lookup map
// --------------------------------------------------------------------------

/**
 * Load all model specs from binary DAT files.
 * @param {string} specDir - Directory containing the DAT files
 * @returns {Map<string, object>} Map of model_number -> spec record (with _family added)
 */
function loadAllSpecs(specDir) {
    specDir = specDir || DEFAULT_SPEC_DIR;
    const specMap = new Map();

    for (const [familyKey, config] of Object.entries(FAMILIES)) {
        const filePath = path.join(specDir, config.file);
        const records = parseDatFile(filePath, config.fields);

        for (const record of records) {
            record._family = config.family;
            record._logType = config.logType;
            // Normalize model name for lookup (trim, uppercase)
            const key = record.MODNAME.toUpperCase().trim();
            specMap.set(key, record);
        }

        console.log(`[SPEC] Loaded ${records.length} models from ${config.file} (${config.family})`);
    }

    console.log(`[SPEC] Total models loaded: ${specMap.size}`);
    return specMap;
}

/**
 * Look up specs for a model number.
 * Tries exact match, then common prefix variations (SCM5B <-> 5B, DSCA <-> DSC).
 * @param {Map} specMap - Spec map from loadAllSpecs()
 * @param {string} modelNumber - Model number to look up
 * @returns {object|null} Spec record or null
 */
function getSpecs(specMap, modelNumber) {
    if (!modelNumber) return null;
    const key = modelNumber.toUpperCase().trim();

    // Exact match
    if (specMap.has(key)) return specMap.get(key);

    // Try adding/removing SCM prefix: "5B41-03" <-> "SCM5B41-03"
    if (key.startsWith('SCM5B')) {
        const short = key.replace('SCM5B', '5B');
        if (specMap.has(short)) return specMap.get(short);
    } else if (key.startsWith('5B')) {
        const full = 'SCM' + key;
        if (specMap.has(full)) return specMap.get(full);
    }

    // Try adding/removing SCM prefix for 7B
    if (key.startsWith('SCM7B')) {
        const short = key.replace('SCM7B', '7B');
        if (specMap.has(short)) return specMap.get(short);
    } else if (key.startsWith('7B')) {
        const full = 'SCM' + key;
        if (specMap.has(full)) return specMap.get(full);
    }

    // Try DSCA variations
    if (key.startsWith('DSCA')) {
        // Some specs stored without the 'A'
        const short = key.replace('DSCA', 'DSC');
        if (specMap.has(short)) return specMap.get(short);
    }

    // Try partial match on model base (before any suffix like C, D)
    // e.g., "DSCA30-05C" -> try "DSCA30-05"
    const baseMatch = key.match(/^(.+?)([A-Z])$/);
    if (baseMatch) {
        const base = baseMatch[1];
        if (specMap.has(base)) return specMap.get(base);
        // Also try with prefix variations
        if (base.startsWith('SCM5B')) {
            const short = base.replace('SCM5B', '5B');
            if (specMap.has(short)) return specMap.get(short);
        } else if (base.startsWith('5B')) {
            if (specMap.has('SCM' + base)) return specMap.get('SCM' + base);
        }
    }

    return null;
}

/**
 * Determine product family from model number string
 */
function getFamily(modelNumber) {
    if (!modelNumber) return null;
    const m = modelNumber.toUpperCase();
    if (m.startsWith('SCM5B') || m.startsWith('5B')) return 'SCM5B';
    if (m.startsWith('SCM7B') || m.startsWith('7B')) return 'SCM7B';
    if (m.startsWith('8B')) return '8B';
    if (m.startsWith('DSCA')) return 'DSCA';
    if (m.startsWith('DSCT') || m.startsWith('SCT')) return 'DSCT';
    return null;
}

// --------------------------------------------------------------------------
// CLI: test the parser
// --------------------------------------------------------------------------

if (require.main === module) {
    const specDir = process.argv[2] || DEFAULT_SPEC_DIR;
    console.log(`Loading specs from: ${specDir}\n`);

    const specMap = loadAllSpecs(specDir);

    // Print a few examples from each family
    const examples = {};
    for (const [key, spec] of specMap) {
        const fam = spec._family;
        if (!examples[fam]) examples[fam] = [];
        if (examples[fam].length < 3) {
            examples[fam].push(spec);
        }
    }

    for (const [fam, specs] of Object.entries(examples)) {
        console.log(`\n--- ${fam} Examples ---`);
        for (const s of specs) {
            console.log(`  ${s.MODNAME}: SENTYPE=${s.SENTYPE}, MININ=${s.MININ}, MAXIN=${s.MAXIN}, MINOUT=${s.MINOUT}, MAXOUT=${s.MAXOUT}`);
        }
    }
}

module.exports = { loadAllSpecs, getSpecs, getFamily, FAMILIES };
