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

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   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 5 End-User License
   Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
   27th April 2017).

   End User License Agreement: www.juce.com/juce-5-licence
   Privacy Policy: www.juce.com/juce-5-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 CDBurnerHelpers
{
    IDiscRecorder* enumCDBurners (StringArray* list, int indexToOpen, IDiscMaster** master)
    {
        CoInitialize (0);

        IDiscMaster* dm;
        IDiscRecorder* result = nullptr;

        if (SUCCEEDED (CoCreateInstance (CLSID_MSDiscMasterObj, 0,
                                         CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER,
                                         IID_IDiscMaster,
                                         (void**) &dm)))
        {
            if (SUCCEEDED (dm->Open()))
            {
                IEnumDiscRecorders* drEnum = nullptr;

                if (SUCCEEDED (dm->EnumDiscRecorders (&drEnum)))
                {
                    IDiscRecorder* dr = nullptr;
                    DWORD dummy;
                    int index = 0;

                    while (drEnum->Next (1, &dr, &dummy) == S_OK)
                    {
                        if (indexToOpen == index)
                        {
                            result = dr;
                            break;
                        }
                        else if (list != nullptr)
                        {
                            BSTR path;

                            if (SUCCEEDED (dr->GetPath (&path)))
                                list->add ((const WCHAR*) path);
                        }

                        ++index;
                        dr->Release();
                    }

                    drEnum->Release();
                }

                if (master == 0)
                    dm->Close();
            }

            if (master != nullptr)
                *master = dm;
            else
                dm->Release();
        }

        return result;
    }
}

//==============================================================================
class AudioCDBurner::Pimpl  : public ComBaseClassHelper <IDiscMasterProgressEvents>,
                              public Timer
{
public:
    Pimpl (AudioCDBurner& owner_, IDiscMaster* discMaster_, IDiscRecorder* discRecorder_)
      : owner (owner_), discMaster (discMaster_), discRecorder (discRecorder_), redbook (0),
        listener (0), progress (0), shouldCancel (false)
    {
        HRESULT hr = discMaster->SetActiveDiscMasterFormat (IID_IRedbookDiscMaster, (void**) &redbook);
        jassert (SUCCEEDED (hr));
        hr = discMaster->SetActiveDiscRecorder (discRecorder);
        //jassert (SUCCEEDED (hr));

        lastState = getDiskState();
        startTimer (2000);
    }

    ~Pimpl()  {}

    void releaseObjects()
    {
        discRecorder->Close();
        if (redbook != nullptr)
            redbook->Release();
        discRecorder->Release();
        discMaster->Release();
        Release();
    }

    JUCE_COMRESULT QueryCancel (boolean* pbCancel)
    {
        if (listener != nullptr && ! shouldCancel)
            shouldCancel = listener->audioCDBurnProgress (progress);

        *pbCancel = shouldCancel;

        return S_OK;
    }

    JUCE_COMRESULT NotifyBlockProgress (long nCompleted, long nTotal)
    {
        progress = nCompleted / (float) nTotal;
        shouldCancel = listener != nullptr && listener->audioCDBurnProgress (progress);

        return E_NOTIMPL;
    }

    JUCE_COMRESULT NotifyPnPActivity (void)                              { return E_NOTIMPL; }
    JUCE_COMRESULT NotifyAddProgress (long /*nCompletedSteps*/, long /*nTotalSteps*/)    { return E_NOTIMPL; }
    JUCE_COMRESULT NotifyTrackProgress (long /*nCurrentTrack*/, long /*nTotalTracks*/)   { return E_NOTIMPL; }
    JUCE_COMRESULT NotifyPreparingBurn (long /*nEstimatedSeconds*/)      { return E_NOTIMPL; }
    JUCE_COMRESULT NotifyClosingDisc (long /*nEstimatedSeconds*/)        { return E_NOTIMPL; }
    JUCE_COMRESULT NotifyBurnComplete (HRESULT /*status*/)               { return E_NOTIMPL; }
    JUCE_COMRESULT NotifyEraseComplete (HRESULT /*status*/)              { return E_NOTIMPL; }

    class ScopedDiscOpener
    {
    public:
        ScopedDiscOpener (Pimpl& p) : pimpl (p) { pimpl.discRecorder->OpenExclusive(); }
        ~ScopedDiscOpener()                     { pimpl.discRecorder->Close(); }

    private:
        Pimpl& pimpl;

        JUCE_DECLARE_NON_COPYABLE (ScopedDiscOpener)
    };

    DiskState getDiskState()
    {
        const ScopedDiscOpener opener (*this);

        long type, flags;
        HRESULT hr = discRecorder->QueryMediaType (&type, &flags);

        if (FAILED (hr))
            return unknown;

        if (type != 0 && (flags & MEDIA_WRITABLE) != 0)
            return writableDiskPresent;

        if (type == 0)
            return noDisc;

        return readOnlyDiskPresent;
    }

    int getIntProperty (const LPOLESTR name, const int defaultReturn) const
    {
        ComSmartPtr<IPropertyStorage> prop;
        if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
            return defaultReturn;

        PROPSPEC iPropSpec;
        iPropSpec.ulKind = PRSPEC_LPWSTR;
        iPropSpec.lpwstr = name;

        PROPVARIANT iPropVariant;
        return FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant))
                   ? defaultReturn : (int) iPropVariant.lVal;
    }

    bool setIntProperty (const LPOLESTR name, const int value) const
    {
        ComSmartPtr<IPropertyStorage> prop;
        if (FAILED (discRecorder->GetRecorderProperties (prop.resetAndGetPointerAddress())))
            return false;

        PROPSPEC iPropSpec;
        iPropSpec.ulKind = PRSPEC_LPWSTR;
        iPropSpec.lpwstr = name;

        PROPVARIANT iPropVariant;
        if (FAILED (prop->ReadMultiple (1, &iPropSpec, &iPropVariant)))
            return false;

        iPropVariant.lVal = (long) value;
        return SUCCEEDED (prop->WriteMultiple (1, &iPropSpec, &iPropVariant, iPropVariant.vt))
                && SUCCEEDED (discRecorder->SetRecorderProperties (prop));
    }

    void timerCallback() override
    {
        const DiskState state = getDiskState();

        if (state != lastState)
        {
            lastState = state;
            owner.sendChangeMessage();
        }
    }

    AudioCDBurner& owner;
    DiskState lastState;
    IDiscMaster* discMaster;
    IDiscRecorder* discRecorder;
    IRedbookDiscMaster* redbook;
    AudioCDBurner::BurnProgressListener* listener;
    float progress;
    bool shouldCancel;
};

