/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

static const char* const aiffFormatName = "AIFF file";

//==============================================================================
const char* const AiffAudioFormat::appleOneShot         = "apple one shot";
const char* const AiffAudioFormat::appleRootSet         = "apple root set";
const char* const AiffAudioFormat::appleRootNote        = "apple root note";
const char* const AiffAudioFormat::appleBeats           = "apple beats";
const char* const AiffAudioFormat::appleDenominator     = "apple denominator";
const char* const AiffAudioFormat::appleNumerator       = "apple numerator";
const char* const AiffAudioFormat::appleTag             = "apple tag";
const char* const AiffAudioFormat::appleKey             = "apple key";

//==============================================================================
namespace AiffFileHelpers
{
    inline int chunkName (const char* name) noexcept    { return (int) ByteOrder::littleEndianInt (name); }

   #if JUCE_MSVC
    #pragma pack (push, 1)
   #endif

    //==============================================================================
    struct InstChunk
    {
        struct Loop
        {
            uint16 type; // these are different in AIFF and WAV
            uint16 startIdentifier;
            uint16 endIdentifier;
        } JUCE_PACKED;

        int8 baseNote;
        int8 detune;
        int8 lowNote;
        int8 highNote;
        int8 lowVelocity;
        int8 highVelocity;
        int16 gain;
        Loop sustainLoop;
        Loop releaseLoop;

        void copyTo (std::map<String, String>& values) const
        {
            values.emplace ("MidiUnityNote",        String (baseNote));
            values.emplace ("Detune",               String (detune));

            values.emplace ("LowNote",              String (lowNote));
            values.emplace ("HighNote",             String (highNote));
            values.emplace ("LowVelocity",          String (lowVelocity));
            values.emplace ("HighVelocity",         String (highVelocity));

            values.emplace ("Gain",                 String ((int16) ByteOrder::swapIfLittleEndian ((uint16) gain)));

            values.emplace ("NumSampleLoops",       String (2));        // always 2 with AIFF, WAV can have more
            values.emplace ("Loop0Type",            String (ByteOrder::swapIfLittleEndian (sustainLoop.type)));
            values.emplace ("Loop0StartIdentifier", String (ByteOrder::swapIfLittleEndian (sustainLoop.startIdentifier)));
            values.emplace ("Loop0EndIdentifier",   String (ByteOrder::swapIfLittleEndian (sustainLoop.endIdentifier)));
            values.emplace ("Loop1Type",            String (ByteOrder::swapIfLittleEndian (releaseLoop.type)));
            values.emplace ("Loop1StartIdentifier", String (ByteOrder::swapIfLittleEndian (releaseLoop.startIdentifier)));
            values.emplace ("Loop1EndIdentifier",   String (ByteOrder::swapIfLittleEndian (releaseLoop.endIdentifier)));
        }

        static uint16 getValue16 (const StringPairArray& values, const char* name, const char* def)
        {
            return ByteOrder::swapIfLittleEndian ((uint16) values.getValue (name, def).getIntValue());
        }

        static int8 getValue8 (const StringPairArray& values, const char* name, const char* def)
        {
            return (int8) values.getValue (name, def).getIntValue();
        }

        static void create (MemoryBlock& block, const StringPairArray& values)
        {
            if (values.getAllKeys().contains ("MidiUnityNote", true))
            {
                block.setSize ((sizeof (InstChunk) + 3) & ~(size_t) 3, true);
                auto& inst = *static_cast<InstChunk*> (block.getData());

                inst.baseNote      = getValue8 (values, "MidiUnityNote", "60");
                inst.detune        = getValue8 (values, "Detune", "0");
                inst.lowNote       = getValue8 (values, "LowNote", "0");
                inst.highNote      = getValue8 (values, "HighNote", "127");
                inst.lowVelocity   = getValue8 (values, "LowVelocity", "1");
                inst.highVelocity  = getValue8 (values, "HighVelocity", "127");
                inst.gain          = (int16) getValue16 (values, "Gain", "0");

                inst.sustainLoop.type              = getValue16 (values, "Loop0Type", "0");
                inst.sustainLoop.startIdentifier   = getValue16 (values, "Loop0StartIdentifier", "0");
                inst.sustainLoop.endIdentifier     = getValue16 (values, "Loop0EndIdentifier", "0");
                inst.releaseLoop.type              = getValue16 (values, "Loop1Type", "0");
                inst.releaseLoop.startIdentifier   = getValue16 (values, "Loop1StartIdentifier", "0");
                inst.releaseLoop.endIdentifier     = getValue16 (values, "Loop1EndIdentifier", "0");
            }
        }

    } JUCE_PACKED;

    //==============================================================================
    struct BASCChunk
    {
        enum Key
        {
            minor = 1,
            major = 2,
            neither = 3,
            both = 4
        };

