import type { Pointer } from "bun:ffi"
import { gpiod, GPIOD_LINE_DIRECTION_INPUT, GPIOD_LINE_DIRECTION_OUTPUT } from "./ffi"
import { cstr, mapPullToLibgpiod, mapEdgeToLibgpiod } from "./utils"
import type { PullMode, EdgeMode } from "./types"

type LineRequestResult = {
  chip: Pointer
  request: Pointer
}

export type InputLineConfig = {
  chipPath: string
  offset: number
  pull: PullMode
  debounce: number
  edge: EdgeMode
}

export type OutputLineConfig = {
  chipPath: string
  offset: number
  initialValue: 0 | 1
}

const cleanup = (message: string): never => {
  throw new Error(message)
}

const requestLine = (
  chipPath: string,
  offset: number,
  consumer: string,
  configureSettings: (settings: Pointer) => void
): LineRequestResult => {
  const chip = gpiod.gpiod_chip_open(cstr(chipPath))
  if (!chip) cleanup("Failed to open GPIO chip")

  const settings = gpiod.gpiod_line_settings_new()
  if (!settings) {
    gpiod.gpiod_chip_close(chip)
    cleanup("Failed to create line settings")
  }

  configureSettings(settings!)

  const lineConfig = gpiod.gpiod_line_config_new()
  if (!lineConfig) {
    gpiod.gpiod_line_settings_free(settings)
    gpiod.gpiod_chip_close(chip)
    cleanup("Failed to create line config")
  }

  const offsets = new Uint32Array([offset])
  const ret = gpiod.gpiod_line_config_add_line_settings(lineConfig, offsets, 1, settings)
  gpiod.gpiod_line_settings_free(settings)

  if (ret !== 0) {
    gpiod.gpiod_line_config_free(lineConfig)
    gpiod.gpiod_chip_close(chip)
    cleanup("Failed to add line settings")
  }

  const requestConfig = gpiod.gpiod_request_config_new()
  if (!requestConfig) {
    gpiod.gpiod_line_config_free(lineConfig)
    gpiod.gpiod_chip_close(chip)
    cleanup("Failed to create request config")
  }

  gpiod.gpiod_request_config_set_consumer(requestConfig, cstr(consumer))

  const request = gpiod.gpiod_chip_request_lines(chip, requestConfig, lineConfig)
  gpiod.gpiod_request_config_free(requestConfig)
  gpiod.gpiod_line_config_free(lineConfig)

  if (!request) {
    gpiod.gpiod_chip_close(chip)
    cleanup("Failed to request GPIO line")
  }

  return { chip: chip!, request: request! }
}

export const requestInputLine = (config: InputLineConfig): LineRequestResult => {
  return requestLine(config.chipPath, config.offset, "bun-gpio-input", (settings) => {
    gpiod.gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_INPUT)
    gpiod.gpiod_line_settings_set_bias(settings, mapPullToLibgpiod(config.pull))
    gpiod.gpiod_line_settings_set_edge_detection(settings, mapEdgeToLibgpiod(config.edge))

    if (config.debounce > 0) {
      gpiod.gpiod_line_settings_set_debounce_period_us(settings, config.debounce * 1000)
    }
  })
}

export const requestOutputLine = (config: OutputLineConfig): LineRequestResult => {
  return requestLine(config.chipPath, config.offset, "bun-gpio", (settings) => {
    gpiod.gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT)
    gpiod.gpiod_line_settings_set_output_value(settings, config.initialValue)
  })
}
