#include <iostream>
#include <fstream>
#include <cctype>
#include <codecvt>
#include <filesystem>

#include "dubbing_sdk_device.h"

#include "idubbing_engine.h"
#include "dubbing_global.h"
#include "my_dubbing_callBack.h"
#include "utils.h"
#include "device_transform_data_observer.h"

using namespace DUBBING;

void initEngineConfig(DUBBING::EngineConfig& engineConfig, const std::string& exePath, IDubbbingCallBack* idubbbingCallBack = nullptr)
{
    std::string resourcePath = exePath + "/dubbing_resource";
    std::string token = "access_key=\"xxxxxx\",timestamp=\"xxxxxx\",nonce=\"xxxxxx\",id=\"xxxxxx\",signature=\"xxxxxx\"";
    engineConfig.token(token)
        .resourcePath(resourcePath)
        .enableLog(true)
        .channel(1)
        .format(AUDIO_PCM_S16)
        .sampleRate(ma_standard_sample_rate_48000)
        .isSync(true)
        .dubbbingCallBack(idubbbingCallBack);
}

void transformDeviceAudio(const std::string& exePath)
{
    DeviceTransformDataObserver deviceDataObserver;

    DubbingSdkDevice dubbingSdkDevice;
    dubbingSdkDevice.registerDeviceDataObserver(&deviceDataObserver);

    //init engine config
    MyDubbbingCallBack myDubbbingCallBack;
    DUBBING::EngineConfig engineConfig;
    initEngineConfig(engineConfig, exePath, &myDubbbingCallBack);

    std::vector<MiniDeviceInfo> deviceInfoList = DubbingSdkDevice::getPlaybackDeviceInfoList();
    std::vector<MiniDeviceInfo> deviceInfoList2 = DubbingSdkDevice::getCaptureDeviceInfoList();
    std::cout << "======playback device list======" << std::endl;
    for (int i = 0; i < deviceInfoList.size(); i++)
    {
        std::cout << "id:  " << deviceInfoList[i].m_id.wasapi;
        std::cout << "  name:  " << deviceInfoList[i].m_name << std::endl;
    }
    std::cout << "======capture device list======" << std::endl;
    for (int i = 0; i < deviceInfoList2.size(); i++)
    {
        std::cout << "id:  " << deviceInfoList2[i].m_id.wasapi;
        std::cout << "  name:  " << deviceInfoList2[i].m_name << std::endl;
    }

    ma_device_config playbackConfig;
    dubbingSdkDevice.initDeviceConfig(playbackConfig, ma_device_type_playback, &deviceInfoList[0].m_id, nullptr);
    dubbingSdkDevice.modifyPlaybackDeviceConfig(playbackConfig, (ma_format)(engineConfig.getFormat() + 1), engineConfig.getChannel());
    dubbingSdkDevice.modifyDeviceConfig(playbackConfig, engineConfig.getSampleRate(), engineConfig.getSampleRate() / 100);
    ma_device* pPlaybackDevice = dubbingSdkDevice.startDevice(playbackConfig);
    ma_device_config captureConfig;
    dubbingSdkDevice.initDeviceConfig(captureConfig, ma_device_type_capture, nullptr, &deviceInfoList2[1].m_id);
    dubbingSdkDevice.modifyCaptureDeviceConfig(captureConfig, (ma_format)(engineConfig.getFormat() + 1), engineConfig.getChannel());
    dubbingSdkDevice.modifyDeviceConfig(captureConfig, engineConfig.getSampleRate(), engineConfig.getSampleRate() / 100);
    ma_device* pCaptureDevice = dubbingSdkDevice.startDevice(captureConfig);

    DUBBING::IDubbingEngine* engine = createDubbingEngine(engineConfig);
    myDubbbingCallBack.setDubbingEngine(engine);
    engine->checkResources();
    engine->prepare();
    while (engine->getEngineStatus() != PREPARED)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    // get voice list
    std::string voices = engine->getVoiceList();
    printf(voices.c_str());
    // The conversion work can only be carried out after successfully setting the voice
    int voiceId = 0;
    std::cout << "please input voice id: ";
    std::cin >> voiceId;
    engine->setVoice(voiceId);
    deviceDataObserver.setIDubbingEngine(engine);

    bool exit = false;
    while (!exit)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));
        int cmdIndex;
        std::cout << std::endl;
        std::cout << std::endl;
        std::cout << "===========help=============" << std::endl;
        std::cout << "0: getCalibrationFile" << std::endl;
        std::cout << "1: proCalibration" << std::endl;
        std::cout << "2: getIntonation" << std::endl;
        std::cout << "3: getPitch" << std::endl;
        std::cout << "4: getMode" << std::endl;
        std::cout << "5: getVoiceList" << std::endl;
        std::cout << "6: startRecordCalibrationFile" << std::endl;
        std::cout << "7: stopRecordCalibrationFile" << std::endl;
        std::cout << "8: setMode" << std::endl;
        std::cout << "9: setVoice" << std::endl;
        std::cout << "10: getCurrentVoice" << std::endl;
        std::cout << "11: openProMode" << std::endl;
        std::cout << "12: closeProMode" << std::endl;
        std::cout << "13: quit" << std::endl;
        std::cout << "please input cmd index: ";
        std::cin >> cmdIndex;
        std::cout << std::endl;
        std::cout << std::endl;
        switch (cmdIndex)
        {
        case 0:
        {
            if (engine)
            {
                std::string filePath = engine->getCalibrationFile();
                std::cout << "calibration file: " << filePath << std::endl;
            }
            break;
        }
        case 1:
        {
            if (engine)
            {
                engine->proCalibration();
            }
            break;
        }
        case 2:
        {
            if (engine)
            {
                float intonation = engine->getIntonation();
                std::cout << "intonation: " << intonation << std::endl;
            }
            break;
        }
        case 3:
        {
            if (engine)
            {
                float pitch = engine->getPitch();
                std::cout << "pitch: " << pitch << std::endl;
            }
            break;
        }
        case 4:
        {
            if (engine)
            {
                std::string mode = engine->getMode() == NORMAL_MODE ? "NORMAL_MODE" : "PRO_MODE";
                std::cout << "mode: " << mode << std::endl;
            }
            break;
        }
        case 5:
        {
            if (engine)
            {
                std::string voiceList = engine->getVoiceList();
                std::wstring wVoiceList = utf8string2wstring(voiceList);
                std::cout << "voice list: " << voiceList << std::endl;
                wprintf(L"%s", wVoiceList.c_str());
            }
            break;
        }
        case 6:
        {
            deviceDataObserver.startRecordCalibrationFile();
            break;
        }
        case 7:
        {
            deviceDataObserver.stopRecordCalibrationFile();
            break;
        }
        case 8:
        {
            int mode = NORMAL_MODE;
            std::cout << "input mode:";
            std::cin >> mode;
            if (mode != PRO_MODE)
            {
                mode = NORMAL_MODE;
            }
            float intonation = 0;
            std::cout << "input intonation:";
            std::cin >> intonation;

            float pitch = 0;
            std::cout << "input pitch:";
            std::cin >> pitch;
            if (engine)
            {
                engine->setMode((DubbingMode)mode, intonation, pitch);
            }
            break;
        }
        case 9:
        {
            int voiceId = 0;
            std::cout << "input voice id:";
            std::cin >> voiceId;
            if (engine)
            {
                engine->setVoice(voiceId);
            }
            break;
        }
        case 10:
        {
            int voiceId = 0;
            if (engine)
            {
                voiceId = engine->getCurrentVoice();
                std::cout << "current voice id:" << voiceId << std::endl;
            }
            break;
        }
        case 11:
        {
            if (engine)
            {
                float intonation = engine->getIntonation();
                float pitch = engine->getPitch();
                engine->setMode(PRO_MODE, intonation, pitch);
            }
            break;
        }
        case 12:
        {
            if (engine)
            {
                float intonation = engine->getIntonation();
                float pitch = engine->getPitch();
                engine->setMode(NORMAL_MODE, intonation, pitch);
            }
            break;
        }
        case 13:
        {
            exit = true;
            break;
        }
        default:
            break;
        }
    }

    deviceDataObserver.setIDubbingEngine(nullptr);
    dubbingSdkDevice.stopDevice(pPlaybackDevice);
    dubbingSdkDevice.stopDevice(pCaptureDevice);
    engine->engineRelease();
    destroyDubbingEngine(engine);
}

