#include "DSMediaView.h"
#include "MediaItem.h"
#include <cnoid/TimeBar>
#include <cnoid/Archive>
#include <cnoid/ConnectionSet>
#include <cnoid/MessageView>
#include <cnoid/ViewManager>
#include <cnoid/RootItem>
#include <cnoid/ItemList>
#include <cnoid/MenuManager>
#include <cnoid/MainMenu>
#include <cnoid/Sleep>
#include <cnoid/Format>
#include <cnoid/UTF8>
#include <filesystem>
#include <QPainter>
#include <QWindow>
#include <Dshow.h>
#include <errors.h>
#include <iostream>
#include <algorithm>
#include <windows.h>
#include <mbctype.h>
#include "gettext.h"

using namespace std;
using namespace cnoid;

namespace {
const bool TRACE_FUNCTIONS = false;

bool initialized = false;

Action* aspectRatioCheck = nullptr;
Action* orgSizeCheck = nullptr;

const UINT WM_DSHOW_NOTIFY = WM_USER + 21;

}

namespace cnoid {

class DSMediaViewImpl
{
public:
    DSMediaViewImpl(DSMediaView* self);
    ~DSMediaViewImpl();

    DSMediaView* self;
    MessageView* mv;
    ScopedConnection rootItemConnection;
    ScopedConnectionSet timeBarConnections;
    MediaItemPtr currentMediaItem;

    enum PLAYSTATE { Empty, Stopped, Paused, Running } currentState;

    // DirectShow interfaces
    IGraphBuilder*   graphBuilder;
    IMediaControl*   mediaControl;
    IMediaEventEx*   mediaEvent;
    IMediaSeeking*   mediaSeeking;
    IVideoWindow*    videoWindow;
    IBasicVideo*     basicVideo;
    IVideoFrameStep* videoFrameStep;

    // windows handle
    HWND hwnd;

    bool bMapped;
    bool bLoaded;
    bool bCanSeek;

    // Original Window Procedure
    WNDPROC orgWinProc;

    void onWindowIdChanged();
    void onAspectRatioCheckToggled();
    void onOrgSizeCheckToggled();
    void onItemCheckToggled(Item* item, bool isChecked);
    void initializeScreenWindow();
    void clearScreenWindow();
    void load();
    void unload();
    HRESULT adjustVideoWindow();
    void connectTimeBarSignals();
    bool onPlaybackInitialized(double time);
    void onPlaybackStarted(double time);
    bool onTimeChanged(double time);
    double onPlaybackStopped(double time, bool isStoppedManually);
    HRESULT seekMedia(double time);
    HRESULT playMedia();
    HRESULT pauseMedia();
    HRESULT stopMedia();
    HRESULT HandleDShowEvent();
    bool storeState(Archive& archive);
    bool restoreState(const Archive& archive);
};

}


bool DSMediaView::initialize(ExtensionManager* ext)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::initialize()" << endl;
    }
    
    if(!initialized){

        if(!SUCCEEDED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))){
            MessageView::mainInstance()->putln(_("CoInitialize failed. MediaView is not available."));
            return false;
        }

        ext->viewManager().registerClass<DSMediaView>(
            N_("MediaView"), N_("Media"), ViewManager::SINGLE_OPTIONAL);

        if(auto optionsMenu = MainMenu::instance()->get_Options_Menu()){
            MenuManager& mm = ext->menuManager();
            mm.setCurrent(optionsMenu).setPath(N_("Media View"));
            aspectRatioCheck = mm.addCheckItem(_("Keep Aspect Ratio"));
            orgSizeCheck = mm.addCheckItem(_("Keep Original Size"));
        } else {
            aspectRatioCheck = new Action;
            orgSizeCheck = new Action;
        }
        aspectRatioCheck->setChecked(true);
        orgSizeCheck->setChecked(true);

        initialized = true;
    }

    return initialized;
}


void DSMediaView::finalize()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::finalize()" << endl;
    }
    
    if(initialized){
        CoUninitialize();
    }
}


DSMediaView::DSMediaView()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::DSMediaView()" << endl;
    }
    
    setName(N_("Media"));
    setDefaultLayoutArea(View::CENTER);
    
    setAttribute(Qt::WA_NativeWindow);
    setAttribute(Qt::WA_PaintOnScreen);
    setAttribute(Qt::WA_OpaquePaintEvent);

    impl = new DSMediaViewImpl(this);
}


