#include "FFmpeg.h"

#include <iostream>
#include <sstream>
#include <d3d11_1.h>

bool FFmpeg::ms_bFFmpegInstalled = false;

FFmpeg::FFmpeg(int fps, int crf, MODE mode)
 : m_pFILE(nullptr),
   m_pStaging(nullptr),
   m_width(-1),
   m_height(-1),
   m_mode(mode),
   m_fps(fps),
   m_crf(crf),
   m_frame(-1)
{
    static bool s_checkPerformed = false;

    m_timestamp[0].QuadPart = m_timestamp[1].QuadPart = 0;
    m_frequency.QuadPart = 0;
    m_startTime.QuadPart = 0;

    // Check once if ffmpeg.exe is installed, i.e. if it's in system path
    if (!s_checkPerformed) 
    {
        FILE* file = _popen("ffmpeg -version", "rt");
        if (_pclose(file) == 0)
        {
            ms_bFFmpegInstalled = true;
        }
        else 
        {
            ms_bFFmpegInstalled = false;
            std::cout << "Warning: ffmpeg.exe not found, video recording disabled..." << std::endl;
        }

        s_checkPerformed = true;
    }
}

FFmpeg::~FFmpeg()
{
    StopRecording();
}


HRESULT FFmpeg::StartRecording(ID3D11Device* pd3dDevice, ID3D11RenderTargetView* pRenderTargetView, char* filename)
{
    if (!ms_bFFmpegInstalled) { return S_OK; }

    HRESULT hr;
    std::stringstream cmd;

    // 1) Create a staging texture from the non-MSAA source
    ID3D11Resource* pResource = nullptr;
    pRenderTargetView->GetResource(&pResource);

    ID3D11Texture2D* pTexture = nullptr;
    hr = pResource->QueryInterface( __uuidof(ID3D11Texture2D), (void**) &pTexture );
    if (hr != S_OK) { goto StartRecording_Return; }
                    
    D3D11_TEXTURE2D_DESC desc;
    pTexture->GetDesc( &desc );

    if (desc.Format != DXGI_FORMAT_R8G8B8A8_TYPELESS   &&
        desc.Format != DXGI_FORMAT_R8G8B8A8_UNORM      &&
        desc.Format != DXGI_FORMAT_R8G8B8A8_UNORM_SRGB &&
        desc.Format != DXGI_FORMAT_R8G8B8A8_UINT       &&
        desc.Format != DXGI_FORMAT_R8G8B8A8_SNORM      &&
        desc.Format != DXGI_FORMAT_R8G8B8A8_SINT)
    {
        std::cout << "FFmpeg error: Currently only R8G8B8A8 render targets are supported." << std::endl;
        hr = E_INVALIDARG;
        goto StartRecording_Return;
    }

    desc.BindFlags = 0;
    desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    desc.Usage = D3D11_USAGE_STAGING;

    hr = pd3dDevice->CreateTexture2D( &desc, 0, &m_pStaging );
    if (hr != S_OK) { goto StartRecording_Return; }

    // 2) Spawn ffmpeg process    
    // For h264 control options, see https://trac.ffmpeg.org/wiki/Encode/H.264
    cmd << "ffmpeg "
           "-f rawvideo -pix_fmt rgba " // raw rgba input video format
           "-s " << desc.Width << "x" << desc.Height << " " // input resolution
           "-r " << m_fps << " " // input framerate
           " -i - " // read input from stdin
           "-y " // auto-overwrite output file
           "-codec:video libx264 " // use h264 codec
           "-preset fast " // encoder preset (ultrafast,superfast, veryfast, faster, fast, medium, slow, slower, veryslow)
           "-crf " << m_crf << " " // constant rate factor
        << filename;

    // open pipe to ffmpeg's stdin in binary write mode
    m_pFILE = _popen(cmd.str().c_str(), "wb");

    // 3) Allocate buffers
    m_width  = desc.Width;
    m_height = desc.Height;
    m_buffer[0].resize(m_width * m_height * sizeof(uint32_t));
    m_buffer[1].resize(m_width * m_height * sizeof(uint32_t));
    m_buffer[2].resize(m_width * m_height * sizeof(uint32_t));
    m_frame = -1;

    StartRecording_Return:
    if (pResource) {pResource->Release(); pResource = nullptr; }
    if (pTexture ) {pTexture ->Release(); pTexture  = nullptr; }
    return hr;   
}