        BASCChunk (InputStream& input)
        {
            zerostruct (*this);

            flags       = (uint32) input.readIntBigEndian();
            numBeats    = (uint32) input.readIntBigEndian();
            rootNote    = (uint16) input.readShortBigEndian();
            key         = (uint16) input.readShortBigEndian();
            timeSigNum  = (uint16) input.readShortBigEndian();
            timeSigDen  = (uint16) input.readShortBigEndian();
            oneShot     = (uint16) input.readShortBigEndian();
            input.read (unknown, sizeof (unknown));
        }

        void addToMetadata (std::map<String, String>& metadata) const
        {
            const bool rootNoteSet = rootNote != 0;

            setBoolFlag (metadata, AiffAudioFormat::appleOneShot, oneShot == 2);
            setBoolFlag (metadata, AiffAudioFormat::appleRootSet, rootNoteSet);

            if (rootNoteSet)
                metadata.emplace (AiffAudioFormat::appleRootNote,   String (rootNote));

            metadata.emplace (AiffAudioFormat::appleBeats,          String (numBeats));
            metadata.emplace (AiffAudioFormat::appleDenominator,    String (timeSigDen));
            metadata.emplace (AiffAudioFormat::appleNumerator,      String (timeSigNum));

            const char* keyString = nullptr;

            switch (key)
            {
                case minor:     keyString = "minor";   break;
                case major:     keyString = "major";   break;
                case neither:   keyString = "neither"; break;
                case both:      keyString = "both";    break;
                default:                               break;
            }

            if (keyString != nullptr)
                metadata.emplace (AiffAudioFormat::appleKey, keyString);
        }

        void setBoolFlag (std::map<String, String>& values,
                          const char* name,
                          bool shouldBeSet) const
        {
            values.emplace (name, shouldBeSet ? "1" : "0");
        }

        uint32 flags;
        uint32 numBeats;
        uint16 rootNote;
        uint16 key;
        uint16 timeSigNum;
        uint16 timeSigDen;
        uint16 oneShot;
        uint8 unknown[66];
    } JUCE_PACKED;

   #if JUCE_MSVC
    #pragma pack (pop)
   #endif

    //==============================================================================
    namespace CATEChunk
    {
        static bool isValidTag (const char* d) noexcept
        {
            return CharacterFunctions::isLetterOrDigit (d[0]) && CharacterFunctions::isUpperCase (static_cast<juce_wchar> (d[0]))
                && CharacterFunctions::isLetterOrDigit (d[1]) && CharacterFunctions::isLowerCase (static_cast<juce_wchar> (d[1]))
                && CharacterFunctions::isLetterOrDigit (d[2]) && CharacterFunctions::isLowerCase (static_cast<juce_wchar> (d[2]));
        }

        static bool isAppleGenre (const String& tag) noexcept
        {
            static const char* appleGenres[] =
            {
                "Rock/Blues",
                "Electronic/Dance",
                "Jazz",
                "Urban",
                "World/Ethnic",
                "Cinematic/New Age",
                "Orchestral",
                "Country/Folk",
                "Experimental",
                "Other Genre"
            };

            for (int i = 0; i < numElementsInArray (appleGenres); ++i)
                if (tag == appleGenres[i])
                    return true;

            return false;
        }

        static String read (InputStream& input, const uint32 length)
        {
            MemoryBlock mb;
            input.skipNextBytes (4);
            input.readIntoMemoryBlock (mb, (ssize_t) length - 4);

            StringArray tagsArray;

            auto* data = static_cast<const char*> (mb.getData());
            auto* dataEnd = data + mb.getSize();

            while (data < dataEnd)
            {
                bool isGenre = false;

                if (isValidTag (data))
                {
                    auto tag = String (CharPointer_UTF8 (data), CharPointer_UTF8 (dataEnd));
                    isGenre = isAppleGenre (tag);
                    tagsArray.add (tag);
                }

                data += isGenre ? 118 : 50;

                if (data < dataEnd && data[0] == 0)
                {
                    if      (data + 52  < dataEnd && isValidTag (data + 50))   data += 50;
                    else if (data + 120 < dataEnd && isValidTag (data + 118))  data += 118;
                    else if (data + 170 < dataEnd && isValidTag (data + 168))  data += 168;
                }
            }

            return tagsArray.joinIntoString (";");
        }
    }

    //==============================================================================
    namespace MarkChunk
    {
        static bool metaDataContainsZeroIdentifiers (const StringPairArray& values)
        {
            // (zero cue identifiers are valid for WAV but not for AIFF)
            const String cueString ("Cue");
            const String noteString ("CueNote");
            const String identifierString ("Identifier");

            for (auto& key : values.getAllKeys())
            {
                if (key.startsWith (noteString))
                    continue; // zero identifier IS valid in a COMT chunk

                if (key.startsWith (cueString) && key.contains (identifierString))
                    if (values.getValue (key, "-1").getIntValue() == 0)
                        return true;
            }

            return false;
        }