DSMediaViewImpl::DSMediaViewImpl(DSMediaView* self)
    : self(self),
      mv(MessageView::mainInstance())
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaViewImpl::DSMediaViewImpl()" << endl;
    }
    
    graphBuilder = nullptr;
    mediaControl = nullptr;
    mediaEvent = nullptr;
    mediaSeeking = nullptr;
    videoWindow = nullptr;
    basicVideo = nullptr;
    videoFrameStep = nullptr;
    hwnd = 0;
    
    bMapped = false;
    bLoaded = false;
    bCanSeek = false;
    
    currentState = Empty;

    orgWinProc = NULL;

    aspectRatioCheck->sigToggled().connect(
        [&](bool){ onAspectRatioCheckToggled(); });

    orgSizeCheck->sigToggled().connect(
        [&](bool){ onOrgSizeCheckToggled(); });

    rootItemConnection = 
        RootItem::instance()->sigCheckToggled().connect(
            [&](Item* item, bool on){ onItemCheckToggled(item, on); });
}


DSMediaView::~DSMediaView()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::~DSMediaView()" << endl;
    }
    
    delete impl;
}


DSMediaViewImpl::~DSMediaViewImpl()
{

}

bool DSMediaView::event(QEvent* event){
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::event()" << endl;
    }
    
    if(event->type() == QEvent::WinIdChange){
        impl->onWindowIdChanged();
        return true;
    }
    return QWidget::event(event);

    //return false;
}

namespace {
    
LRESULT CALLBACK videoWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::videoWindowProc()" << endl;
    }
    DSMediaViewImpl* impl = (DSMediaViewImpl*)GetWindowLongPtr(hwnd, GWLP_USERDATA);
    switch(uMsg) {
    case WM_DSHOW_NOTIFY:
        impl->HandleDShowEvent();
        break;
    }
    return CallWindowProc(impl->orgWinProc, hwnd, uMsg, wParam, lParam);
}
}


void DSMediaViewImpl::onWindowIdChanged()
{
    hwnd = (HWND)self->winId();
    
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::onWindowIdChanged(): hwnd = " << hwnd << endl;
    }

    initializeScreenWindow();
}


void DSMediaView::resizeEvent(QResizeEvent* event)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::resizeEvent()" << endl;
    }

    if(impl->bLoaded){
        impl->adjustVideoWindow();
    }
}


void DSMediaView::paintEvent(QPaintEvent* event)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::paintEvent()" << endl;
    }

    // Fill background with black using Win32 GDI
    HWND hwnd = (HWND)winId();
    if(hwnd){
        HDC hdc = GetDC(hwnd);
        if(hdc){
            RECT rect;
            GetClientRect(hwnd, &rect);
            HBRUSH brush = CreateSolidBrush(RGB(0, 0, 0));
            FillRect(hdc, &rect, brush);
            DeleteObject(brush);
            ReleaseDC(hwnd, hdc);
        }
    }
}


QPaintEngine* DSMediaView::paintEngine () const
{
    return nullptr;
}


void DSMediaView::onActivated()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::onActivated()" << endl;
    }
    impl->initializeScreenWindow();
}


void DSMediaView::onDeactivated()
{
    if(impl->bLoaded){
        impl->unload();
    }

    impl->clearScreenWindow();
}


void DSMediaViewImpl::onAspectRatioCheckToggled()
{
    if(bLoaded){
        adjustVideoWindow();
    }
}


void DSMediaViewImpl::onOrgSizeCheckToggled()
{
    if(bLoaded){
        adjustVideoWindow();
    }
}


void DSMediaViewImpl::onItemCheckToggled(Item* item, bool isChecked)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaViewImpl::onItemCheckToggled()" << endl;
    }

    MediaItem* mediaItem = dynamic_cast<MediaItem*>(item);
    if(mediaItem){
        MediaItemPtr targetItem = RootItem::instance()->checkedItems<MediaItem>().toSingle(true);
         if(targetItem != currentMediaItem){
            if(currentMediaItem){
                if(bLoaded){
                    unload();
                }
            }
            currentMediaItem = targetItem;
            if(bMapped && currentMediaItem){
                load();
            }
        }
    }
}


