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

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

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-9-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR 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;
}

std::unique_ptr<AudioFormatWriter> AiffAudioFormat::createWriterFor (std::unique_ptr<OutputStream>& streamToWriteTo,
                                                                     const AudioFormatWriterOptions& options)
{
    if (streamToWriteTo == nullptr || ! getPossibleBitDepths().contains (options.getBitsPerSample()))
        return nullptr;

    StringPairArray metadata;
    metadata.addUnorderedMap (options.getMetadataValues());

    return std::make_unique<AiffAudioFormatWriter> (std::exchange (streamToWriteTo, {}).release(),
                                                    options.getSampleRate(),
                                                    (unsigned int) options.getNumChannels(),
                                                    (unsigned int) options.getBitsPerSample(),
                                                    metadata);
}

} // namespace juce