        static void create (MemoryBlock& block, const StringPairArray& values)
        {
            auto numCues = values.getValue ("NumCuePoints", "0").getIntValue();

            if (numCues > 0)
            {
                MemoryOutputStream out (block, false);
                out.writeShortBigEndian ((short) numCues);

                auto numCueLabels = values.getValue ("NumCueLabels", "0").getIntValue();
                auto idOffset = metaDataContainsZeroIdentifiers (values) ? 1 : 0; // can't have zero IDs in AIFF

               #if JUCE_DEBUG
                Array<int> identifiers;
               #endif

                for (int i = 0; i < numCues; ++i)
                {
                    auto prefixCue = "Cue" + String (i);
                    auto identifier = idOffset + values.getValue (prefixCue + "Identifier", "1").getIntValue();

                   #if JUCE_DEBUG
                    jassert (! identifiers.contains (identifier));
                    identifiers.add (identifier);
                   #endif

                    auto offset = values.getValue (prefixCue + "Offset", "0").getIntValue();
                    auto label = "CueLabel" + String (i);

                    for (int labelIndex = 0; labelIndex < numCueLabels; ++labelIndex)
                    {
                        auto prefixLabel = "CueLabel" + String (labelIndex);
                        auto labelIdentifier = idOffset + values.getValue (prefixLabel + "Identifier", "1").getIntValue();

                        if (labelIdentifier == identifier)
                        {
                            label = values.getValue (prefixLabel + "Text", label);
                            break;
                        }
                    }

                    out.writeShortBigEndian ((short) identifier);
                    out.writeIntBigEndian (offset);

                    auto labelLength = jmin ((size_t) 254, label.getNumBytesAsUTF8()); // seems to need null terminator even though it's a pstring
                    out.writeByte (static_cast<char> (labelLength + 1));
                    out.write (label.toUTF8(), labelLength);
                    out.writeByte (0);

                    if ((out.getDataSize() & 1) != 0)
                        out.writeByte (0);
                }
            }
        }
    }

    //==============================================================================
    namespace COMTChunk
    {
        static void create (MemoryBlock& block, const StringPairArray& values)
        {
            auto numNotes = values.getValue ("NumCueNotes", "0").getIntValue();

            if (numNotes > 0)
            {
                MemoryOutputStream out (block, false);
                out.writeShortBigEndian ((short) numNotes);

                for (int i = 0; i < numNotes; ++i)
                {
                    auto prefix = "CueNote" + String (i);

                    out.writeIntBigEndian (values.getValue (prefix + "TimeStamp", "0").getIntValue());
                    out.writeShortBigEndian ((short) values.getValue (prefix + "Identifier", "0").getIntValue());

                    auto comment = values.getValue (prefix + "Text", String());
                    auto commentLength = jmin (comment.getNumBytesAsUTF8(), (size_t) 65534);

                    out.writeShortBigEndian (static_cast<short> (commentLength + 1));
                    out.write (comment.toUTF8(), commentLength);
                    out.writeByte (0);

                    if ((out.getDataSize() & 1) != 0)
                        out.writeByte (0);
                }
            }
        }
    }
}

