//
//  VolumeControl.cpp
//
//  Controls system audio volume.
//

#include "VolumeControl.h"

#include "math/Misc.h"
#include "Log.h"

#if defined(_RPI_)
#include "Settings.h"
#endif

#if defined(_WIN64)
#include <mmdeviceapi.h>
#endif

// The ALSA Audio Card and Audio Device selection code is disabled at the moment.
// As PulseAudio controls the sound devices for the desktop environment, it doesn't
// make much sense to be able to select ALSA devices directly. Normally (always?)
// the selection doesn't make any difference at all. But maybe some PulseAudio
// settings could be added later on, if needed.
// The code is still active for Raspberry Pi though as I'm not sure if this is
// useful for that device.
// Keeping mixerName and mixerCard at their default values should make sure that
// the rest of the volume control code in here compiles and works fine.
#if defined(__linux__)
#if defined(_RPI_) || defined(_VERO4K_)
const char * VolumeControl::mixerName = "PCM";
#else
const char * VolumeControl::mixerName = "Master";
#endif
const char * VolumeControl::mixerCard = "default";
#endif

std::weak_ptr<VolumeControl> VolumeControl::sInstance;

VolumeControl::VolumeControl()
        : originalVolume(0),
        internalVolume(0)
        #if defined(__APPLE__)
//        #error TODO: Not implemented for MacOS yet!!!
        #elif defined(__linux__)
        , mixerIndex(0),
        mixerHandle(nullptr),
        mixerElem(nullptr),
        mixerSelemId(nullptr)
        #elif defined(_WIN64)
        , mixerHandle(nullptr),
        endpointVolume(nullptr)
        #endif
{
    init();

    // Get original volume levels for system.
    originalVolume = getVolume();
}

VolumeControl::VolumeControl(
        const VolumeControl & right):
        originalVolume(0),
        internalVolume(0)
        #if defined(__APPLE__)
//        #error TODO: Not implemented for MacOS yet!!!
        #elif defined(__linux__)
        , mixerIndex(0),
        mixerHandle(nullptr),
        mixerElem(nullptr),
        mixerSelemId(nullptr)
        #elif defined(_WIN64)
        , mixerHandle(nullptr),
        endpointVolume(nullptr)
        #endif
{
    (void)right;
    sInstance = right.sInstance;
}

VolumeControl & VolumeControl::operator=(const VolumeControl& right)
{
    if (this != &right)
        sInstance = right.sInstance;

    return *this;
}

VolumeControl::~VolumeControl()
{
    // Set original volume levels for system.
    //setVolume(originalVolume);

    deinit();
}

std::shared_ptr<VolumeControl> & VolumeControl::getInstance()
{
    // Check if an VolumeControl instance is already created, if not create one.
    static std::shared_ptr<VolumeControl> sharedInstance = sInstance.lock();
    if (sharedInstance == nullptr) {
        sharedInstance.reset(new VolumeControl);
        sInstance = sharedInstance;
    }
    return sharedInstance;
}