void transformWavFile(const std::string& exePath)
{
    std::ofstream outFile;
    outFile.open("transform.wav", std::ios::out | std::ios::binary);
    bool isSuccess;

    MyDubbbingCallBack myDubbbingCallBack;
    DUBBING::EngineConfig engineConfig;
    initEngineConfig(engineConfig, exePath, &myDubbbingCallBack);
    DUBBING::IDubbingEngine* engine = createDubbingEngine(engineConfig);
    myDubbbingCallBack.setDubbingEngine(engine);
    engine->prepare();
    while (engine->getEngineStatus() != PREPARED)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    // get voice list
    std::string voices = engine->getVoiceList();
    printf(voices.c_str());
    // The conversion work can only be carried out after successfully setting the voice
    int voiceId = 0;
    std::cout << "please input voice id: ";
    std::cin >> voiceId;
    engine->setVoice(voiceId);
    while (!myDubbbingCallBack.hasSetVoice())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    std::ifstream audioFile;
    audioFile.open("48k_1_16.wav", std::ios::in | std::ios::binary);
    if (audioFile.is_open())
    {
        char head[44];
        audioFile.read(head, sizeof(head));
        outFile.write(head, sizeof(head));
        int nCount = 0;
        while (!audioFile.eof())
        {
            nCount++;

            const int len = 480 * 16;
            char data[len];
            audioFile.read(data, sizeof(data));
            int readSize = audioFile.gcount();
            uint64_t startTime = getCurTime();
            isSuccess = engine->transform(data, readSize);
            uint64_t endTime = getCurTime();
            if (endTime - startTime > 20)
            {
                std::cout << "transfrom time:" << endTime - startTime << std::endl;
            }
            if (!engineConfig.getIsSync())
            {
                std::this_thread::sleep_for(std::chrono::milliseconds(5));
            }
            if (isSuccess)
            {
                outFile.write(data, readSize);
            }
        }
    }

    audioFile.close();
    outFile.close();
    destroyDubbingEngine(engine);
}

int main(int argc, char** argv)
{
    std::string exePath = argv[0];
    int nPos = exePath.rfind("/");
    if (nPos == std::string::npos)
    {
        nPos = exePath.rfind("\\");
    }
    if (nPos != std::string::npos)
    {
        exePath = exePath.substr(0, nPos);
    }
    int type = 0;
    std::cout << std::endl;
    std::cout << std::endl;
    std::cout << "===========help=============" << std::endl;
    std::cout << "0: transformDeviceAudio [defalut]" << std::endl;
    std::cout << "1: transformWavFile" << std::endl;
    std::cout << "please input transform type index: ";
    std::cin >> type;
    std::cout << std::endl;
    std::cout << std::endl;
    if (type == 1)
    {
        transformWavFile(exePath);
    }
    else
    {
        transformDeviceAudio(exePath);
    }
}