void FFmpeg::StopRecording()
{
    // Release everything and clean up
    if (!ms_bFFmpegInstalled) { return; }

    if (m_pFILE) { _pclose(m_pFILE); m_pFILE = nullptr; }
    
    if (m_pStaging) {m_pStaging ->Release(); m_pStaging  = nullptr; }

    m_buffer[0].swap(std::vector<uint8_t>());
    m_buffer[1].swap(std::vector<uint8_t>());
    m_buffer[2].swap(std::vector<uint8_t>());
    m_timestamp[0].QuadPart = m_timestamp[1].QuadPart = 0;
    m_frequency.QuadPart = 0;
    m_startTime.QuadPart = 0;
    
    m_width  = -1;
    m_height = -1;
    m_frame  = -1;
}

HRESULT FFmpeg::AddFrame(ID3D11DeviceContext* pd3dContext, ID3D11RenderTargetView* pRenderTargetView)
{
    if (!ms_bFFmpegInstalled) { return S_OK; }
        
    HRESULT hr;

    // 1) Copy render target to staging texture
    ID3D11Resource* pTex2D = nullptr;
    pRenderTargetView->GetResource(&pTex2D);
    pd3dContext->CopyResource(m_pStaging, pTex2D);
    pTex2D->Release();

    // 2) Download staging texture from GPU to buffer
    m_buffer[0].swap(m_buffer[1]);
    uint32_t* pixels = (uint32_t*)(m_buffer[1].data());

    D3D11_MAPPED_SUBRESOURCE mapped;
    hr = pd3dContext->Map(m_pStaging, 0, D3D11_MAP_READ, 0, &mapped);
    if (hr == S_OK)
    {
        for (int y = 0; y < m_height; y++)
        {
            uint32_t* pRowData = (uint32_t*)mapped.pData + y * (mapped.RowPitch / sizeof(uint32_t));
            for (int x = 0; x < m_width; x++)
            {
                pixels[y * m_width + x] = pRowData[x];
            }
        }

        pd3dContext->Unmap(m_pStaging, 0);
    }
        
    // 3) Query timestamps and write first frame to ffmpeg
    if (m_frame == -1)
    {
        QueryPerformanceFrequency(&m_frequency);
        QueryPerformanceCounter(&m_startTime);
        m_timestamp[0].QuadPart = m_timestamp[1].QuadPart = 0;
        
        fwrite(m_buffer[1].data(), 1, m_buffer[1].size(), m_pFILE);
        m_frame ++;
    }
    else
    {
        m_timestamp[0] = m_timestamp[1];
        QueryPerformanceCounter(&m_timestamp[1]);
        m_timestamp[1].QuadPart -= m_startTime.QuadPart;
    }

    // 4) Write frame(s) to ffmpeg depending on recording mode
    if (m_mode == MODE_FRAME_COPY)
    {
        fwrite(m_buffer[1].data(), 1, m_buffer[1].size(), m_pFILE);
    }
    else
    {
        while (double(m_frame + 1) * m_frequency.QuadPart / m_fps <= m_timestamp[1].QuadPart)
        {
            if (m_mode == MODE_INTERPOLATE)
            {
                double t = (double(m_frame + 1) * m_frequency.QuadPart / m_fps - m_timestamp[0].QuadPart)
                         / (m_timestamp[1].QuadPart - m_timestamp[0].QuadPart);

                // interpolate
                for (size_t i = 0; i < m_buffer[0].size(); i++)
                {
                    m_buffer[2][i] = uint8_t((1 - t) * m_buffer[0][i] + t * m_buffer[1][i]);
                }    

                fwrite(m_buffer[2].data(), 1, m_buffer[2].size(), m_pFILE);
            }
            else if (m_mode == MODE_DUPLICATE)
            {
                fwrite(m_buffer[1].data(), 1, m_buffer[1].size(), m_pFILE);
            }

            m_frame ++;
        }
    }

    return hr;
}