void VolumeControl::init()
{
    // Initialize audio mixer interface.
    #if defined(__APPLE__)
//    #error TODO: Not implemented for MacOS yet!!!
    #elif defined(__linux__)
    // Try to open mixer device.
    if (mixerHandle == nullptr) {
        // Allow user to override the AudioCard and AudioDevice in es_settings.cfg.
        #if defined(_RPI_)
        mixerCard = Settings::getInstance()->getString("AudioCard").c_str();
        mixerName = Settings::getInstance()->getString("AudioDevice").c_str();
        #endif

        snd_mixer_selem_id_alloca(&mixerSelemId);
        // Sets simple-mixer index and name.
        snd_mixer_selem_id_set_index(mixerSelemId, mixerIndex);
        snd_mixer_selem_id_set_name(mixerSelemId, mixerName);
        // Open mixer.
        if (snd_mixer_open(&mixerHandle, 0) >= 0) {
            LOG(LogDebug) << "VolumeControl::init() - Opened ALSA mixer";
            // Ok, attach to defualt card.
            if (snd_mixer_attach(mixerHandle, mixerCard) >= 0) {
                LOG(LogDebug) << "VolumeControl::init() - Attached to default card";
                // Ok, register simple element class.
                if (snd_mixer_selem_register(mixerHandle, nullptr, nullptr) >= 0) {
                    LOG(LogDebug) << "VolumeControl::init() - Registered simple element class";
                    // Ok, load registered elements.
                    if (snd_mixer_load(mixerHandle) >= 0) {
                        LOG(LogDebug) << "VolumeControl::init() - Loaded mixer elements";
                        // Ok, find elements now.
                        mixerElem = snd_mixer_find_selem(mixerHandle, mixerSelemId);
                        if (mixerElem != nullptr) {
                            // Wohoo. good to go...
                            LOG(LogDebug) << "VolumeControl::init() - Mixer initialized";
                        }
                        else {
                            LOG(LogError) <<
                                    "VolumeControl::init() - Failed to find mixer elements!";
                            snd_mixer_close(mixerHandle);
                            mixerHandle = nullptr;
                        }
                    }
                    else {
                        LOG(LogError) << "VolumeControl::init() - Failed to load mixer elements!";
                        snd_mixer_close(mixerHandle);
                        mixerHandle = nullptr;
                    }
                }
                else {
                    LOG(LogError) <<
                            "VolumeControl::init() - Failed to register simple element class!";
                    snd_mixer_close(mixerHandle);
                    mixerHandle = nullptr;
                }
            }
            else {
                LOG(LogError) << "VolumeControl::init() - Failed to attach to default card!";
                snd_mixer_close(mixerHandle);
                mixerHandle = nullptr;
            }
        }
        else {
            LOG(LogError) << "VolumeControl::init() - Failed to open ALSA mixer!";
        }
    }
    #elif defined(_WIN64)
    // Get windows version information.
    OSVERSIONINFOEXA osVer = {sizeof(OSVERSIONINFO)};
    ::GetVersionExA(reinterpret_cast<LPOSVERSIONINFOA>(&osVer));
    // Check windows version.
    if (osVer.dwMajorVersion < 6) {
        // Windows older than Vista. use mixer API. open default mixer.
        if (mixerHandle == nullptr) {
            #if defined(_WIN64)
            if (mixerOpen(&mixerHandle, 0, (DWORD_PTR)nullptr, 0, 0) == MMSYSERR_NOERROR) {
            #else
            if (mixerOpen(&mixerHandle, 0, nullptr, 0, 0) == MMSYSERR_NOERROR) {
            #endif
                // Retrieve info on the volume slider control for the "Speaker Out" line.
                MIXERLINECONTROLS mixerLineControls;
                mixerLineControls.cbStruct = sizeof(MIXERLINECONTROLS);
                mixerLineControls.dwLineID = 0xFFFF0000; // Id of "Speaker Out" line.
                mixerLineControls.cControls = 1;
                // Id of "Speaker Out" line's volume slider.
                //mixerLineControls.dwControlID = 0x00000000;
                 //Get volume control.
                mixerLineControls.dwControlType = MIXERCONTROL_CONTROLTYPE_VOLUME;
                mixerLineControls.pamxctrl = &mixerControl;
                mixerLineControls.cbmxctrl = sizeof(MIXERCONTROL);
                if (mixerGetLineControls((HMIXEROBJ)mixerHandle, &mixerLineControls,
                        MIXER_GETLINECONTROLSF_ONEBYTYPE) != MMSYSERR_NOERROR) {
                    LOG(LogError) <<
                            "VolumeControl::getVolume() - Failed to get mixer volume control!";
                    mixerClose(mixerHandle);
                    mixerHandle = nullptr;
                }
            }
            else {
                LOG(LogError) << "VolumeControl::init() - Failed to open mixer!";
            }
        }
    }
    else {
        // Windows Vista or above. use EndpointVolume API. get device enumerator.
        if (endpointVolume == nullptr) {
            CoInitialize(nullptr);
            IMMDeviceEnumerator * deviceEnumerator = nullptr;
            CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
                    __uuidof(IMMDeviceEnumerator), (LPVOID *)&deviceEnumerator);
            if (deviceEnumerator != nullptr) {
                // Get default endpoint.
                IMMDevice * defaultDevice = nullptr;
                deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &defaultDevice);
                if (defaultDevice != nullptr) {
                    // Retrieve endpoint volume.
                    defaultDevice->Activate(__uuidof(IAudioEndpointVolume),
                            CLSCTX_INPROC_SERVER, nullptr, (LPVOID *)&endpointVolume);
                    if (endpointVolume == nullptr)
                        LOG(LogError) << "VolumeControl::init() - "
                                "Failed to get default audio endpoint volume!";
                    // Release default device. we don't need it anymore.
                    defaultDevice->Release();
                }
                else {
                    LOG(LogError) <<
                            "VolumeControl::init() - Failed to get default audio endpoint!";
                }
                // Release device enumerator. we don't need it anymore.
                deviceEnumerator->Release();
            }
            else {
                LOG(LogError) << "VolumeControl::init() - Failed to get audio endpoint enumerator!";
                CoUninitialize();
            }
        }
    }
    #endif
}

void VolumeControl::deinit()
{
    // Deinitialize audio mixer interface.
    #if defined(__APPLE__)
//    #error TODO: Not implemented for MacOS yet!!!
    #elif defined(__linux__)
    if (mixerHandle != nullptr) {
        snd_mixer_detach(mixerHandle, mixerCard);
        snd_mixer_free(mixerHandle);
        snd_mixer_close(mixerHandle);
        mixerHandle = nullptr;
        mixerElem = nullptr;
    }
    #elif defined(_WIN64)
    if (mixerHandle != nullptr) {
        mixerClose(mixerHandle);
        mixerHandle = nullptr;
    }
    else if (endpointVolume != nullptr) {
        endpointVolume->Release();
        endpointVolume = nullptr;
        CoUninitialize();
    }
    #endif
}

