local ffi = require("ffi")
local loaded, lib = pcall(ffi.load, "opus")

if not loaded then return nil, lib end

local new, typeof, gc = ffi.new, ffi.typeof, ffi.gc
ffi.cdef[[
typedef int16_t opus_int16;
typedef int32_t opus_int32;
typedef uint16_t opus_uint16;
typedef uint32_t opus_uint32;

typedef struct OpusEncoder OpusEncoder;
typedef struct OpusDecoder OpusDecoder;

const char *opus_strerror(int error);
const char *opus_get_version_string(void);

OpusEncoder *opus_encoder_create(opus_int32 Fs, int channels, int application, int *error);
int opus_encoder_init(OpusEncoder *st, opus_int32 Fs, int channels, int application);
int opus_encoder_get_size(int channels);
int opus_encoder_ctl(OpusEncoder *st, int request, ...);
void opus_encoder_destroy(OpusEncoder *st);

opus_int32 opus_encode(
	OpusEncoder *st,
	const opus_int16 *pcm,
	int frame_size,
	unsigned char *data,
	opus_int32 max_data_bytes
);

opus_int32 opus_encode_float(
	OpusEncoder *st,
	const float *pcm,
	int frame_size,
	unsigned char *data,
	opus_int32 max_data_bytes
);

OpusDecoder *opus_decoder_create(opus_int32 Fs, int channels, int *error);
int opus_decoder_init(OpusDecoder *st, opus_int32 Fs, int channels);
int opus_decoder_get_size(int channels);
int opus_decoder_ctl(OpusDecoder *st, int request, ...);
void opus_decoder_destroy(OpusDecoder *st);

int opus_decode(
	OpusDecoder *st,
	const unsigned char *data,
	opus_int32 len,
	opus_int16 *pcm,
	int frame_size,
	int decode_fec
);

int opus_decode_float(
	OpusDecoder *st,
	const unsigned char *data,
	opus_int32 len,
	float *pcm,
	int frame_size,
	int decode_fec
);
]]
local opus = {}
opus.OK = 0
opus.BAD_ARG = -1
opus.BUFFER_TOO_SMALL = -2
opus.INTERNAL_ERROR = -3
opus.INVALID_PACKET = -4
opus.UNIMPLEMENTED = -5
opus.INVALID_STATE = -6
opus.ALLOC_FAIL = -7
opus.APPLICATION_VOIP = 2048
opus.APPLICATION_AUDIO = 2049
opus.APPLICATION_RESTRICTED_LOWDELAY = 2051
opus.AUTO = -1000
opus.BITRATE_MAX = -1
opus.SIGNAL_VOICE = 3001
opus.SIGNAL_MUSIC = 3002
opus.BANDWIDTH_NARROWBAND = 1101
opus.BANDWIDTH_MEDIUMBAND = 1102
opus.BANDWIDTH_WIDEBAND = 1103
opus.BANDWIDTH_SUPERWIDEBAND = 1104
opus.BANDWIDTH_FULLBAND = 1105
opus.SET_APPLICATION_REQUEST = 4000
opus.GET_APPLICATION_REQUEST = 4001
opus.SET_BITRATE_REQUEST = 4002
opus.GET_BITRATE_REQUEST = 4003
opus.SET_MAX_BANDWIDTH_REQUEST = 4004
opus.GET_MAX_BANDWIDTH_REQUEST = 4005
opus.SET_VBR_REQUEST = 4006
opus.GET_VBR_REQUEST = 4007
opus.SET_BANDWIDTH_REQUEST = 4008
opus.GET_BANDWIDTH_REQUEST = 4009
opus.SET_COMPLEXITY_REQUEST = 4010
opus.GET_COMPLEXITY_REQUEST = 4011
opus.SET_INBAND_FEC_REQUEST = 4012
opus.GET_INBAND_FEC_REQUEST = 4013
opus.SET_PACKET_LOSS_PERC_REQUEST = 4014
opus.GET_PACKET_LOSS_PERC_REQUEST = 4015
opus.SET_DTX_REQUEST = 4016
opus.GET_DTX_REQUEST = 4017
opus.SET_VBR_CONSTRAINT_REQUEST = 4020
opus.GET_VBR_CONSTRAINT_REQUEST = 4021
opus.SET_FORCE_CHANNELS_REQUEST = 4022
opus.GET_FORCE_CHANNELS_REQUEST = 4023
opus.SET_SIGNAL_REQUEST = 4024
opus.GET_SIGNAL_REQUEST = 4025
opus.GET_LOOKAHEAD_REQUEST = 4027
opus.GET_SAMPLE_RATE_REQUEST = 4029
opus.GET_FINAL_RANGE_REQUEST = 4031
opus.GET_PITCH_REQUEST = 4033
opus.SET_GAIN_REQUEST = 4034
opus.GET_GAIN_REQUEST = 4045
opus.SET_LSB_DEPTH_REQUEST = 4036
opus.GET_LSB_DEPTH_REQUEST = 4037
opus.GET_LAST_PACKET_DURATION_REQUEST = 4039
opus.SET_EXPERT_FRAME_DURATION_REQUEST = 4040
opus.GET_EXPERT_FRAME_DURATION_REQUEST = 4041
opus.SET_PREDICTION_DISABLED_REQUEST = 4042
opus.GET_PREDICTION_DISABLED_REQUEST = 4043
opus.SET_PHASE_INVERSION_DISABLED_REQUEST = 4046
opus.GET_PHASE_INVERSION_DISABLED_REQUEST = 4047
opus.FRAMESIZE_ARG = 5000
opus.FRAMESIZE_2_5_MS = 5001
opus.FRAMESIZE_5_MS = 5002
opus.FRAMESIZE_10_MS = 5003
opus.FRAMESIZE_20_MS = 5004
opus.FRAMESIZE_40_MS = 5005
opus.FRAMESIZE_60_MS = 5006
opus.FRAMESIZE_80_MS = 5007
opus.FRAMESIZE_100_MS = 5008
opus.FRAMESIZE_120_MS = 5009
local int_ptr_t = typeof("int[1]")
local opus_int32_t = typeof("opus_int32")
local opus_int32_ptr_t = typeof("opus_int32[1]")

