// Runs a lab's MATLAB layer in a numbl session.
//
// One session per (driver, script) pair, booted on first use and then reused:
// changing a control only rewrites params.json and re-runs main.m, which is
// fast enough to drive sliders. Editing the script needs a fresh session,
// because the script is compiled into main.m at boot.
import { createNumblSession, type BootFile, type NumblSession } from 'numbl/browser'
import type { Params, RunResult } from './types.ts'
import { LIB_FILES } from './lib.ts'

function bootFiles(driver: string, script: string): BootFile[] {
  return [
    ...LIB_FILES,
    { path: 'main.m', content: `${driver}\n${script}` },
    // params.json is overwritten before every run; it only needs to exist so
    // that the first fileread has something to find
    { path: 'params.json', content: '{}' },
  ]
}

export class Engine {
  private session: NumblSession | null = null
  private bootedFor: string | null = null
  private queue: Promise<unknown> = Promise.resolve()
  private chunks: string[] = []

  /** Resolves when the run finishes; runs are serialised in call order. */
  run<T>(driver: string, script: string, params: Params): Promise<RunResult<T>> {
    const task = this.queue.then(
      () => this.exec<T>(driver, script, params),
      () => this.exec<T>(driver, script, params),
    )
    this.queue = task.catch(() => undefined)
    return task
  }

  private async exec<T>(driver: string, script: string, params: Params): Promise<RunResult<T>> {
    const t0 = performance.now()
    const fail = (error: string): RunResult<T> => ({
      ok: false,
      error,
      output: this.chunks.join(''),
      ms: performance.now() - t0,
    })

    const key = `${driver} ${script}`
    try {
      if (!this.session || this.bootedFor !== key) {
        this.session?.dispose()
        this.session = null
        this.bootedFor = null
        this.chunks = []
        const session = await createNumblSession({
          files: bootFiles(driver, script),
          mip: false,
          persistSystem: false,
          optimization: '1',
          onOutput: (text) => {
            this.chunks.push(text)
          },
        })
        this.session = session
        this.bootedFor = key
      }

      this.chunks = []
      this.session.writeFile('params.json', JSON.stringify(params))
      // run('main.m'), not `main;`. numbl (0.4.18) mis-binds the arguments of a
      // script's local functions when the script is invoked by name from the
      // REPL, which is what session.execute gives us.
      const res = await this.session.execute("run('main.m');")
      if (!res.ok) return fail(res.error ?? 'the script failed')

      const bytes = await this.session.readFile('out.json')
      return {
        ok: true,
        data: JSON.parse(new TextDecoder().decode(bytes)) as T,
        output: this.chunks.join(''),
        ms: performance.now() - t0,
      }
    } catch (err) {
      // a boot failure leaves nothing usable behind
      this.session?.dispose()
      this.session = null
      this.bootedFor = null
      return fail(err instanceof Error ? err.message : String(err))
    }
  }

  dispose() {
    this.session?.dispose()
    this.session = null
    this.bootedFor = null
  }
}
