#pragma once

#import "AudioFile.hpp"
#import <AudioToolbox/AudioToolbox.h>
#import <AudioToolbox/AudioConverter.h>
#import <AudioToolbox/ExtendedAudioFile.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <Gosu/IO.hpp>
#import <Gosu/Platform.hpp>
#import <Gosu/Utility.hpp>
#import <OpenAL/al.h>
#import <algorithm>
#import <arpa/inet.h>
#import <stdexcept>
#import <vector>

inline static void throw_os_error(OSStatus status, unsigned line)
{
    std::string what;
    @autoreleasepool {
        NSError* error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
        NSString* message = [NSString stringWithFormat:@"Error 0x%x on line %u: %@", line, status,
                                      error.localizedDescription];
        what = message.UTF8String;
    }
    throw std::runtime_error(what);
}

#define CHECK_OS(status) do { if (status) throw_os_error(status, __LINE__); } while (0)

namespace Gosu
{
    class AudioToolboxFile : public AudioFile
    {
        Gosu::Buffer buffer_;
        AudioFileID file_id_;
        ExtAudioFileRef file_;
        SInt64 position_;
        SInt64 seek_offset_;
        
        ALenum format_;
        ALuint sample_rate_;
        UInt32 bytes_per_frame_;
        bool big_endian_;
        
        static OSStatus AudioFile_ReadProc(void* in_client_data, SInt64 in_position,
            UInt32 request_count, void* buffer, UInt32* actual_count)
        {
            const Resource& res = *static_cast<Resource*>(in_client_data);
            *actual_count = std::min<UInt32>(request_count, res.size() - in_position);
            res.read(in_position, *actual_count, buffer);
            return noErr;
        }
        
        static SInt64 AudioFile_GetSizeProc(void* in_client_data)
        {
            const Resource& res = *static_cast<Resource*>(in_client_data);
            return res.size();
        }
        
        void init_seek_offset()
        {
            AudioConverterRef ac_ref;
            UInt32 acr_size = sizeof ac_ref;
            CHECK_OS(ExtAudioFileGetProperty(file_, kExtAudioFileProperty_AudioConverter,
                                             &acr_size, &ac_ref));
            
            AudioConverterPrimeInfo prime_info;
            UInt32 pi_size = sizeof prime_info;
            OSStatus result = AudioConverterGetProperty(ac_ref, kAudioConverterPrimeInfo,
                                                        &pi_size, &prime_info);
            if (result != kAudioConverterErr_PropertyNotSupported) {
                CHECK_OS(result);
                seek_offset_ = prime_info.leadingFrames;
            }
        }
        
        void init_client_format_based_on(const AudioStreamBasicDescription& base)
        {
            AudioStreamBasicDescription client_data = { 0 };
            sample_rate_ = client_data.mSampleRate = 22050;
            client_data.mFormatID = kAudioFormatLinearPCM;
            client_data.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
            client_data.mBitsPerChannel = 16;
            client_data.mChannelsPerFrame = base.mChannelsPerFrame;
            client_data.mFramesPerPacket = 1;
            client_data.mBytesPerPacket =
                client_data.mBytesPerFrame =
                    client_data.mChannelsPerFrame * client_data.mBitsPerChannel / 8;
            CHECK_OS(ExtAudioFileSetProperty(file_, kExtAudioFileProperty_ClientDataFormat,
                                             sizeof client_data, &client_data));
            
            init_seek_offset();

            format_ = client_data.mChannelsPerFrame == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16;
        }
        
        void init()
        {
            // Streaming starts at beginning
            position_ = 0;
            
            // Unless overridden later, assume that the index into seek() is 0-based
            seek_offset_ = 0;
            
            AudioStreamBasicDescription desc;
            UInt32 size_of_property = sizeof desc;
            CHECK_OS(ExtAudioFileGetProperty(file_, kExtAudioFileProperty_FileDataFormat,
                                             &size_of_property, &desc));

            // Sample rate for OpenAL
            sample_rate_ = desc.mSampleRate;
            
            // Sanity checks
            if (desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved) {
                throw std::runtime_error("Non-interleaved formats are unsupported");
            }
            
            // Easy formats
            format_ = 0;
            if (desc.mChannelsPerFrame == 1) {
                /*if (desc.mBitsPerChannel == 8)
                    format_ = AL_FORMAT_MONO8;
                else*/ if (desc.mBitsPerChannel == 16)
                    format_ = AL_FORMAT_MONO16;
            }
            else if (desc.mChannelsPerFrame == 2) {
                /*if (desc.mBitsPerChannel == 8)
                    format_ = AL_FORMAT_STEREO8;
                else */if (desc.mBitsPerChannel == 16)
                    format_ = AL_FORMAT_STEREO16;
            }
            
            if (format_ == 0 ||
                    // If format not native for OpenAL, set client data format
                    // to enable conversion
                    desc.mFormatFlags & kAudioFormatFlagIsBigEndian ||
                    desc.mFormatFlags & kAudioFormatFlagIsFloat ||
                    !(desc.mFormatFlags & kAudioFormatFlagIsSignedInteger)) {
                init_client_format_based_on(desc);
            }
            else {
                // Just set the old format as the client format so
                // ExtAudioFileSeek will work for us.
                CHECK_OS(ExtAudioFileSetProperty(file_, kExtAudioFileProperty_ClientDataFormat,
                                                 sizeof desc, &desc));
            }
        }
        
    public:
        AudioToolboxFile(const std::string& filename)
        {
            NSURL* URL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:filename.c_str()]];
            CHECK_OS(ExtAudioFileOpenURL((__bridge CFURLRef) URL, &file_));
            
            file_id_ = 0;
            
            init();
        }
        
        AudioToolboxFile(Gosu::Reader reader)
        {
            buffer_.resize(reader.resource().size() - reader.position());
            reader.read(buffer_.data(), buffer_.size());
            
            // TODO: This fails on iOS with MP3 files.
            // TODO: ^ Is the comment above still true on non-ancient iOS versions?
            
            void* client_data = &buffer_;
            CHECK_OS(AudioFileOpenWithCallbacks(client_data, AudioFile_ReadProc, 0,
                                                AudioFile_GetSizeProc, 0, 0, &file_id_));
            CHECK_OS(ExtAudioFileWrapAudioFileID(file_id_, false, &file_));
            
            init();
        }
        
        ~AudioToolboxFile() override
        {
            ExtAudioFileDispose(file_);

            if (file_id_) {
                AudioFileClose(file_id_);
            }
        }
        
        ALenum format() const override
        {
            return format_;
        }
        
        ALuint sample_rate() const override
        {
            return sample_rate_;
        }
        
        void rewind() override
        {
            CHECK_OS(ExtAudioFileSeek(file_, 0 + seek_offset_));
        }
        
        std::size_t read_data(void* dest, size_t length) override
        {
            AudioBufferList abl;
            abl.mNumberBuffers = 1;
            abl.mBuffers[0].mNumberChannels = 1;
            abl.mBuffers[0].mDataByteSize = length;
            abl.mBuffers[0].mData = dest;
            UInt32 numFrames = 0xffffffff; // give us as many frames as possible given our buffer
            CHECK_OS(ExtAudioFileRead(file_, &numFrames, &abl));
            return abl.mBuffers[0].mDataByteSize;
        }
    };
}