#include "audio/audio.h"
#include <AudioToolbox/AudioToolbox.h>
#include <CoreAudio/CoreAudio.h>
#include <string.h>
#include <math.h>
#include <stdatomic.h>

#pragma clang diagnostic ignored "-Wdeprecated-declarations"

#define AUDIO_MAX_DEVICES   32
#define AUDIO_MAX_CHANNELS  32
#define AUDIO_TEST_TONE_HZ  440.0
#define AUDIO_TEST_TONE_SEC 2.0
#define AUDIO_PI            3.14159265358979323846

struct CoreAudioDeviceInfo {
    AudioDeviceID device_id;
};

struct AudioEngine {
    AudioDeviceInfo       devices[AUDIO_MAX_DEVICES];
    CoreAudioDeviceInfo   ca_devices[AUDIO_MAX_DEVICES];
    S32               device_count;

    AUGraph               graph;
    AudioUnit             output_unit;
    S32               active_device_index;
    F64                sample_rate;
    S32               num_channels;

    // Test tone state (accessed from audio render thread)
    _Atomic S32       test_tone_active;
    _Atomic S32       test_tone_samples_remaining;
    F64                test_tone_phase;
};

////////////////////////////////
// Audio render callback

static AudioEngine *g_audio_engine = nullptr;

static OSStatus audio_render_callback(void *inRefCon,
    AudioUnitRenderActionFlags *ioActionFlags,
    const AudioTimeStamp *inTimeStamp,
    UInt32 inBusNumber,
    UInt32 inNumberFrames,
    AudioBufferList *ioData)
{
    (void)inRefCon; (void)ioActionFlags; (void)inTimeStamp; (void)inBusNumber;

    AudioEngine *engine = g_audio_engine;
    if (!engine) {
        for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++)
            memset(ioData->mBuffers[buf].mData, 0, ioData->mBuffers[buf].mDataByteSize);
        return noErr;
    }

    S32 tone_active = atomic_load(&engine->test_tone_active);

    if (!tone_active) {
        for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++)
            memset(ioData->mBuffers[buf].mData, 0, ioData->mBuffers[buf].mDataByteSize);
        return noErr;
    }

    F64 phase = engine->test_tone_phase;
    F64 phase_inc = 2.0 * AUDIO_PI * AUDIO_TEST_TONE_HZ / engine->sample_rate;
    S32 remaining = atomic_load(&engine->test_tone_samples_remaining);
    S32 samples_to_gen = (remaining < (S32)inNumberFrames) ? remaining : (S32)inNumberFrames;

    // CoreAudio with kAudioFormatFlagIsNonInterleaved: each buffer = one channel
    for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++) {
        F32 *out = (F32 *)ioData->mBuffers[buf].mData;
        F64 p = phase;
        for (UInt32 s = 0; s < inNumberFrames; s++) {
            if ((S32)s < samples_to_gen) {
                out[s] = (F32)(sin(p) * 0.5); // -6dB
                p += phase_inc;
                if (p >= 2.0 * AUDIO_PI) p -= 2.0 * AUDIO_PI;
            } else {
                out[s] = 0.0f;
            }
        }
    }

    // Advance phase using first channel's traversal
    phase += phase_inc * samples_to_gen;
    while (phase >= 2.0 * AUDIO_PI) phase -= 2.0 * AUDIO_PI;
    engine->test_tone_phase = phase;

    S32 new_remaining = atomic_fetch_sub(&engine->test_tone_samples_remaining, samples_to_gen);
    if (new_remaining <= samples_to_gen) {
        atomic_store(&engine->test_tone_active, 0);
        atomic_store(&engine->test_tone_samples_remaining, 0);
    }

    return noErr;
}

////////////////////////////////
// Device enumeration

static void enumerate_output_devices(AudioEngine *engine) {
    engine->device_count = 0;

    AudioObjectPropertyAddress prop = {
        kAudioHardwarePropertyDevices,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain
    };

    UInt32 data_size = 0;
    if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, nullptr, &data_size) != noErr)
        return;

    S32 device_count = (S32)(data_size / sizeof(AudioDeviceID));
    if (device_count <= 0) return;

    AudioDeviceID *device_ids = (AudioDeviceID *)malloc(data_size);
    if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, nullptr, &data_size, device_ids) != noErr) {
        free(device_ids);
        return;
    }

    for (S32 i = 0; i < device_count && engine->device_count < AUDIO_MAX_DEVICES; i++) {
        // Check if device has output channels
        AudioObjectPropertyAddress stream_prop = {
            kAudioDevicePropertyStreamConfiguration,
            kAudioDevicePropertyScopeOutput,
            kAudioObjectPropertyElementMain
        };

        UInt32 stream_size = 0;
        if (AudioObjectGetPropertyDataSize(device_ids[i], &stream_prop, 0, nullptr, &stream_size) != noErr)
            continue;

        AudioBufferList *buf_list = (AudioBufferList *)malloc(stream_size);
        if (AudioObjectGetPropertyData(device_ids[i], &stream_prop, 0, nullptr, &stream_size, buf_list) != noErr) {
            free(buf_list);
            continue;
        }

        S32 output_channels = 0;
        for (UInt32 b = 0; b < buf_list->mNumberBuffers; b++)
            output_channels += (S32)buf_list->mBuffers[b].mNumberChannels;
        free(buf_list);

        if (output_channels == 0) continue;

        // Get device name
        AudioObjectPropertyAddress name_prop = {
            kAudioObjectPropertyName,
            kAudioObjectPropertyScopeGlobal,
            kAudioObjectPropertyElementMain
        };

        CFStringRef name_ref = nullptr;
        UInt32 name_size = sizeof(name_ref);
        if (AudioObjectGetPropertyData(device_ids[i], &name_prop, 0, nullptr, &name_size, &name_ref) != noErr)
            continue;

        S32 idx = engine->device_count++;
        CFStringGetCString(name_ref, engine->devices[idx].name, sizeof(engine->devices[idx].name),
                           kCFStringEncodingUTF8);
        CFRelease(name_ref);
        engine->devices[idx].id = idx;
        engine->ca_devices[idx].device_id = device_ids[i];
    }

    free(device_ids);
}