//==============================================================================
class AiffAudioFormatReader final : public AudioFormatReader
{
public:
    AiffAudioFormatReader (InputStream* in)
        : AudioFormatReader (in, aiffFormatName)
    {
        using namespace AiffFileHelpers;

        std::map<String, String> metadataValuesMap;

        for (int i = 0; i != metadataValues.size(); ++i)
        {
            metadataValuesMap.emplace (metadataValues.getAllKeys().getReference (i),
                                       metadataValues.getAllValues().getReference (i));
        }

        // If this fails, there were duplicate keys in the metadata
        jassert ((size_t) metadataValuesMap.size() == (size_t) metadataValues.size());

        if (input->readInt() == chunkName ("FORM"))
        {
            auto len = input->readIntBigEndian();
            auto end = input->getPosition() + len;
            auto nextType = input->readInt();

            if (nextType == chunkName ("AIFF") || nextType == chunkName ("AIFC"))
            {
                bool hasGotVer = false;
                bool hasGotData = false;
                bool hasGotType = false;

                while (input->getPosition() < end)
                {
                    auto type = input->readInt();
                    auto length = (uint32) input->readIntBigEndian();
                    auto chunkEnd = input->getPosition() + length;

                    if (type == chunkName ("FVER"))
                    {
                        hasGotVer = true;
                        auto ver = input->readIntBigEndian();

                        if (ver != 0 && ver != (int) 0xa2805140)
                            break;
                    }
                    else if (type == chunkName ("COMM"))
                    {
                        hasGotType = true;

                        numChannels = (unsigned int) input->readShortBigEndian();
                        lengthInSamples = input->readIntBigEndian();
                        bitsPerSample = (unsigned int) input->readShortBigEndian();
                        bytesPerFrame = (int) ((numChannels * bitsPerSample) >> 3);

                        unsigned char sampleRateBytes[10];
                        input->read (sampleRateBytes, 10);
                        const int byte0 = sampleRateBytes[0];

                        if ((byte0 & 0x80) != 0
                             || byte0 <= 0x3F || byte0 > 0x40
                             || (byte0 == 0x40 && sampleRateBytes[1] > 0x1C))
                            break;

                        auto sampRate = ByteOrder::bigEndianInt (sampleRateBytes + 2);
                        sampRate >>= (16414 - ByteOrder::bigEndianShort (sampleRateBytes));
                        sampleRate = (int) sampRate;

                        if (length <= 18)
                        {
                            // some types don't have a chunk large enough to include a compression
                            // type, so assume it's just big-endian pcm
                            littleEndian = false;
                        }
                        else
                        {
                            auto compType = input->readInt();

                            if (compType == chunkName ("NONE") || compType == chunkName ("twos"))
                            {
                                littleEndian = false;
                            }
                            else if (compType == chunkName ("sowt"))
                            {
                                littleEndian = true;
                            }
                            else if (compType == chunkName ("fl32") || compType == chunkName ("FL32"))
                            {
                                littleEndian = false;
                                usesFloatingPointData = true;
                            }
                            else
                            {
                                sampleRate = 0;
                                break;
                            }
                        }
                    }
                    else if (type == chunkName ("SSND"))
                    {
                        hasGotData = true;

                        auto offset = input->readIntBigEndian();
                        dataChunkStart = input->getPosition() + 4 + offset;
                        lengthInSamples = (bytesPerFrame > 0) ? jmin (lengthInSamples, ((int64) length) / (int64) bytesPerFrame) : 0;
                    }
                    else if (type == chunkName ("MARK"))
                    {
                        auto numCues = (uint16) input->readShortBigEndian();

                        // these two are always the same for AIFF-read files
                        metadataValuesMap.emplace ("NumCuePoints", String (numCues));
                        metadataValuesMap.emplace ("NumCueLabels", String (numCues));

                        for (uint16 i = 0; i < numCues; ++i)
                        {
                            auto identifier = (uint16) input->readShortBigEndian();
                            auto offset = (uint32) input->readIntBigEndian();
                            auto stringLength = (uint8) input->readByte();
                            MemoryBlock textBlock;
                            input->readIntoMemoryBlock (textBlock, stringLength);

                            // if the stringLength is even then read one more byte as the
                            // string needs to be an even number of bytes INCLUDING the
                            // leading length character in the pascal string
                            if ((stringLength & 1) == 0)
                                input->readByte();

                            auto prefixCue = "Cue" + String (i);
                            metadataValuesMap.emplace (prefixCue + "Identifier", String (identifier));
                            metadataValuesMap.emplace (prefixCue + "Offset", String (offset));

                            auto prefixLabel = "CueLabel" + String (i);
                            metadataValuesMap.emplace (prefixLabel + "Identifier", String (identifier));
                            metadataValuesMap.emplace (prefixLabel + "Text", textBlock.toString());
                        }
                    }
                    else if (type == chunkName ("COMT"))
                    {
                        auto numNotes = (uint16) input->readShortBigEndian();
                        metadataValuesMap.emplace ("NumCueNotes", String (numNotes));

                        for (uint16 i = 0; i < numNotes; ++i)
                        {
                            auto timestamp = (uint32) input->readIntBigEndian();
                            auto identifier = (uint16) input->readShortBigEndian(); // may be zero in this case
                            auto stringLength = (uint16) input->readShortBigEndian();

                            MemoryBlock textBlock;
                            input->readIntoMemoryBlock (textBlock, stringLength + (stringLength & 1));

                            auto prefix = "CueNote" + String (i);
                            metadataValuesMap.emplace (prefix + "TimeStamp", String (timestamp));
                            metadataValuesMap.emplace (prefix + "Identifier", String (identifier));
                            metadataValuesMap.emplace (prefix + "Text", textBlock.toString());
                        }
                    }
                    else if (type == chunkName ("INST"))
                    {
                        HeapBlock<InstChunk> inst;
                        inst.calloc (jmax ((size_t) length + 1, sizeof (InstChunk)), 1);
                        input->read (inst, (int) length);
                        inst->copyTo (metadataValuesMap);
                    }
                    else if (type == chunkName ("basc"))
                    {
                        AiffFileHelpers::BASCChunk (*input).addToMetadata (metadataValuesMap);
                    }
                    else if (type == chunkName ("cate"))
                    {
                        metadataValuesMap.emplace (AiffAudioFormat::appleTag,
                                                  AiffFileHelpers::CATEChunk::read (*input, length));
                    }
                    else if ((hasGotVer && hasGotData && hasGotType)
                              || chunkEnd < input->getPosition()
                              || input->isExhausted())
                    {
                        break;
                    }

                    input->setPosition (chunkEnd + (chunkEnd & 1)); // (chunks should be aligned to an even byte address)
                }
            }
        }

        if (metadataValuesMap.size() > 0)
            metadataValuesMap.emplace ("MetaDataSource", "AIFF");

        metadataValues.addMap (metadataValuesMap);
    }

