/****************************************************************************
*                                                                           *
* Project64 - A Nintendo 64 emulator.                                       *
* http://www.pj64-emu.com/                                                  *
* Copyright (C) 2012 Project64. All rights reserved.                        *
*                                                                           *
* License:                                                                  *
* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html                        *
*                                                                           *
****************************************************************************/

#include "stdafx.h"
#include "DebuggerUI.h"
#include "CPULog.h"

#include "Debugger-CPULogView.h"
#include <Project64-core/N64System/Mips/OpCodeName.h>

CDebugCPULogView* CDebugCPULogView::_this = NULL;
HHOOK CDebugCPULogView::hWinMessageHook = NULL;

CDebugCPULogView::CDebugCPULogView(CDebuggerUI* debugger) :
    CDebugDialog<CDebugCPULogView>(debugger),
    m_CPULogCopy(NULL),
    m_LogStartIndex(0),
    m_RowHeight(13)
{
}

CDebugCPULogView::~CDebugCPULogView()
{
    if (m_CPULogCopy != NULL)
    {
        delete m_CPULogCopy;
    }
}

LRESULT CDebugCPULogView::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    DlgResize_Init(false, true);
    DlgToolTip_Init();
    DlgSavePos_Init(DebuggerUI_CPULogPos);

    m_CPUListView.Attach(GetDlgItem(IDC_CPU_LIST));
    m_StateInfoEdit.Attach(GetDlgItem(IDC_STATEINFO_EDIT));
    m_EnabledChk.Attach(GetDlgItem(IDC_CHK_ENABLE));
    m_BuffSizeEdit.Attach(GetDlgItem(IDC_BUFFSIZE_EDIT));
    m_Scrollbar.Attach(GetDlgItem(IDC_SCRL_BAR));
    m_ExportBtn.Attach(GetDlgItem(IDC_BTN_EXPORT));

    m_CPUListView.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);
    m_CPUListView.ModifyStyle(LVS_OWNERDRAWFIXED, 0, 0);
    m_CPUListView.AddColumn(L"PC", 0);
    m_CPUListView.AddColumn(L"Command", 1);
    m_CPUListView.AddColumn(L"Parameters", 2);
    m_CPUListView.SetColumnWidth(0, 65);
    m_CPUListView.SetColumnWidth(1, 60);
    m_CPUListView.SetColumnWidth(2, 120);
    
    bool bLoggingEnabled = g_Settings->LoadBool(Debugger_CPULoggingEnabled);
    uint32_t bufferSize = g_Settings->LoadDword(Debugger_CPULogBufferSize);

    m_EnabledChk.SetCheck(bLoggingEnabled);

    m_BuffSizeEdit.SetDisplayType(CEditNumber32::DisplayDec);
    m_BuffSizeEdit.SetValue(bufferSize);
    m_BuffSizeEdit.EnableWindow(!bLoggingEnabled);
    
    RefreshList(true);

    m_ExportBtn.EnableWindow(false);

    if (!bLoggingEnabled && m_CPULogCopy != NULL)
    {
        m_ExportBtn.EnableWindow(true);
    }

    _this = this;
    DWORD dwThreadID = ::GetCurrentThreadId();
    hWinMessageHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)HookProc, NULL, dwThreadID);

    LoadWindowPos();
    WindowCreated();
    return TRUE;
}

LRESULT CDebugCPULogView::OnDestroy(void)
{
    UnhookWindowsHookEx(hWinMessageHook);

    m_CPUListView.Detach();
    m_StateInfoEdit.Detach();
    m_EnabledChk.Detach();
    m_BuffSizeEdit.Detach();
    m_Scrollbar.Detach();
    m_ExportBtn.Detach();

    if (m_CPULogCopy != NULL)
    {
        delete m_CPULogCopy;
        m_CPULogCopy = NULL;
    }
    return 0;
}

LRESULT CALLBACK CDebugCPULogView::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    MSG *pMsg = (MSG*)lParam;

    switch (pMsg->message)
    {
    case WM_MOUSEWHEEL:
        _this->InterceptMouseWheel(pMsg->wParam, pMsg->lParam);
        break;
    }

    if (nCode < 0)
    {
        return CallNextHookEx(hWinMessageHook, nCode, wParam, lParam);
    }

    return 0;
}

LRESULT CDebugCPULogView::OnClicked(WORD /*wNotifyCode*/, WORD wID, HWND, BOOL& /*bHandled*/)
{
    switch (wID)
    {
    case IDOK:
        EndDialog(0);
        break;
    case IDCANCEL:
        EndDialog(0);
        break;
    case IDC_CHK_ENABLE:
        ToggleLoggingEnabled();
        break;
    case IDC_BTN_EXPORT:
        Export();
        break;
    }
    
    return FALSE;
}

LRESULT CDebugCPULogView::OnActivate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    return FALSE;
}