int VolumeControl::getVolume() const
{
    int volume = 0;

    #if defined(__APPLE__)
//    #error TODO: Not implemented for MacOS yet!!!
    #elif defined(__linux__)
    if (mixerElem != nullptr) {
        // Get volume range.
        long minVolume;
        long maxVolume;
        if (snd_mixer_selem_get_playback_volume_range(mixerElem, &minVolume, &maxVolume) == 0) {
            // Ok, now get volume.
            long rawVolume;
            if (snd_mixer_selem_get_playback_volume(mixerElem,
                    SND_MIXER_SCHN_MONO, &rawVolume) == 0) {
                // Worked. bring into range 0-100.
                rawVolume -= minVolume;
                if (rawVolume > 0)
                    volume = (rawVolume * 100.0) / (maxVolume - minVolume) + 0.5;
                //else
                //   volume = 0;
            }
            else {
                LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume!";
            }
        }
        else {
            LOG(LogError) << "VolumeControl::getVolume() - Failed to get volume range!";
        }
    }
    #elif defined(_WIN64)
    if (mixerHandle != nullptr) {
        // Windows older than Vista. use mixer API. get volume from line control.
        MIXERCONTROLDETAILS_UNSIGNED value;
        MIXERCONTROLDETAILS mixerControlDetails;
        mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
        mixerControlDetails.dwControlID = mixerControl.dwControlID;
        // Always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control.
        mixerControlDetails.cChannels = 1;
        // Always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control.
        mixerControlDetails.cMultipleItems = 0;
        mixerControlDetails.paDetails = &value;
        mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
        if (mixerGetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails,
                MIXER_GETCONTROLDETAILSF_VALUE) == MMSYSERR_NOERROR)
            volume = (int)Math::round((value.dwValue * 100) / 65535.0f);
        else
            LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume!";
    }
    else if (endpointVolume != nullptr) {
        // Windows Vista or above. use EndpointVolume API.
        float floatVolume = 0.0f; // 0-1
        if (endpointVolume->GetMasterVolumeLevelScalar(&floatVolume) == S_OK) {
            volume = (int)Math::round(floatVolume * 100.0f);
            LOG(LogInfo) << "System audio volume is " << volume;
        }
        else {
            LOG(LogError) << "VolumeControl::getVolume() - Failed to get master volume!";
        }
    }
    #endif

    // Clamp to 0-100 range.
    if (volume < 0)
        volume = 0;
    if (volume > 100)
        volume = 100;
    return volume;
}

void VolumeControl::setVolume(int volume)
{
    // Clamp to 0-100 range.
    if (volume < 0)
        volume = 0;
    if (volume > 100)
        volume = 100;

    // Store values in internal variables.
    internalVolume = volume;
    #if defined(__APPLE__)
//    #error TODO: Not implemented for MacOS yet!!!
    #elif defined(__linux__)
    if (mixerElem != nullptr) {
        // Get volume range.
        long minVolume;
        long maxVolume;
        if (snd_mixer_selem_get_playback_volume_range(mixerElem, &minVolume, &maxVolume) == 0) {
            // Ok, bring into minVolume-maxVolume range and set.
            long rawVolume = (volume * (maxVolume - minVolume) / 100) + minVolume;
            if (snd_mixer_selem_set_playback_volume(mixerElem,
                    SND_MIXER_SCHN_FRONT_LEFT, rawVolume) < 0 ||
                    snd_mixer_selem_set_playback_volume(mixerElem,
                    SND_MIXER_SCHN_FRONT_RIGHT, rawVolume) < 0) {
                LOG(LogError) << "VolumeControl::getVolume() - Failed to set mixer volume!";
            }
        }
        else {
            LOG(LogError) << "VolumeControl::getVolume() - Failed to get volume range!";
        }
    }
    #elif defined(_WIN64)
    if (mixerHandle != nullptr) {
        // Windows older than Vista. use mixer API. get volume from line control.
        MIXERCONTROLDETAILS_UNSIGNED value;
        value.dwValue = (volume * 65535) / 100;
        MIXERCONTROLDETAILS mixerControlDetails;
        mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
        mixerControlDetails.dwControlID = mixerControl.dwControlID;
        // Always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control.
        mixerControlDetails.cChannels = 1;
        // Always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control.
        mixerControlDetails.cMultipleItems = 0;
        mixerControlDetails.paDetails = &value;
        mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
        if (mixerSetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails,
                MIXER_SETCONTROLDETAILSF_VALUE) != MMSYSERR_NOERROR)
            LOG(LogError) << "VolumeControl::setVolume() - Failed to set mixer volume!";
    }
    else if (endpointVolume != nullptr) {
        // Windows Vista or above. use EndpointVolume API.
        float floatVolume = 0.0f; // 0-1
        if (volume > 0)
            floatVolume = (float)volume / 100.0f;
        if (endpointVolume->SetMasterVolumeLevelScalar(floatVolume, nullptr) != S_OK)
            LOG(LogError) << "VolumeControl::setVolume() - Failed to set master volume!";
    }
    #endif
}