//==============================================================================
AudioCDBurner::AudioCDBurner (const int deviceIndex)
{
    IDiscMaster* discMaster = nullptr;
    IDiscRecorder* discRecorder = CDBurnerHelpers::enumCDBurners (0, deviceIndex, &discMaster);

    if (discRecorder != nullptr)
        pimpl = new Pimpl (*this, discMaster, discRecorder);
}

AudioCDBurner::~AudioCDBurner()
{
    if (pimpl != nullptr)
        pimpl.release()->releaseObjects();
}

StringArray AudioCDBurner::findAvailableDevices()
{
    StringArray devs;
    CDBurnerHelpers::enumCDBurners (&devs, -1, 0);
    return devs;
}

AudioCDBurner* AudioCDBurner::openDevice (const int deviceIndex)
{
    ScopedPointer<AudioCDBurner> b (new AudioCDBurner (deviceIndex));

    if (b->pimpl == 0)
        b = nullptr;

    return b.release();
}

AudioCDBurner::DiskState AudioCDBurner::getDiskState() const
{
    return pimpl->getDiskState();
}

bool AudioCDBurner::isDiskPresent() const
{
    return getDiskState() == writableDiskPresent;
}

bool AudioCDBurner::openTray()
{
    const Pimpl::ScopedDiscOpener opener (*pimpl);
    return SUCCEEDED (pimpl->discRecorder->Eject());
}

AudioCDBurner::DiskState AudioCDBurner::waitUntilStateChange (int timeOutMilliseconds)
{
    const int64 timeout = Time::currentTimeMillis() + timeOutMilliseconds;
    DiskState oldState = getDiskState();
    DiskState newState = oldState;

    while (newState == oldState && Time::currentTimeMillis() < timeout)
    {
        newState = getDiskState();
        Thread::sleep (jmin (250, (int) (timeout - Time::currentTimeMillis())));
    }

    return newState;
}