    //==============================================================================
    bool readSamples (int* const* destSamples, int numDestChannels, int startOffsetInDestBuffer,
                      int64 startSampleInFile, int numSamples) override
    {
        clearSamplesBeyondAvailableLength (destSamples, numDestChannels, startOffsetInDestBuffer,
                                           startSampleInFile, numSamples, lengthInSamples);

        if (numSamples <= 0)
            return true;

        input->setPosition (dataChunkStart + startSampleInFile * bytesPerFrame);

        while (numSamples > 0)
        {
            const int tempBufSize = 480 * 3 * 4; // (keep this a multiple of 3)
            char tempBuffer [tempBufSize];

            const int numThisTime = jmin (tempBufSize / bytesPerFrame, numSamples);
            const int bytesRead = input->read (tempBuffer, numThisTime * bytesPerFrame);

            if (bytesRead < numThisTime * bytesPerFrame)
            {
                jassert (bytesRead >= 0);
                zeromem (tempBuffer + bytesRead, (size_t) (numThisTime * bytesPerFrame - bytesRead));
            }

            if (littleEndian)
                copySampleData<AudioData::LittleEndian> (bitsPerSample, usesFloatingPointData,
                                                         destSamples, startOffsetInDestBuffer, numDestChannels,
                                                         tempBuffer, (int) numChannels, numThisTime);
            else
                copySampleData<AudioData::BigEndian> (bitsPerSample, usesFloatingPointData,
                                                      destSamples, startOffsetInDestBuffer, numDestChannels,
                                                      tempBuffer, (int) numChannels, numThisTime);

            startOffsetInDestBuffer += numThisTime;
            numSamples -= numThisTime;
        }

        return true;
    }

    template <typename Endianness>
    static void copySampleData (unsigned int numBitsPerSample, bool floatingPointData,
                                int* const* destSamples, int startOffsetInDestBuffer, int numDestChannels,
                                const void* sourceData, int numberOfChannels, int numSamples) noexcept
    {
        switch (numBitsPerSample)
        {
            case 8:     ReadHelper<AudioData::Int32, AudioData::Int8,  Endianness>::read (destSamples, startOffsetInDestBuffer, numDestChannels, sourceData, numberOfChannels, numSamples); break;
            case 16:    ReadHelper<AudioData::Int32, AudioData::Int16, Endianness>::read (destSamples, startOffsetInDestBuffer, numDestChannels, sourceData, numberOfChannels, numSamples); break;
            case 24:    ReadHelper<AudioData::Int32, AudioData::Int24, Endianness>::read (destSamples, startOffsetInDestBuffer, numDestChannels, sourceData, numberOfChannels, numSamples); break;
            case 32:    if (floatingPointData) ReadHelper<AudioData::Float32, AudioData::Float32, Endianness>::read (destSamples, startOffsetInDestBuffer, numDestChannels, sourceData, numberOfChannels, numSamples);
                        else                   ReadHelper<AudioData::Int32,   AudioData::Int32,   Endianness>::read (destSamples, startOffsetInDestBuffer, numDestChannels, sourceData, numberOfChannels, numSamples);
                        break;
            default:    jassertfalse; break;
        }
    }

    int bytesPerFrame;
    int64 dataChunkStart;
    bool littleEndian;

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AiffAudioFormatReader)
};

//==============================================================================
class AiffAudioFormatWriter final : public AudioFormatWriter
{
public:
    AiffAudioFormatWriter (OutputStream* out, double rate,
                           unsigned int numChans, unsigned int bits,
                           const StringPairArray& metadataValues)
        : AudioFormatWriter (out, aiffFormatName, rate, numChans, bits)
    {
        using namespace AiffFileHelpers;

        if (metadataValues.size() > 0)
        {
            // The meta data should have been sanitised for the AIFF format.
            // If it was originally sourced from a WAV file the MetaDataSource
            // key should be removed (or set to "AIFF") once this has been done
            jassert (metadataValues.getValue ("MetaDataSource", "None") != "WAV");

            MarkChunk::create (markChunk, metadataValues);
            COMTChunk::create (comtChunk, metadataValues);
            InstChunk::create (instChunk, metadataValues);
        }

        headerPosition = out->getPosition();
        writeHeader();
    }