void DSMediaViewImpl::initializeScreenWindow()
{
    hwnd = (HWND)self->winId();
    
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::initializeScreenWindow(): hwnd = " << hwnd << endl;
    }

    if(hwnd != NULL && orgWinProc == NULL){

#ifdef _WIN64
        orgWinProc = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_WNDPROC);
        
        //SetWindowLong(hwnd, GWL_WNDPROC, (LONG)(WNDPROC)videoWindowProc);
        SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)videoWindowProc);
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)this);
#else
        orgWinProc = (WNDPROC)GetWindowLong(hwnd, GWL_WNDPROC);

        //SetWindowLong(hwnd, GWL_WNDPROC, (LONG)(WNDPROC)videoWindowProc);
        SetWindowLongPtr(hwnd, GWL_WNDPROC, (LONG_PTR)videoWindowProc);
        SetWindowLongPtr(hwnd, GWL_USERDATA, (LONG_PTR)this);
#endif


        bMapped = true;
        if(currentMediaItem){
            load();
        }
    }
}


void DSMediaViewImpl::clearScreenWindow()
{
    if(hwnd != NULL && orgWinProc != NULL){

#ifdef _WIN64
        SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)orgWinProc);
#else
        SetWindowLongPtr(hwnd, GWL_WNDPROC, (LONG_PTR)orgWinProc);
#endif

    }

    orgWinProc = NULL;
    hwnd = NULL;
    bMapped = false;
}    


void DSMediaViewImpl::load()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::load()" << endl;
    }
    
    HRESULT result;

#define EIF(x)                                                          \
    result = (x);                                                       \
    if(FAILED(result)) {                                                \
        throw DSException(formatC("FAILED(hr=0x{:x}) in" #x, result));   \
    }

    struct DSException {
        DSException(std::string m) : message(m) { }
        std::string message;
    };
    
    try{
        // Get the interface for DirectShow's GraphBuilder
        EIF(CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
                             IID_IGraphBuilder, (void**)&graphBuilder));

        // Get the file path and convert to wide character string for DirectShow
        filesystem::path fpath(fromUTF8(currentMediaItem->mediaFilePath()));
        std::wstring wFile = fpath.wstring();

        mv->putln(formatC("Attempting to load media file: {}", currentMediaItem->mediaFilePath()));

        // Have the graph builder construct its the appropriate graph automatically
        result = graphBuilder->RenderFile(wFile.c_str(), NULL);
        if(FAILED(result)) {
            WCHAR errorText[512];
            std::string errorMsg;
            if(AMGetErrorTextW(result, errorText, 512)) {
                char mbErrorText[1024];
                WideCharToMultiByte(CP_UTF8, 0, errorText, -1, mbErrorText, 1024, NULL, NULL);
                errorMsg = formatC("FAILED(hr=0x{:08x}) in graphBuilder->RenderFile: {}",
                                   static_cast<unsigned int>(result), mbErrorText);
            } else {
                errorMsg = formatC("FAILED(hr=0x{:08x}) in graphBuilder->RenderFile",
                                   static_cast<unsigned int>(result));
            }
            throw DSException(errorMsg);
        }

        // QueryInterface for DirectShow interfaces
        EIF(graphBuilder->QueryInterface(IID_IMediaControl, (void**)&mediaControl));
        EIF(graphBuilder->QueryInterface(IID_IMediaEventEx, (void**)&mediaEvent));
        EIF(graphBuilder->QueryInterface(IID_IMediaSeeking, (void**)&mediaSeeking));
        
        // Check seekable
        DWORD caps = AM_SEEKING_CanSeekAbsolute | AM_SEEKING_CanGetDuration; 
        bCanSeek = (S_OK == mediaSeeking->CheckCapabilities(&caps));
        if(!bCanSeek){
            mv->putln(_("This media file is not able to seek!"));
        }
        
        // Query for video interfaces, which may not be relevant for audio files
        EIF(graphBuilder->QueryInterface(IID_IVideoWindow, (void**)&videoWindow));
        EIF(graphBuilder->QueryInterface(IID_IBasicVideo, (void**)&basicVideo));

        // Have the graph signal event via window callbacks for performance
        EIF(mediaEvent->SetNotifyWindow((OAHWND)hwnd, WM_DSHOW_NOTIFY, 0));

        try{
            // Setup the video window
            EIF(videoWindow->put_Owner((OAHWND)hwnd));
            EIF(videoWindow->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS));

            EIF(adjustVideoWindow());

            EIF(videoWindow->SetWindowForeground(OATRUE));
            EIF(videoWindow->put_Visible(OATRUE));
        }
        catch (const DSException&){
            // comes here if loaded file is pure audio (e.g. wav file) and the filter graph has no video renderer attached to it
            videoWindow = nullptr;
            basicVideo = nullptr;
        }
        
        connectTimeBarSignals();

        seekMedia(TimeBar::instance()->time());
        mediaControl->StopWhenReady();

        currentState = Stopped;
        bLoaded = true;

    } catch (const DSException& ex){
        mv->putln(ex.message);
        bLoaded = false;
    }
}