Array<int> AudioCDBurner::getAvailableWriteSpeeds() const
{
    Array<int> results;
    const int maxSpeed = pimpl->getIntProperty (L"MaxWriteSpeed", 1);
    const int speeds[] = { 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 64, 80 };

    for (int i = 0; i < numElementsInArray (speeds); ++i)
        if (speeds[i] <= maxSpeed)
            results.add (speeds[i]);

    results.addIfNotAlreadyThere (maxSpeed);
    return results;
}

bool AudioCDBurner::setBufferUnderrunProtection (const bool shouldBeEnabled)
{
    if (pimpl->getIntProperty (L"BufferUnderrunFreeCapable", 0) == 0)
        return false;

    pimpl->setIntProperty (L"EnableBufferUnderrunFree", shouldBeEnabled ? -1 : 0);
    return pimpl->getIntProperty (L"EnableBufferUnderrunFree", 0) != 0;
}

int AudioCDBurner::getNumAvailableAudioBlocks() const
{
    long blocksFree = 0;
    pimpl->redbook->GetAvailableAudioTrackBlocks (&blocksFree);
    return blocksFree;
}

String AudioCDBurner::burn (AudioCDBurner::BurnProgressListener* listener, bool ejectDiscAfterwards,
                            bool performFakeBurnForTesting, int writeSpeed)
{
    pimpl->setIntProperty (L"WriteSpeed", writeSpeed > 0 ? writeSpeed : -1);

    pimpl->listener = listener;
    pimpl->progress = 0;
    pimpl->shouldCancel = false;

    UINT_PTR cookie;
    HRESULT hr = pimpl->discMaster->ProgressAdvise ((AudioCDBurner::Pimpl*) pimpl, &cookie);

    hr = pimpl->discMaster->RecordDisc (performFakeBurnForTesting,
                                        ejectDiscAfterwards);

    String error;
    if (hr != S_OK)
    {
        const char* e = "Couldn't open or write to the CD device";

        if (hr == IMAPI_E_USERABORT)
            e = "User cancelled the write operation";
        else if (hr == IMAPI_E_MEDIUM_NOTPRESENT || hr == IMAPI_E_TRACKOPEN)
            e = "No Disk present";

        error = e;
    }

    pimpl->discMaster->ProgressUnadvise (cookie);
    pimpl->listener = 0;

    return error;
}

bool AudioCDBurner::addAudioTrack (AudioSource* audioSource, int numSamples)
{
    if (audioSource == 0)
        return false;

    ScopedPointer<AudioSource> source (audioSource);

    long bytesPerBlock;
    HRESULT hr = pimpl->redbook->GetAudioBlockSize (&bytesPerBlock);

    const int samplesPerBlock = bytesPerBlock / 4;
    bool ok = true;

    hr = pimpl->redbook->CreateAudioTrack ((long) numSamples / (bytesPerBlock * 4));

    HeapBlock<byte> buffer (bytesPerBlock);
    AudioSampleBuffer sourceBuffer (2, samplesPerBlock);
    int samplesDone = 0;

    source->prepareToPlay (samplesPerBlock, 44100.0);

    while (ok)
    {
        {
            AudioSourceChannelInfo info (&sourceBuffer, 0, samplesPerBlock);
            sourceBuffer.clear();

            source->getNextAudioBlock (info);
        }

        buffer.clear (bytesPerBlock);

        typedef AudioData::Pointer <AudioData::Int16, AudioData::LittleEndian,
                                    AudioData::Interleaved, AudioData::NonConst> CDSampleFormat;

        typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian,
                                    AudioData::NonInterleaved, AudioData::Const> SourceSampleFormat;

        CDSampleFormat left (buffer, 2);
        left.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (0)), samplesPerBlock);
        CDSampleFormat right (buffer + 2, 2);
        right.convertSamples (SourceSampleFormat (sourceBuffer.getReadPointer (1)), samplesPerBlock);

        hr = pimpl->redbook->AddAudioTrackBlocks (buffer, bytesPerBlock);

        if (FAILED (hr))
            ok = false;

        samplesDone += samplesPerBlock;

        if (samplesDone >= numSamples)
            break;
    }

    hr = pimpl->redbook->CloseAudioTrack();
    return ok && hr == S_OK;
}
