/**
 * dna_webgpu.js — WebGPU Host: Braid Sort QUBO Solver
 *
 * Runs in browser or Node.js with WebGPU support.
 * Loads the braid sort compute shader and dispatches it.
 *
 * The smuggle:
 *   1. CPU: encode QUBO → packed base-8 DNA sequences
 *   2. GPU: braid sort (triangle math compute shader)
 *   3. CPU: read sorted indices → optimal solution
 *
 * Zero-copy: sequences live in GPU buffer the entire time.
 * The CPU writes once, the GPU sorts, the CPU reads once.
 */

class DNABraidSolver {
    constructor() {
        this.device = null;
        this.pipeline = null;
        this.bindGroupLayout = null;
    }

    async init() {
        // Request WebGPU adapter and device
        if (!navigator.gpu) {
            throw new Error('WebGPU not supported');
        }

        const adapter = await navigator.gpu.requestAdapter();
        if (!adapter) {
            throw new Error('No WebGPU adapter found');
        }

        this.device = await adapter.requestDevice();

        // Load shader
        const shaderCode = await fetch('dna_braid.wgsl').then(r => r.text());
        const shaderModule = this.device.createShaderModule({
            code: shaderCode,
        });

        // Create bind group layout
        this.bindGroupLayout = this.device.createBindGroupLayout({
            entries: [
                { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
                { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
                { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
                { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
            ],
        });

        // Create pipelines for each kernel
        const layout = this.device.createPipelineLayout({
            bindGroupLayouts: [this.bindGroupLayout],
        });

        this.pipelines = {
            extractDigits: this.device.createComputePipeline({
                layout,
                compute: { module: shaderModule, entryPoint: 'extract_digits' },
            }),
            braidSortOdd: this.device.createComputePipeline({
                layout,
                compute: { module: shaderModule, entryPoint: 'braid_sort_odd' },
            }),
            braidSortEven: this.device.createComputePipeline({
                layout,
                compute: { module: shaderModule, entryPoint: 'braid_sort_even' },
            }),
            eigensolidCheck: this.device.createComputePipeline({
                layout,
                compute: { module: shaderModule, entryPoint: 'eigensolid_check' },
            }),
        };
    }

    /**
     * Encode QUBO solutions as packed base-8 DNA sequences.
     * Each sequence is a u32 with seq_length base-8 digits (3 bits each).
     */
    encodeSolutions(Q, nVars) {
        const n = 1 << nVars; // 2^n
        const seqLength = Math.ceil(Math.log(n) / Math.log(8));

        // Compute all energies
        const energies = new Float64Array(n);
        const solutions = new Uint32Array(n);

        for (let i = 0; i < n; i++) {
            // Decode binary vector
            const x = [];
            for (let j = 0; j < nVars; j++) {
                x.push((i >> j) & 1);
            }

            // Compute QUBO energy
            let energy = 0;
            for (let a = 0; a < nVars; a++) {
                for (let b = 0; b < nVars; b++) {
                    energy += Q[a][b] * x[a] * x[b];
                }
            }
            energies[i] = energy;
        }

        // Sort by energy (monotone encoding)
        const indices = Array.from({ length: n }, (_, i) => i);
        indices.sort((a, b) => energies[a] - energies[b]);

        // Assign packed base-8 sequences in energy order
        const sequences = new Uint32Array(n);
        for (let rank = 0; rank < n; rank++) {
            sequences[rank] = rank; // base-8 encoding of rank
        }

        return { sequences, indices: new Uint32Array(indices), nVars, seqLength, n };
    }

    /**
     * Run the braid sort on GPU.
     * Returns sorted indices (first index = optimal solution).
     */
    async solve(sequences, n, seqLength) {
        const workgroupSize = 256;
        const numWorkgroups = Math.ceil(n / workgroupSize);

        // Create GPU buffers (zero-copy: CPU writes directly)
        const sequencesBuffer = this.device.createBuffer({
            size: n * 4,
            usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
            mappedAtCreation: true,
        });
        new Uint32Array(sequencesBuffer.getMappedRange()).set(sequences);
        sequencesBuffer.unmap();

        const indicesBuffer = this.device.createBuffer({
            size: n * 4,
            usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
            mappedAtCreation: true,
        });
        // Initialize indices: 0, 1, 2, ..., n-1
        const indicesArray = new Uint32Array(n);
        for (let i = 0; i < n; i++) indicesArray[i] = i;
        new Uint32Array(indicesBuffer.getMappedRange()).set(indicesArray);
        indicesBuffer.unmap();

        const scratchBuffer = this.device.createBuffer({
            size: n * 4,
            usage: GPUBufferUsage.STORAGE,
        });

        const paramsBuffer = this.device.createBuffer({
            size: 16,
            usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
        });

        // Create bind group
        const bindGroup = this.device.createBindGroup({
            layout: this.bindGroupLayout,
            entries: [
                { binding: 0, resource: { buffer: sequencesBuffer } },
                { binding: 1, resource: { buffer: indicesBuffer } },
                { binding: 2, resource: { buffer: scratchBuffer } },
                { binding: 3, resource: { buffer: paramsBuffer } },
            ],
        });

        // Run radix sort: one pass per digit (LSD)
        for (let pass = 0; pass < seqLength; pass++) {
            // Set parameters
            const params = new Uint32Array([n, seqLength, pass, 0]);
            this.device.queue.writeBuffer(paramsBuffer, 0, params);

            // Extract digits
            const encoder = this.device.createCommandEncoder();
            const passEncoder = encoder.beginComputePass();
            passEncoder.setPipeline(this.pipelines.extractDigits);
            passEncoder.setBindGroup(0, bindGroup);
            passEncoder.dispatchWorkgroups(numWorkgroups);
            passEncoder.end();
            this.device.queue.submit([encoder.finish()]);

            // Braid sort (odd-even transposition)
            for (let i = 0; i < n; i++) {
                const encoder = this.device.createCommandEncoder();
                const passEncoder = encoder.beginComputePass();

                if (i % 2 === 0) {
                    passEncoder.setPipeline(this.pipelines.braidSortOdd);
                } else {
                    passEncoder.setPipeline(this.pipelines.braidSortEven);
                }

                passEncoder.setBindGroup(0, bindGroup);
                passEncoder.dispatchWorkgroups(numWorkgroups);
                passEncoder.end();
                this.device.queue.submit([encoder.finish()]);
            }
        }

        // Read back sorted indices
        const readBuffer = this.device.createBuffer({
            size: n * 4,
            usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
        });

        const encoder = this.device.createCommandEncoder();
        encoder.copyBufferToBuffer(indicesBuffer, 0, readBuffer, 0, n * 4);
        this.device.queue.submit([encoder.finish()]);

        await readBuffer.mapAsync(GPUMapMode.READ);
        const result = new Uint32Array(readBuffer.getMappedRange()).slice();
        readBuffer.unmap();

        return result;
    }

    /**
     * Full solve pipeline: encode → GPU sort → decode.
     */
    async solveQUBO(Q, nVars) {
        const t0 = performance.now();

        // Encode
        const { sequences, indices, seqLength, n } = this.encodeSolutions(Q, nVars);
        const tEncode = performance.now() - t0;

        // GPU sort
        const t1 = performance.now();
        const sortedIndices = await this.solve(sequences, n, seqLength);
        const tSort = performance.now() - t1;

        // Decode: first sorted index is the optimal solution
        const optimalRank = sortedIndices[0];
        const optimalIdx = indices[optimalRank];

        // Reconstruct solution vector
        const x = [];
        for (let j = 0; j < nVars; j++) {
            x.push((optimalIdx >> j) & 1);
        }

        // Compute energy
        let energy = 0;
        for (let a = 0; a < nVars; a++) {
            for (let b = 0; b < nVars; b++) {
                energy += Q[a][b] * x[a] * x[b];
            }
        }

        return {
            solution: x,
            energy,
            nSolutions: n,
            encodeTime: tEncode,
            sortTime: tSort,
            totalTime: performance.now() - t0,
        };
    }
}

// ============================================================
// DEMO
// ============================================================

async function demo() {
    console.log('='.repeat(60));
    console.log('DNA Braid Sort: WebGPU QUBO Solver');
    console.log('='.repeat(60));

    const solver = new DNABraidSolver();
    await solver.init();
    console.log('WebGPU initialized');

    // Generate a banded QUBO
    const nVars = 12;
    const Q = Array.from({ length: nVars }, () => Array(nVars).fill(0));
    const rng = mulberry32(42);

    for (let i = 0; i < nVars; i++) {
        Q[i][i] = 2 + rng() * 6;
        if (i + 1 < nVars) {
            const c = -(0.5 + rng() * 2.5);
            Q[i][i + 1] = c;
            Q[i + 1][i] = c;
        }
    }

    console.log(`\nQUBO: ${nVars} variables, 2^${nVars} = ${(1 << nVars).toLocaleString()} solutions`);

    const result = await solver.solveQUBO(Q, nVars);

    console.log(`\nOptimal: x=[${result.solution}], E=${result.energy.toFixed(4)}`);
    console.log(`Encode:  ${result.encodeTime.toFixed(1)}ms`);
    console.log(`Sort:    ${result.sortTime.toFixed(1)}ms`);
    console.log(`Total:   ${result.totalTime.toFixed(1)}ms`);
}

// Simple PRNG
function mulberry32(seed) {
    return function() {
        seed |= 0; seed = seed + 0x6D2B79F5 | 0;
        let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
        t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
        return ((t ^ t >>> 14) >>> 0) / 4294967296;
    };
}

// Run demo if in browser
if (typeof window !== 'undefined') {
    demo().catch(console.error);
}

// Export for Node.js
if (typeof module !== 'undefined') {
    module.exports = { DNABraidSolver };
}