void DSMediaViewImpl::unload()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::unload()" << endl;
    }
    
#define SAFE_RELEASE(x) { if(x) x->Release(); x = 0; }
    
    timeBarConnections.disconnect();

    // Release and zero DirectShow interfaces
    SAFE_RELEASE(mediaEvent);
    SAFE_RELEASE(mediaSeeking);
    SAFE_RELEASE(mediaControl);
    SAFE_RELEASE(basicVideo);
    SAFE_RELEASE(videoWindow);
    SAFE_RELEASE(videoFrameStep);
    SAFE_RELEASE(graphBuilder);

    currentState = Empty;

    bLoaded = false;
}


HRESULT DSMediaViewImpl::adjustVideoWindow()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::adjustVideoWindow()" << endl;
    }

    if(videoWindow && basicVideo){
        // Get devicePixelRatio for this widget to convert Qt logical coordinates
        // to physical coordinates for DirectShow
        qreal dpr = 1.0;
        if(auto window = self->windowHandle()){
            dpr = window->devicePixelRatio();
        }

        // Get logical dimensions from Qt widget
        int logicalWidth = self->width();
        int logicalHeight = self->height();

        // Convert to physical pixels for DirectShow
        LONG screenWidth = qRound(logicalWidth * dpr);
        LONG screenHeight = qRound(logicalHeight * dpr);

        // Don't update if widget doesn't have valid size yet
        if(screenWidth <= 0 || screenHeight <= 0){
            return S_OK;
        }

        LONG width, height;
        if(basicVideo->GetVideoSize(&width, &height) != E_NOINTERFACE){
            // width and height from GetVideoSize are already in physical pixels
            if(orgSizeCheck->isChecked()){
                // Center the video regardless of whether it fits in the view
                return videoWindow->SetWindowPosition(
                    (screenWidth - width) / 2, (screenHeight - height) / 2, width, height);
            } else if(aspectRatioCheck->isChecked()){
                double r1 = (double)screenWidth / width;
                double r2 = (double)screenHeight / height;
                LONG w = (LONG)(width * (std::min)(r1, r2));
                LONG h = (LONG)(height * (std::min)(r1, r2));
                return videoWindow->SetWindowPosition(
                    (screenWidth - w) / 2, (screenHeight - h) / 2, w, h);
            }
        }
        return videoWindow->SetWindowPosition(0, 0, screenWidth, screenHeight);
    }

    return S_OK;
}


void DSMediaViewImpl::connectTimeBarSignals()
{
    if(timeBarConnections.empty()){
        TimeBar* timeBar = TimeBar::instance();
        timeBarConnections.add(
            timeBar->sigPlaybackInitialized().connect(
                [&](double time){ return onPlaybackInitialized(time); }));
        timeBarConnections.add(
            timeBar->sigPlaybackStarted().connect(
                [&](double time){ onPlaybackStarted(time); }));
        timeBarConnections.add(
            timeBar->sigPlaybackStoppedEx().connect(
                [&](double time, bool isStoppedManually){ return onPlaybackStopped(time, isStoppedManually); }));
        timeBarConnections.add(
            timeBar->sigTimeChanged().connect(
                [&](double time){ return onTimeChanged(time); }));
    }
}


bool DSMediaViewImpl::onPlaybackInitialized(double time)
{
    seekMedia(time);
    pauseMedia();
    OAFilterState state;
    mediaControl->GetState(1000, &state);
    return true;
}


void DSMediaViewImpl::onPlaybackStarted(double time)
{
    playMedia();
}


bool DSMediaViewImpl::onTimeChanged(double time)
{
    if(currentState == Running){
        return true;
    } else {
        seekMedia(time);
        mediaControl->StopWhenReady();
    }
    return false;
}


double DSMediaViewImpl::onPlaybackStopped(double time, bool isStoppedManually)
{
    double lastValidTime = time;

    if(mediaSeeking && currentMediaItem){
        // Query DirectShow for the actual media duration
        LONGLONG duration_100ns = 0;
        if(SUCCEEDED(mediaSeeking->GetDuration(&duration_100ns))){
            // Convert 100-nanosecond units to seconds and subtract offset
            double mediaDuration = (double)duration_100ns / 10000000.0;
            double mediaEndTime = mediaDuration - currentMediaItem->offsetTime();
            lastValidTime = mediaEndTime;
        }
    }

    pauseMedia();

    return lastValidTime;
}