local function throw(code)
	local version = ffi.string(lib.opus_get_version_string())
	local message = ffi.string(lib.opus_strerror(code))
	return error(string.format("[%s] %s", version, message))
end

local function check(value)
	return value >= opus.OK and value or throw(value)
end

local Encoder = {}
Encoder.__index = Encoder

function Encoder:__new(sample_rate, channels, app) -- luacheck: ignore self
	app = app or opus.APPLICATION_AUDIO -- TODO: test different appplications
	local err = int_ptr_t()
	local state = lib.opus_encoder_create(sample_rate, channels, app, err)
	check(err[0])
	check(lib.opus_encoder_init(state, sample_rate, channels, app))
	return gc(state, lib.opus_encoder_destroy)
end

function Encoder:encode(pcm, frame_size, max_data_bytes)
	local data = new("unsigned char[?]", max_data_bytes)
	local ret = lib.opus_encode(self, pcm, frame_size, data, max_data_bytes)
	return data, check(ret)
end

function Encoder:get(id)
	local ret = opus_int32_ptr_t()
	lib.opus_encoder_ctl(self, id, ret)
	return check(ret[0])
end

function Encoder:set(id, value)
	if type(value) ~= "number" then return throw(opus.BAD_ARG) end

	local ret = lib.opus_encoder_ctl(self, id, opus_int32_t(value))
	return check(ret)
end

opus.Encoder = ffi.metatype("OpusEncoder", Encoder)
local Decoder = {}
Decoder.__index = Decoder

function Decoder:__new(sample_rate, channels) -- luacheck: ignore self
	local err = int_ptr_t()
	local state = lib.opus_decoder_create(sample_rate, channels, err)
	check(err[0])
	check(lib.opus_decoder_init(state, sample_rate, channels))
	return gc(state, lib.opus_decoder_destroy)
end

function Decoder:decode(data, len, frame_size, output_len)
	local pcm = new("opus_int16[?]", output_len)
	local ret = lib.opus_decode(self, data, len, pcm, frame_size, 0)
	return pcm, check(ret)
end

function Decoder:get(id)
	local ret = opus_int32_ptr_t()
	lib.opus_decoder_ctl(self, id, ret)
	return check(ret[0])
end

function Decoder:set(id, value)
	if type(value) ~= "number" then return throw(opus.BAD_ARG) end

	local ret = lib.opus_decoder_ctl(self, id, opus_int32_t(value))
	return check(ret)
end

opus.Decoder = ffi.metatype("OpusDecoder", Decoder)
return opus