////////////////////////////////
// Public API

AudioEngine *audio_create(void *hwnd) {
    (void)hwnd;

    AudioEngine *engine = new AudioEngine();
    memset(engine, 0, sizeof(*engine));
    engine->active_device_index = -1;
    atomic_store(&engine->test_tone_active, 0);
    atomic_store(&engine->test_tone_samples_remaining, 0);
    g_audio_engine = engine;

    enumerate_output_devices(engine);
    return engine;
}

void audio_destroy(AudioEngine *engine) {
    audio_close_device(engine);
    if (g_audio_engine == engine) g_audio_engine = nullptr;
    delete engine;
}

void audio_refresh_devices(AudioEngine *engine) {
    audio_close_device(engine);
    enumerate_output_devices(engine);
}

S32 audio_get_device_count(AudioEngine *engine) {
    return engine->device_count;
}

AudioDeviceInfo *audio_get_device(AudioEngine *engine, S32 index) {
    if (index < 0 || index >= engine->device_count) return nullptr;
    return &engine->devices[index];
}

B32 audio_open_device(AudioEngine *engine, S32 index) {
    audio_close_device(engine);

    if (index < 0 || index >= engine->device_count) return false;

    AudioDeviceID device_id = engine->ca_devices[index].device_id;

    // Create AUGraph
    if (NewAUGraph(&engine->graph) != noErr) return false;

    // Add HAL output node
    AudioComponentDescription output_desc = {};
    output_desc.componentType = kAudioUnitType_Output;
    output_desc.componentSubType = kAudioUnitSubType_HALOutput;
    output_desc.componentManufacturer = kAudioUnitManufacturer_Apple;

    AUNode output_node;
    if (AUGraphAddNode(engine->graph, &output_desc, &output_node) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    if (AUGraphOpen(engine->graph) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    if (AUGraphNodeInfo(engine->graph, output_node, nullptr, &engine->output_unit) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    // Set device
    if (AudioUnitSetProperty(engine->output_unit, kAudioOutputUnitProperty_CurrentDevice,
            kAudioUnitScope_Global, 0, &device_id, sizeof(device_id)) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    // Get device sample rate
    AudioObjectPropertyAddress rate_prop = {
        kAudioDevicePropertyNominalSampleRate,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain
    };
    Float64 sample_rate = 44100.0;
    UInt32 rate_size = sizeof(sample_rate);
    AudioObjectGetPropertyData(device_id, &rate_prop, 0, nullptr, &rate_size, &sample_rate);
    engine->sample_rate = sample_rate;

    // Set stream format: Float32, non-interleaved
    AudioStreamBasicDescription fmt = {};
    fmt.mSampleRate = sample_rate;
    fmt.mFormatID = kAudioFormatLinearPCM;
    fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsNonInterleaved | kAudioFormatFlagIsPacked;
    fmt.mBitsPerChannel = 32;
    fmt.mChannelsPerFrame = 2;
    fmt.mFramesPerPacket = 1;
    fmt.mBytesPerFrame = 4;
    fmt.mBytesPerPacket = 4;
    engine->num_channels = 2;

    AudioUnitSetProperty(engine->output_unit, kAudioUnitProperty_StreamFormat,
        kAudioUnitScope_Input, 0, &fmt, sizeof(fmt));

    // Set render callback
    AURenderCallbackStruct cb = {};
    cb.inputProc = audio_render_callback;
    cb.inputProcRefCon = engine;

    if (AudioUnitSetProperty(engine->output_unit, kAudioUnitProperty_SetRenderCallback,
            kAudioUnitScope_Input, 0, &cb, sizeof(cb)) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    // Initialize and start
    if (AUGraphInitialize(engine->graph) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    if (AUGraphStart(engine->graph) != noErr) {
        DisposeAUGraph(engine->graph);
        engine->graph = nullptr;
        return false;
    }

    engine->active_device_index = index;
    engine->test_tone_phase = 0.0;
    atomic_store(&engine->test_tone_active, 0);
    atomic_store(&engine->test_tone_samples_remaining, 0);

    return true;
}

void audio_close_device(AudioEngine *engine) {
    if (!engine->graph) return;

    atomic_store(&engine->test_tone_active, 0);
    atomic_store(&engine->test_tone_samples_remaining, 0);

    AUGraphStop(engine->graph);
    AUGraphUninitialize(engine->graph);
    DisposeAUGraph(engine->graph);
    engine->graph = nullptr;
    engine->output_unit = nullptr;
    engine->active_device_index = -1;
    engine->test_tone_phase = 0.0;
}

void audio_play_test_tone(AudioEngine *engine) {
    if (!engine->graph) return;

    engine->test_tone_phase = 0.0;
    S32 total_samples = (S32)(engine->sample_rate * AUDIO_TEST_TONE_SEC);
    atomic_store(&engine->test_tone_samples_remaining, total_samples);
    atomic_store(&engine->test_tone_active, 1);
}

B32 audio_is_test_tone_playing(AudioEngine *engine) {
    return atomic_load(&engine->test_tone_active) != 0;
}

void audio_update(AudioEngine *engine, F32 dt) {
    (void)engine;
    (void)dt;
}