LRESULT CDebugCPULogView::OnListItemChanged(NMHDR* pNMHDR)
{
    NMITEMACTIVATE* pIA = reinterpret_cast<NMITEMACTIVATE*>(pNMHDR);
    int nItem = pIA->iItem;

    ShowRegStates(m_LogStartIndex + nItem);

    return FALSE;
}

LRESULT CDebugCPULogView::OnListDblClicked(NMHDR* pNMHDR)
{
    NMITEMACTIVATE* pIA = reinterpret_cast<NMITEMACTIVATE*>(pNMHDR);
    int nItem = pIA->iItem;

    CPUState* state = m_CPULogCopy->GetEntry(m_LogStartIndex + nItem);

    if (state == NULL)
    {
        return FALSE;
    }

    m_Debugger->Debug_ShowCommandsLocation(state->pc, true);

    return FALSE;
}

LRESULT CDebugCPULogView::OnMeasureItem(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)
{
    if (wParam == IDC_CPU_LIST)
    {
        CClientDC dc(m_hWnd);
        dc.SelectFont(GetFont());
        TEXTMETRIC tm;
        dc.GetTextMetrics(&tm);

        m_RowHeight = tm.tmHeight + tm.tmExternalLeading;

        MEASUREITEMSTRUCT* lpMeasureItem = (MEASUREITEMSTRUCT*)lParam;
        lpMeasureItem->itemHeight = m_RowHeight;
    }
    return FALSE;
}

LRESULT CDebugCPULogView::OnScroll(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)
{
    WORD type = LOWORD(wParam);
    HWND hScrollbar = (HWND)lParam;
    WORD scrlId = (WORD)::GetDlgCtrlID(hScrollbar);

    SCROLLINFO scrollInfo;
    scrollInfo.cbSize = sizeof(SCROLLINFO);
    scrollInfo.fMask = SIF_ALL;

    ::GetScrollInfo(hScrollbar, SB_CTL, &scrollInfo);

    int newPos;

    switch (type)
    {
    case SB_LINEUP: newPos = max(scrollInfo.nMin, scrollInfo.nPos - 1); break;
    case SB_LINEDOWN: newPos = min(scrollInfo.nMax, scrollInfo.nPos + 1); break;
    case SB_THUMBTRACK: newPos = scrollInfo.nTrackPos; break;
    default: return 0;
    }

    m_LogStartIndex = newPos;
    ::SetScrollPos(hScrollbar, SB_CTL, newPos, TRUE);

    if (scrlId == IDC_SCRL_BAR)
    {
        RefreshList(false);
    }

    return 0;
}

void CDebugCPULogView::InterceptMouseWheel(WPARAM wParam, LPARAM /*lParam*/)
{
    int nScroll = -((short)HIWORD(wParam) / WHEEL_DELTA);

    if (MouseHovering(IDC_CPU_LIST) || MouseHovering(IDC_SCRL_BAR))
    {
        int scrlMin, scrlMax;
        m_Scrollbar.GetScrollRange(&scrlMin, &scrlMax);

        int scrollPos = m_Scrollbar.GetScrollPos();
        int newPos = scrollPos + nScroll;

        if (newPos < scrlMin)
        {
            newPos = scrlMin;
        }
        else if (newPos > scrlMax)
        {
            newPos = scrlMax;
        }

        m_LogStartIndex = newPos;
        m_Scrollbar.SetScrollPos(newPos, true);

        RefreshList(false);
    }
}

void CDebugCPULogView::OnExitSizeMove(void)
{
    RefreshList(false);
    SaveWindowPos(true);
}

void CDebugCPULogView::ToggleLoggingEnabled(void)
{
    bool bEnableLogging = (m_EnabledChk.GetCheck() == BST_CHECKED);

    m_BuffSizeEdit.EnableWindow(!bEnableLogging);

    g_Settings->SaveBool(Debugger_CPULoggingEnabled, bEnableLogging);

    if (bEnableLogging)
    {
        uint32_t newSize = m_BuffSizeEdit.GetValue();
        g_Settings->SaveDword(Debugger_CPULogBufferSize, newSize);
        m_Debugger->CPULog()->Reset();
        m_ExportBtn.EnableWindow(false);
    }
    else
    {
        RefreshList(true);
        m_ExportBtn.EnableWindow(m_CPULogCopy != NULL);
    }
}