HRESULT DSMediaViewImpl::seekMedia(double time)
{
    HRESULT result = S_OK;
    if(bCanSeek){
        LONGLONG llNow;
        const LONGLONG ONE_SECOND = 10000000;
        llNow = (LONGLONG)((time + currentMediaItem->offsetTime()) * ONE_SECOND);
        result = mediaSeeking->SetPositions(
            &llNow, AM_SEEKING_AbsolutePositioning, NULL, AM_SEEKING_NoPositioning);
    }
    return result;
}


HRESULT DSMediaViewImpl::playMedia()
{
    HRESULT result = S_OK;
    if(currentState == Stopped || currentState == Paused){
        result = mediaControl->Run();
        if(FAILED(result)){
            mv->putln("MediaView failed to play media");
        } else {
            currentState = Running;
        }
    }
    return result;
}


HRESULT DSMediaViewImpl::pauseMedia()
{
    HRESULT result = S_OK;
    if(currentState == Running){
        result = mediaControl->Pause();
        if(FAILED(result)){
            mv->putln("MediaView failed to pause media");
        } else {
            currentState = Paused;
        }
    }
    return result;
}


HRESULT DSMediaViewImpl::stopMedia()
{
    HRESULT result = S_OK;
    if(currentState == Running || currentState == Paused){
        result = mediaControl->Stop();
        currentState = Stopped;
        seekMedia(0.0);
        result = mediaControl->Pause();
        if(FAILED(result)){
            mv->putln("MediaView failed to pause media");
        } else {
            currentState = Paused;
        }
    }
    return result;
}


HRESULT DSMediaViewImpl::HandleDShowEvent()
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaView::HandleDShowEvent()" << endl;
    }
    
    // Make sure that we don't access the media event interface
    // after it has already been released.
    if(!mediaEvent){
        return S_OK;
    }

    LONG evCode;
    LONG_PTR evParam1, evParam2;
    HRESULT result = S_OK;

    // Process all queued events
    while(SUCCEEDED(mediaEvent->GetEvent(&evCode, &evParam1, &evParam2, 0))){
        // Free memory associated with callback, since we're not using it
        result = mediaEvent->FreeEventParams(evCode, evParam1, evParam2);
        // If this is the end of the clip, reset to beginning
        if(EC_COMPLETE == evCode){
            LONGLONG pos = 0;
            if(SUCCEEDED(result = mediaControl->Pause())){
                currentState = Paused;
            } else {
                if (SUCCEEDED(result = mediaControl->Stop())){
                    currentState = Stopped;
                } else {
                    mv->putln(formatR(_("Failed({}) to stop media clip!\n"), result));
                    unload();
                    break;
                }
            }
            /*
            //os << "repeat!!" << endl;
            // Reset to first frame of movie
            hr = mediaSeeking->SetPositions(&pos, AM_SEEKING_AbsolutePositioning ,
            NULL, AM_SEEKING_NoPositioning);
            if (FAILED(hr))
            {
            // Some custom filters (like the Windows CE MIDI filter)
            // may not implement seeking interfaces (IMediaSeeking)
            // to allow seeking to the start.  In that case, just stop
            // and restart for the same effect.  This should not be
            // necessary in most cases.
            if (FAILED(hr = mediaControl->Stop()))
            {
            os << fmt("Failed(0x%08lx) to stop media clip!\n") % hr << endl;
            break;
            }

            if (FAILED(hr = mediaControl->Run()))
            {
            os << fmt("Failed(0x%08lx) to reset media clip!\n") % hr << endl;
            break;
            }
            }
            */
        }
    }

    return result;
}


bool DSMediaView::storeState(Archive& archive)
{
    return impl->storeState(archive);
}


bool DSMediaViewImpl::storeState(Archive& archive)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaViewImpl::storeState()" << endl;
    }
    archive.write("keepAspectRatio", aspectRatioCheck->isChecked());
    archive.write("keepOriginalSize", orgSizeCheck->isChecked());
    return true;
}


bool DSMediaView::restoreState(const Archive& archive)
{
    return impl->restoreState(archive);
}


bool DSMediaViewImpl::restoreState(const Archive& archive)
{
    if(TRACE_FUNCTIONS){
        cout << "DSMediaViewImpl::restoreState()" << endl;
    }
    aspectRatioCheck->setChecked(archive.get("keepAspectRatio", aspectRatioCheck->isChecked()));
    orgSizeCheck->setChecked(archive.get("keepOriginalSize", orgSizeCheck->isChecked()));
    return true;
}