    ~AiffAudioFormatWriter() override
    {
        if ((bytesWritten & 1) != 0)
            output->writeByte (0);

        writeHeader();
    }

    //==============================================================================
    bool write (const int** data, int numSamples) override
    {
        jassert (numSamples >= 0);
        jassert (data != nullptr && *data != nullptr); // the input must contain at least one channel!

        if (writeFailed)
            return false;

        auto bytes = numChannels * (size_t) numSamples * bitsPerSample / 8;
        tempBlock.ensureSize (bytes, false);

        switch (bitsPerSample)
        {
            case 8:     WriteHelper<AudioData::Int8,  AudioData::Int32, AudioData::BigEndian>::write (tempBlock.getData(), (int) numChannels, data, numSamples); break;
            case 16:    WriteHelper<AudioData::Int16, AudioData::Int32, AudioData::BigEndian>::write (tempBlock.getData(), (int) numChannels, data, numSamples); break;
            case 24:    WriteHelper<AudioData::Int24, AudioData::Int32, AudioData::BigEndian>::write (tempBlock.getData(), (int) numChannels, data, numSamples); break;
            case 32:    WriteHelper<AudioData::Int32, AudioData::Int32, AudioData::BigEndian>::write (tempBlock.getData(), (int) numChannels, data, numSamples); break;
            default:    jassertfalse; break;
        }

        if (bytesWritten + bytes >= (size_t) 0xfff00000
             || ! output->write (tempBlock.getData(), bytes))
        {
            // failed to write to disk, so let's try writing the header.
            // If it's just run out of disk space, then if it does manage
            // to write the header, we'll still have a useable file..
            writeHeader();
            writeFailed = true;
            return false;
        }

        bytesWritten += bytes;
        lengthInSamples += (uint64) numSamples;
        return true;
    }

private:
    MemoryBlock tempBlock, markChunk, comtChunk, instChunk;
    uint64 lengthInSamples = 0, bytesWritten = 0;
    int64 headerPosition = 0;
    bool writeFailed = false;

    void writeHeader()
    {
        using namespace AiffFileHelpers;

        [[maybe_unused]] const bool couldSeekOk = output->setPosition (headerPosition);

        // if this fails, you've given it an output stream that can't seek! It needs
        // to be able to seek back to write the header
        jassert (couldSeekOk);

        auto headerLen = (int) (54 + (markChunk.isEmpty() ? 0 : markChunk.getSize() + 8)
                                   + (comtChunk.isEmpty() ? 0 : comtChunk.getSize() + 8)
                                   + (instChunk.isEmpty() ? 0 : instChunk.getSize() + 8));
        auto audioBytes = (int) (lengthInSamples * ((bitsPerSample * numChannels) / 8));
        audioBytes += (audioBytes & 1);

        output->writeInt (chunkName ("FORM"));
        output->writeIntBigEndian (headerLen + audioBytes - 8);
        output->writeInt (chunkName ("AIFF"));
        output->writeInt (chunkName ("COMM"));
        output->writeIntBigEndian (18);
        output->writeShortBigEndian ((short) numChannels);
        output->writeIntBigEndian ((int) lengthInSamples);
        output->writeShortBigEndian ((short) bitsPerSample);

        uint8 sampleRateBytes[10] = {};

        if (sampleRate <= 1)
        {
            sampleRateBytes[0] = 0x3f;
            sampleRateBytes[1] = 0xff;
            sampleRateBytes[2] = 0x80;
        }
        else
        {
            int mask = 0x40000000;
            sampleRateBytes[0] = 0x40;

            if (sampleRate >= mask)
            {
                jassertfalse;
                sampleRateBytes[1] = 0x1d;
            }
            else
            {
                int n = (int) sampleRate;
                int i;

                for (i = 0; i <= 32 ; ++i)
                {
                    if ((n & mask) != 0)
                        break;

                    mask >>= 1;
                }

                n = n << (i + 1);

                sampleRateBytes[1] = (uint8) (29 - i);
                sampleRateBytes[2] = (uint8) ((n >> 24) & 0xff);
                sampleRateBytes[3] = (uint8) ((n >> 16) & 0xff);
                sampleRateBytes[4] = (uint8) ((n >>  8) & 0xff);
                sampleRateBytes[5] = (uint8) (n & 0xff);
            }
        }

        output->write (sampleRateBytes, 10);

        if (! markChunk.isEmpty())
        {
            output->writeInt (chunkName ("MARK"));
            output->writeIntBigEndian ((int) markChunk.getSize());
            *output << markChunk;
        }

        if (! comtChunk.isEmpty())
        {
            output->writeInt (chunkName ("COMT"));
            output->writeIntBigEndian ((int) comtChunk.getSize());
            *output << comtChunk;
        }

        if (! instChunk.isEmpty())
        {
            output->writeInt (chunkName ("INST"));
            output->writeIntBigEndian ((int) instChunk.getSize());
            *output << instChunk;
        }

        output->writeInt (chunkName ("SSND"));
        output->writeIntBigEndian (audioBytes + 8);
        output->writeInt (0);
        output->writeInt (0);

        jassert (output->getPosition() == headerLen);
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AiffAudioFormatWriter)
};