void CDebugCPULogView::RefreshList(bool bUpdateBuffer)
{
    if (bUpdateBuffer)
    {
        if (m_CPULogCopy != NULL)
        {
            delete m_CPULogCopy;
        }

        m_CPULogCopy = m_Debugger->CPULog()->Clone();
    }

    if (m_CPULogCopy == NULL)
    {
        return;
    }

    size_t count = m_CPULogCopy->GetCount();
    size_t numVisibleRows = GetNumVisibleRows(m_CPUListView);

    bool bCanDisplayAll = (numVisibleRows >= count);
    int scrollRangeMax = bCanDisplayAll ? 0 : count - numVisibleRows;

    m_Scrollbar.SetScrollRange(0, scrollRangeMax, false);
    m_Scrollbar.EnableWindow(!bCanDisplayAll);

    if (bUpdateBuffer)
    {
        m_LogStartIndex = scrollRangeMax;
        m_Scrollbar.SetScrollPos(m_LogStartIndex, true);
        ShowRegStates(count - 1);
    }

    size_t start = m_Scrollbar.GetScrollPos();
    size_t end = start + numVisibleRows;

    if (end > count)
    {
        end = count;
    }

    m_CPUListView.SetRedraw(FALSE);
    m_CPUListView.DeleteAllItems();

    int nItem = 0;

    for (size_t i = start; i < end; i++)
    {
        CPUState* state = m_CPULogCopy->GetEntry(i);

        if (state == NULL)
        {
            break;
        }

        char szPC[9];
        sprintf(szPC, "%08X", state->pc);

        char *tokctx;

        char* szCommand = (char*)R4300iOpcodeName(state->opcode.Hex, state->pc);
        char* szCmdName = strtok_s((char*)szCommand, "\t", &tokctx);
        char* szCmdArgs = strtok_s(NULL, "\t", &tokctx);

        m_CPUListView.AddItem(nItem, 0, stdstr(szPC).ToUTF16().c_str());
        m_CPUListView.AddItem(nItem, 1, stdstr(szCmdName).ToUTF16().c_str());
        m_CPUListView.AddItem(nItem, 2, stdstr(szCmdArgs).ToUTF16().c_str());

        nItem++;
    }

    m_CPUListView.SetRedraw(TRUE);
}

void CDebugCPULogView::ShowRegStates(size_t stateIndex)
{
    CPUState* state = m_CPULogCopy->GetEntry(stateIndex);

    if (state == NULL)
    {
        return;
    }

    char szRegStates[2048];
    char* out = szRegStates;

    out += sprintf(out, "PC: %08X\r\n\r\n", state->pc);

    for (int i = 0; i < 16; i++)
    {
        int regl = i, regr = i + 16;
        out += sprintf(out, "%s: %08X %08X  %s: %08X %08X\r\n",
                       CRegName::GPR[regl], state->gpr[regl].UW[1], state->gpr[regl].UW[0],
                       CRegName::GPR[regr], state->gpr[regr].UW[1], state->gpr[regr].UW[0]);
    }

    out += sprintf(out, "HI: %08X %08X  LO: %08X %08X\r\n\r\n",
                   state->gprHi.UW[1], state->gprHi.UW[0],
                   state->gprLo.UW[1], state->gprLo.UW[0]);

    for (int i = 0; i < 16; i++)
    {
        int regl = i, regr = i + 16;
        out += sprintf(out, "%-3s: %08X  %-3s: %08X\r\n",
                       CRegName::FPR[regl], *(uint32_t*)&state->fpr[regl],
                       CRegName::FPR[regr], *(uint32_t*)&state->fpr[regr]);
    }

    out += sprintf(out, "FPCR: %08X\r\n", state->fpcr);

    m_StateInfoEdit.SetWindowText(stdstr(szRegStates).ToUTF16().c_str());
}

void CDebugCPULogView::Export(void)
{
    if (m_CPULogCopy == NULL)
    {
        return;
    }

    OPENFILENAMEA openfilename;
    char filePath[255];
    
    memset(&filePath, 0, sizeof(filePath));
    memset(&openfilename, 0, sizeof(openfilename));

    sprintf(filePath, "CPULOG.txt");

    openfilename.lStructSize = sizeof(openfilename);
    openfilename.hwndOwner = (HWND)m_hWnd;
    openfilename.lpstrFilter = "CPU Log (*.*)\0*.*;\0";
    openfilename.lpstrFile = filePath;
    openfilename.lpstrInitialDir = "Logs";
    openfilename.nMaxFile = MAX_PATH;
    openfilename.Flags = OFN_HIDEREADONLY;

    if (GetSaveFileNameA(&openfilename))
    {
        m_CPULogCopy->DumpToFile(filePath);
    }
}

// util

int CDebugCPULogView::GetNumVisibleRows(CListViewCtrl& list)
{
    CHeaderCtrl header = list.GetHeader();
    CRect listRect, headRect;
    list.GetWindowRect(&listRect);
    header.GetWindowRect(&headRect);
    int innerHeight = listRect.Height() - headRect.Height();
    return (innerHeight / m_RowHeight);
}

bool CDebugCPULogView::MouseHovering(WORD ctrlId, int xMargin, int yMargin)
{
    CRect rect;
    POINT pointerPos;

    ::GetWindowRect(GetDlgItem(ctrlId), &rect);
    ::GetCursorPos(&pointerPos);

    return (
        pointerPos.x >= rect.left - xMargin &&
        pointerPos.x <= rect.right + xMargin &&
        pointerPos.y >= rect.top - yMargin &&
        pointerPos.y <= rect.bottom + yMargin);
}