//==============================================================================
class MemoryMappedAiffReader final : public MemoryMappedAudioFormatReader
{
public:
    MemoryMappedAiffReader (const File& f, const AiffAudioFormatReader& reader)
        : MemoryMappedAudioFormatReader (f, reader, reader.dataChunkStart,
                                         reader.bytesPerFrame * reader.lengthInSamples, reader.bytesPerFrame),
          littleEndian (reader.littleEndian)
    {
    }

    bool readSamples (int* const* destSamples, int numDestChannels, int startOffsetInDestBuffer,
                      int64 startSampleInFile, int numSamples) override
    {
        clearSamplesBeyondAvailableLength (destSamples, numDestChannels, startOffsetInDestBuffer,
                                           startSampleInFile, numSamples, lengthInSamples);

        if (numSamples <= 0)
            return true;

        if (map == nullptr || ! mappedSection.contains (Range<int64> (startSampleInFile, startSampleInFile + numSamples)))
        {
            jassertfalse; // you must make sure that the window contains all the samples you're going to attempt to read.
            return false;
        }

        if (littleEndian)
            AiffAudioFormatReader::copySampleData<AudioData::LittleEndian>
                    (bitsPerSample, usesFloatingPointData, destSamples, startOffsetInDestBuffer,
                     numDestChannels, sampleToPointer (startSampleInFile), (int) numChannels, numSamples);
        else
            AiffAudioFormatReader::copySampleData<AudioData::BigEndian>
                    (bitsPerSample, usesFloatingPointData, destSamples, startOffsetInDestBuffer,
                     numDestChannels, sampleToPointer (startSampleInFile), (int) numChannels, numSamples);

        return true;
    }

    void getSample (int64 sample, float* result) const noexcept override
    {
        auto num = (int) numChannels;

        if (map == nullptr || ! mappedSection.contains (sample))
        {
            jassertfalse; // you must make sure that the window contains all the samples you're going to attempt to read.

            zeromem (result, (size_t) num * sizeof (float));
            return;
        }

        float** dest = &result;
        const void* source = sampleToPointer (sample);

        if (littleEndian)
        {
            switch (bitsPerSample)
            {
                case 8:     ReadHelper<AudioData::Float32, AudioData::UInt8, AudioData::LittleEndian>::read (dest, 0, 1, source, 1, num); break;
                case 16:    ReadHelper<AudioData::Float32, AudioData::Int16, AudioData::LittleEndian>::read (dest, 0, 1, source, 1, num); break;
                case 24:    ReadHelper<AudioData::Float32, AudioData::Int24, AudioData::LittleEndian>::read (dest, 0, 1, source, 1, num); break;
                case 32:    if (usesFloatingPointData) ReadHelper<AudioData::Float32, AudioData::Float32, AudioData::LittleEndian>::read (dest, 0, 1, source, 1, num);
                            else                       ReadHelper<AudioData::Float32, AudioData::Int32,   AudioData::LittleEndian>::read (dest, 0, 1, source, 1, num);
                            break;
                default:    jassertfalse; break;
            }
        }
        else
        {
            switch (bitsPerSample)
            {
                case 8:     ReadHelper<AudioData::Float32, AudioData::UInt8, AudioData::BigEndian>::read (dest, 0, 1, source, 1, num); break;
                case 16:    ReadHelper<AudioData::Float32, AudioData::Int16, AudioData::BigEndian>::read (dest, 0, 1, source, 1, num); break;
                case 24:    ReadHelper<AudioData::Float32, AudioData::Int24, AudioData::BigEndian>::read (dest, 0, 1, source, 1, num); break;
                case 32:    if (usesFloatingPointData) ReadHelper<AudioData::Float32, AudioData::Float32, AudioData::BigEndian>::read (dest, 0, 1, source, 1, num);
                            else                       ReadHelper<AudioData::Float32, AudioData::Int32,   AudioData::BigEndian>::read (dest, 0, 1, source, 1, num);
                            break;
                default:    jassertfalse; break;
            }
        }
    }

    void readMaxLevels (int64 startSampleInFile, int64 numSamples, Range<float>* results, int numChannelsToRead) override
    {
        numSamples = jmin (numSamples, lengthInSamples - startSampleInFile);

        if (map == nullptr || numSamples <= 0 || ! mappedSection.contains (Range<int64> (startSampleInFile, startSampleInFile + numSamples)))
        {
            jassert (numSamples <= 0); // you must make sure that the window contains all the samples you're going to attempt to read.

            for (int i = 0; i < numChannelsToRead; ++i)
                results[i] = Range<float>();

            return;
        }

        switch (bitsPerSample)
        {
            case 8:     scanMinAndMax<AudioData::UInt8> (startSampleInFile, numSamples, results, numChannelsToRead); break;
            case 16:    scanMinAndMax<AudioData::Int16> (startSampleInFile, numSamples, results, numChannelsToRead); break;
            case 24:    scanMinAndMax<AudioData::Int24> (startSampleInFile, numSamples, results, numChannelsToRead); break;
            case 32:    if (usesFloatingPointData) scanMinAndMax<AudioData::Float32> (startSampleInFile, numSamples, results, numChannelsToRead);
                        else                       scanMinAndMax<AudioData::Int32>   (startSampleInFile, numSamples, results, numChannelsToRead);
                        break;
            default:    jassertfalse; break;
        }
    }

    using AudioFormatReader::readMaxLevels;

private:
    const bool littleEndian;

    template <typename SampleType>
    void scanMinAndMax (int64 startSampleInFile, int64 numSamples, Range<float>* results, int numChannelsToRead) const noexcept
    {
        for (int i = 0; i < numChannelsToRead; ++i)
            results[i] = scanMinAndMaxForChannel<SampleType> (i, startSampleInFile, numSamples);
    }

    template <typename SampleType>
    Range<float> scanMinAndMaxForChannel (int channel, int64 startSampleInFile, int64 numSamples) const noexcept
    {
        return littleEndian ? scanMinAndMaxInterleaved<SampleType, AudioData::LittleEndian> (channel, startSampleInFile, numSamples)
                            : scanMinAndMaxInterleaved<SampleType, AudioData::BigEndian>    (channel, startSampleInFile, numSamples);
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MemoryMappedAiffReader)
};

//==============================================================================
AiffAudioFormat::AiffAudioFormat()  : AudioFormat (aiffFormatName, ".aiff .aif") {}
AiffAudioFormat::~AiffAudioFormat() {}

Array<int> AiffAudioFormat::getPossibleSampleRates()
{
    return { 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000 };
}

Array<int> AiffAudioFormat::getPossibleBitDepths()
{
     return { 8, 16, 24 };
}

bool AiffAudioFormat::canDoStereo() { return true; }
bool AiffAudioFormat::canDoMono()   { return true; }

#if JUCE_MAC
bool AiffAudioFormat::canHandleFile (const File& f)
{
    if (AudioFormat::canHandleFile (f))
        return true;

    auto type = f.getMacOSType();

    // (NB: written as hex to avoid four-char-constant warnings)
    return type == 0x41494646 /* AIFF */ || type == 0x41494643 /* AIFC */
        || type == 0x61696666 /* aiff */ || type == 0x61696663 /* aifc */;
}
#endif

AudioFormatReader* AiffAudioFormat::createReaderFor (InputStream* sourceStream, bool deleteStreamIfOpeningFails)
{
    std::unique_ptr<AiffAudioFormatReader> w (new AiffAudioFormatReader (sourceStream));

    if (w->sampleRate > 0 && w->numChannels > 0)
        return w.release();

    if (! deleteStreamIfOpeningFails)
        w->input = nullptr;

    return nullptr;
}

MemoryMappedAudioFormatReader* AiffAudioFormat::createMemoryMappedReader (const File& file)
{
    return createMemoryMappedReader (file.createInputStream().release());
}

MemoryMappedAudioFormatReader* AiffAudioFormat::createMemoryMappedReader (FileInputStream* fin)
{
    if (fin != nullptr)
    {
        AiffAudioFormatReader reader (fin);

        if (reader.lengthInSamples > 0)
            return new MemoryMappedAiffReader (fin->getFile(), reader);
    }

    return nullptr;
}

AudioFormatWriter* AiffAudioFormat::createWriterFor (OutputStream* out,
                                                     double sampleRate,
                                                     unsigned int numberOfChannels,
                                                     int bitsPerSample,
                                                     const StringPairArray& metadataValues,
                                                     int /*qualityOptionIndex*/)
{
    if (out != nullptr && getPossibleBitDepths().contains (bitsPerSample))
        return new AiffAudioFormatWriter (out, sampleRate, numberOfChannels,
                                          (unsigned int) bitsPerSample, metadataValues);

    return nullptr;
}

} // namespace juce
