#include "stdafx.h"
#include "DebuggerUI.h"
#include "MemoryScanner.h"

CDebugMemorySearch* CDebugMemorySearch::_this = nullptr;
HHOOK CDebugMemorySearch::hWinMessageHook = nullptr;

const CSetValueDlg::ComboItem CDebugMemorySearch::ModalChangeTypeItems[] = {
    { "uint8",  ValueType_uint8 },
    { "int8",   ValueType_int8 },
    { "uint16", ValueType_uint16 },
    { "int16",  ValueType_int16 },
    { "uint32", ValueType_uint32 },
    { "int32",  ValueType_int32 },
    { "uint64", ValueType_uint64 },
    { "int64",  ValueType_int64 },
    { "float",  ValueType_float },
    { "double", ValueType_double },
    { "string", ValueType_string },
    { nullptr, 0 }
};

CDebugMemorySearch::CDebugMemorySearch(CDebuggerUI * debugger) :
    CDebugDialog<CDebugMemorySearch>(debugger)
{
}

CDebugMemorySearch::~CDebugMemorySearch()
{
}

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

    m_HexCheckbox.Attach(GetDlgItem(IDC_CHK_HEX));
    m_UnsignedCheckbox.Attach(GetDlgItem(IDC_CHK_UNSIGNED));
    m_IgnoreCaseCheckbox.Attach(GetDlgItem(IDC_CHK_IGNORECASE));
    m_UnkEncodingCheckbox.Attach(GetDlgItem(IDC_CHK_UNKENCODING));
    m_SearchValue.Attach(GetDlgItem(IDC_SEARCH_VALUE));
    m_SearchTypeOptions.Attach(GetDlgItem(IDC_CMB_SCANTYPE));
    m_ValueTypeOptions.Attach(GetDlgItem(IDC_CMB_VALUETYPE));
    m_AddrStart.Attach(GetDlgItem(IDC_ADDR_START));
    m_AddrEnd.Attach(GetDlgItem(IDC_ADDR_END));
    m_PhysicalCheckbox.Attach(GetDlgItem(IDC_CHK_PHYSICAL));
    m_ResultsListCtrl.Attach(GetDlgItem(IDC_LST_RESULTS));
    m_ResultsScrollbar.Attach(GetDlgItem(IDC_SCRL_RESULTS));
    m_WatchListCtrl.Attach(GetDlgItem(IDC_LST_WATCHLIST));
    m_WatchListScrollbar.Attach(GetDlgItem(IDC_SCRL_WATCHLIST));

    m_SearchValue.SetDisplayFormat(DisplayDefault);
    m_AddrEnd.SetDisplayType(CEditNumber32::DisplayHex);
    m_AddrStart.SetDisplayType(CEditNumber32::DisplayHex);

    UpdateOptions(); // Setup search type combo box

    CComboBox & vtcb = m_ValueTypeOptions;
    vtcb.SetItemData(vtcb.AddString(L"int8"), ValueType_int8);
    vtcb.SetItemData(vtcb.AddString(L"int16"), ValueType_int16);
    vtcb.SetItemData(vtcb.AddString(L"int32"), ValueType_int32);
    vtcb.SetItemData(vtcb.AddString(L"int64"), ValueType_int64);
    vtcb.SetItemData(vtcb.AddString(L"float"), ValueType_float);
    vtcb.SetItemData(vtcb.AddString(L"double"), ValueType_double);
    vtcb.SetItemData(vtcb.AddString(L"string"), ValueType_string);
    vtcb.SetCurSel(0);

    m_ResultsListCtrl.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);
    m_ResultsListCtrl.ModifyStyle(LVS_OWNERDRAWFIXED, 0, 0);
    m_ResultsListCtrl.AddColumn(L"Address", ResultsListCtrl_Col_Address);
    m_ResultsListCtrl.AddColumn(L"Value", ResultsListCtrl_Col_Value);
    m_ResultsListCtrl.AddColumn(L"Previous", ResultsListCtrl_Col_Previous);
    m_ResultsListCtrl.SetColumnWidth(ResultsListCtrl_Col_Address, 80);
    m_ResultsListCtrl.SetColumnWidth(ResultsListCtrl_Col_Value, 80);
    m_ResultsListCtrl.SetColumnWidth(ResultsListCtrl_Col_Previous, 80);

    m_WatchListCtrl.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER);
    m_WatchListCtrl.ModifyStyle(LVS_OWNERDRAWFIXED, 0, 0);
    m_WatchListCtrl.AddColumn(L"Lock", WatchListCtrl_Col_Lock);
    m_WatchListCtrl.AddColumn(L"BP", WatchListCtrl_Col_BP);
    m_WatchListCtrl.AddColumn(L"Address", WatchListCtrl_Col_Address);
    m_WatchListCtrl.AddColumn(L"Description", WatchListCtrl_Col_Description);
    m_WatchListCtrl.AddColumn(L"Type", WatchListCtrl_Col_Type);
    m_WatchListCtrl.AddColumn(L"Value", WatchListCtrl_Col_Value);
    m_WatchListCtrl.SetColumnWidth(WatchListCtrl_Col_Lock, 35);
    m_WatchListCtrl.SetColumnWidth(WatchListCtrl_Col_BP, 35);
    m_WatchListCtrl.SetColumnWidth(WatchListCtrl_Col_Address, 80);
    m_WatchListCtrl.SetColumnWidth(WatchListCtrl_Col_Description, 160);
    m_WatchListCtrl.SetColumnWidth(WatchListCtrl_Col_Type, 50);
    m_WatchListCtrl.SetColumnWidth(WatchListCtrl_Col_Value, 90);

    m_bJalSelected = false;
    m_bJalHexWasChecked = false;
    m_bJalUnsignedWasChecked = false;

    m_bDraggingSeparator = false;
    ::GetWindowRect(GetDlgItem(IDC_SEPARATOR), &m_InitialSeparatorRect);
    ScreenToClient(&m_InitialSeparatorRect);

    uint32_t ramSize = (g_MMU != nullptr) ? g_MMU->RdramSize() : 0x00400000;

    m_AddrStart.SetValue(0x80000000, DisplayMode::AllHex);
    m_AddrEnd.SetValue(0x80000000 + ramSize - 1, DisplayMode::AllHex);

    FixListHeader(m_WatchListCtrl);
    FixListHeader(m_ResultsListCtrl);

    UpdateResultsList(true);
    UpdateWatchList();

    LoadWindowPos();
    WindowCreated();

    m_hCursorSizeNS = LoadCursor(nullptr, IDC_SIZENS);

    SetTimer(TIMER_ID_AUTO_REFRESH, 100, nullptr);

    _this = this;
    m_ThreadId = ::GetCurrentThreadId();
    hWinMessageHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)HookProc, nullptr, m_ThreadId);

    GameReset();

    return TRUE;
}

LRESULT CDebugMemorySearch::OnDestroy(void)
{
    UnhookWindowsHookEx(hWinMessageHook);
    KillTimer(TIMER_ID_AUTO_REFRESH);

    m_UnsignedCheckbox.Detach();
    m_IgnoreCaseCheckbox.Detach();
    m_UnkEncodingCheckbox.Detach();
    m_HexCheckbox.Detach();
    m_SearchValue.Detach();
    m_SearchTypeOptions.Detach();
    m_ValueTypeOptions.Detach();
    m_AddrStart.Detach();
    m_AddrEnd.Detach();
    m_PhysicalCheckbox.Detach();
    m_ResultsListCtrl.Detach();
    m_ResultsScrollbar.Detach();
    m_WatchListCtrl.Detach();
    m_WatchListScrollbar.Detach();
    return 0;
}

void CDebugMemorySearch::GameReset(void)
{
    if (m_hWnd == nullptr)
    {
        return;
    }

    SendMessage(WM_GAMERESET);
}

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

void CDebugMemorySearch::ReloadWatchList(void)
{
    stdstr strGame = g_Settings->LoadStringVal(Game_GameName);

    FlushWatchList();

    if (m_StrGame != strGame)
    {
        m_StrGame = strGame;
        LoadWatchList();
    }
}

void CDebugMemorySearch::OnTimer(UINT_PTR nIDEvent)
{
    if (nIDEvent == TIMER_ID_AUTO_REFRESH)
    {
        RefreshResultsListValues();
        RefreshWatchListValues();
    }
}

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

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

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

    return 0;
}

void CDebugMemorySearch::OnExitSizeMove(void)
{
    UpdateWatchList(true);
    SaveWindowPos(true);
}

void CDebugMemorySearch::OnSizing(UINT /*fwSide*/, LPRECT /*pRect*/)
{
    FixListHeader(m_WatchListCtrl);
    FixListHeader(m_ResultsListCtrl);
    SetMsgHandled(FALSE);
}

LRESULT CDebugMemorySearch::OnCancel(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    EndDialog(0);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnHexCheckbox(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    bool bChecked = (m_HexCheckbox.GetCheck() == BST_CHECKED);
    m_SearchValue.SetDisplayFormat(bChecked ? DisplayHex : DisplayDefault);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnSearchButton(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    g_BaseSystem->ExternalEvent(SysEvent_PauseCPU_SearchMemory);
    Search();
    // Emulator won't resume sometimes unless there's a sleep() here
    Sleep(50); // TODO: fix?
    g_BaseSystem->ExternalEvent(SysEvent_ResumeCPU_SearchMemory);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnResetButton(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    ResetResults();
    return FALSE;
}

LRESULT CDebugMemorySearch::OnScanTypeChanged(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    SearchType searchType = (SearchType)m_SearchTypeOptions.GetItemData(m_SearchTypeOptions.GetCurSel());

    bool bDidFirstScan = m_MemoryScanner.DidFirstScan();

    if (searchType == SearchType_JalTo)
    {
        m_bJalSelected = true;
        m_SearchValue.EnableWindow(TRUE);

        SetComboBoxSelByData(m_ValueTypeOptions, ValueType_int32);
        m_ValueTypeOptions.EnableWindow(FALSE);

        // Remember checkbox states
        m_bJalHexWasChecked = (m_HexCheckbox.GetCheck() == BST_CHECKED);
        m_bJalUnsignedWasChecked = (m_UnsignedCheckbox.GetCheck() == BST_CHECKED);

        m_SearchValue.SetDisplayFormat(DisplayHex);

        m_HexCheckbox.SetCheck(BST_CHECKED);
        m_HexCheckbox.EnableWindow(FALSE);
        m_UnsignedCheckbox.SetCheck(BST_CHECKED);
        m_UnsignedCheckbox.EnableWindow(FALSE);
    }
    else if(m_bJalSelected)
    {
        m_bJalSelected = false;
        m_SearchValue.SetDisplayFormat(m_bJalHexWasChecked ? DisplayHex : DisplayDefault);
        m_HexCheckbox.SetCheck(m_bJalHexWasChecked ? BST_CHECKED : BST_UNCHECKED);
        m_HexCheckbox.EnableWindow(TRUE);
        m_UnsignedCheckbox.SetCheck(m_bJalUnsignedWasChecked ? BST_CHECKED : BST_UNCHECKED);
        m_UnsignedCheckbox.EnableWindow(TRUE);
    }

    switch (searchType)
    {
    case SearchType_JalTo:
        break;
    case SearchType_UnknownValue:
    case SearchType_IncreasedValue:
    case SearchType_DecreasedValue:
    case SearchType_ChangedValue:
    case SearchType_UnchangedValue:
        m_ValueTypeOptions.EnableWindow(!bDidFirstScan);
        m_SearchValue.EnableWindow(FALSE);
        break;
    default:
        m_ValueTypeOptions.EnableWindow(!bDidFirstScan);
        m_SearchValue.EnableWindow(TRUE);
        break;
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnValueTypeChanged(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    ValueType valueType = (ValueType)m_ValueTypeOptions.GetItemData(m_ValueTypeOptions.GetCurSel());

    if (valueType == ValueType_string || valueType == ValueType_istring || valueType == ValueType_unkstring)
    {
        m_SearchValue.EnableWindow(TRUE);
        SetComboBoxSelByData(m_SearchTypeOptions, SearchType_ExactValue);
        m_SearchTypeOptions.EnableWindow(FALSE);

        m_UnkEncodingCheckbox.ShowWindow(SW_SHOW);
        m_IgnoreCaseCheckbox.ShowWindow(SW_SHOW);
        m_UnsignedCheckbox.ShowWindow(SW_HIDE);
    }
    else
    {
        bool bShowUnsigned = (valueType != ValueType_float && valueType != ValueType_double);
        m_UnsignedCheckbox.ShowWindow(bShowUnsigned ? SW_SHOW : SW_HIDE);

        m_UnkEncodingCheckbox.ShowWindow(SW_HIDE);
        m_IgnoreCaseCheckbox.ShowWindow(SW_HIDE);

        m_SearchTypeOptions.EnableWindow(TRUE);
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnRdramButton(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    bool bPhysicalChecked = (m_PhysicalCheckbox.GetCheck() == BST_CHECKED);
    uint32_t addrStart = bPhysicalChecked ? 0 : 0x80000000;
    uint32_t ramSize = (g_MMU != nullptr) ? g_MMU->RdramSize() : 0x00400000;
    m_AddrStart.SetValue(addrStart, DisplayMode::AllHex);
    m_AddrEnd.SetValue(addrStart + ramSize - 1, DisplayMode::AllHex);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnRomButton(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    bool bPhysicalChecked = (m_PhysicalCheckbox.GetCheck() == BST_CHECKED);
    uint32_t addrStart = bPhysicalChecked ? 0x10000000 : 0xB0000000;
    m_AddrStart.SetValue(addrStart, DisplayMode::AllHex);
    m_AddrEnd.SetValue(addrStart + g_Rom->GetRomSize() - 1, DisplayMode::AllHex);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnSpmemButton(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    bool bPhysicalChecked = (m_PhysicalCheckbox.GetCheck() == BST_CHECKED);
    uint32_t addrStart = bPhysicalChecked ? 0x04000000 : 0xA4000000;
    m_AddrStart.SetValue(addrStart, DisplayMode::AllHex);
    m_AddrEnd.SetValue(addrStart + 0x1FFF, DisplayMode::AllHex);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnResultsCustomDraw(LPNMHDR lpnmh)
{
    NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>(lpnmh);
    DWORD drawStage = pLVCD->nmcd.dwDrawStage;

    switch (drawStage)
    {
    case CDDS_PREPAINT: return (CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT);
    case CDDS_ITEMPREPAINT: return CDRF_NOTIFYSUBITEMDRAW;
    case (CDDS_ITEMPREPAINT | CDDS_SUBITEM): break;
    default: return CDRF_DODEFAULT;
    }

    uint32_t nItem = (uint32_t)pLVCD->nmcd.dwItemSpec;
    uint32_t nSubItem = pLVCD->iSubItem;
    size_t index = m_ResultsListCtrl.GetItemData(nItem);
    CScanResult *presult = m_MemoryScanner.GetResult(index);

    switch (nSubItem)
    {
    case ResultsListCtrl_Col_Address:
        if (presult->m_AddressType == AddressType_Physical)
        {
            // Green if address is physical
            pLVCD->clrText = RGB(0x44, 0x88, 0x44);
        }
        break;
    case ResultsListCtrl_Col_Value:
        {
            pLVCD->clrText = RGB(0, 0, 0);
            char szCurrentValue[1024], szOldValue[1024];
            presult->GetMemoryValueString(szCurrentValue, 1024);
            presult->GetValueString(szOldValue, 1024);

            if (presult->IsStringType())
            {
                pLVCD->clrText = RGB(0, 0, 0);
                if (presult->m_DisplayFormat == DisplayHex)
                {
                    // Blue if hex string
                    pLVCD->clrText = RGB(0, 0, 255);
                }
            }
            else if (strcmp(szCurrentValue, szOldValue) != 0)
            {
                // Red if value has changed
                pLVCD->clrText = RGB(255, 0, 0);
            }
        }
        break;
    case ResultsListCtrl_Col_Previous:
        pLVCD->clrText = RGB(0, 0, 0);
        break;
    }

    return CDRF_DODEFAULT;
}

LRESULT CDebugMemorySearch::OnResultsDblClick(LPNMHDR)
{
    LONG iItem = m_ResultsListCtrl.GetNextItem(-1, LVNI_SELECTED);
    if (iItem == -1)
    {
        return true;
    }

    // Copy result to watch list
    int index = m_ResultsListCtrl.GetItemData(iItem);
    AddResultToWatchList(index);

    UpdateWatchList(true);
    return true;
}

LRESULT CDebugMemorySearch::OnResultsRClick(LPNMHDR /*lpnmh*/)
{
    LONG iItem = m_ResultsListCtrl.GetNextItem(-1, LVNI_SELECTED);
    if (iItem == -1)
    {
        return true;
    }

    // Load the menu
    HMENU hMenu = LoadMenu(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDR_MEM_SEARCH));
    HMENU hPopupMenu = GetSubMenu(hMenu, 0);

    // Get the current mouse location
    POINT Mouse;
    GetCursorPos(&Mouse);

    // Show the menu
    TrackPopupMenu(hPopupMenu, 0, Mouse.x, Mouse.y, 0, m_hWnd, nullptr);
    DestroyMenu(hMenu);
    return true;
}

LRESULT CDebugMemorySearch::OnResultsPopupViewMemory(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* presult = GetFirstSelectedScanResult();

    if (presult == nullptr)
    {
        return FALSE;
    }

    bool bUseVaddr = (presult->m_AddressType == AddressType_Virtual);
    m_Debugger->Debug_ShowMemoryLocation(presult->m_Address, bUseVaddr);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnResultsPopupAddToWatchList(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    LONG iItem = -1;

    while ((iItem = m_ResultsListCtrl.GetNextItem(iItem, LVNI_SELECTED)) != -1)
    {
        int index = m_ResultsListCtrl.GetItemData(iItem);
        AddResultToWatchList(index);
    }

    UpdateWatchList(true);

    return FALSE;
}

LRESULT CDebugMemorySearch::OnResultsPopupAddAllToWatchList(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    size_t numResults = m_MemoryScanner.GetNumResults();

    for (size_t i = 0; i < numResults; i++)
    {
        AddResultToWatchList(i);
    }

    UpdateWatchList(true);

    return FALSE;
}

LRESULT CDebugMemorySearch::OnResultsPopupSetValue(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* pFirstResult = GetFirstSelectedScanResult();

    if (pFirstResult == nullptr)
    {
        return FALSE;
    }

    char szInitialValue[1024];
    pFirstResult->GetMemoryValueString(szInitialValue, 1024);

    if (m_SetValueDlg.DoModal("Change value", "New value:", szInitialValue))
    {
        int nItems = m_ResultsListCtrl.GetItemCount();

        for (int iItem = 0; iItem < nItems; iItem++)
        {
            bool bSelected = (m_ResultsListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

            if (bSelected)
            {
                int index = m_ResultsListCtrl.GetItemData(iItem);
                CScanResult* presult = m_MemoryScanner.GetResult(index);
                const std::string & enteredString = m_SetValueDlg.GetEnteredString();
                presult->SetMemoryValueFromString(enteredString.c_str());
                m_ResultsListCtrl.SetItemText(iItem, ResultsListCtrl_Col_Value, stdstr(enteredString).ToUTF16().c_str());
            }
        }
    }
    return FALSE;
}

LRESULT CDebugMemorySearch::OnResultsPopupRemove(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    int nItems = m_ResultsListCtrl.GetItemCount();

    for (int iItem = nItems - 1; iItem >= 0; iItem--)
    {
        bool bSelected = (m_ResultsListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

        if (bSelected)
        {
            size_t index = m_ResultsListCtrl.GetItemData(iItem);
            m_MemoryScanner.RemoveResult(index);
        }
    }

    UpdateResultsList(true);

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListItemChanged(LPNMHDR /*lpnmh*/)
{
    //LPNMLISTVIEW pnmv = (LPNMLISTVIEW)lpnmh;
    //
    //bool bSelected = (pnmv->uNewState & LVNI_SELECTED) != 0;
    //
    //if (bSelected)
    //{
    //    //for (size_t i = 0; i < m_WatchList.size(); i++)
    //    //{
    //    //    m_WatchList[i].SetSelected(false);
    //    //}
    //
    //    int iItem = pnmv->iItem;
    //    int index = m_WatchListCtrl.GetItemData(iItem);
    //    CScanResult *presult = &m_WatchList[index];
    //    //presult->SetSelected(bSelected);
    //}

    return true;
}

LRESULT CDebugMemorySearch::OnWatchListCustomDraw(LPNMHDR lpnmh)
{
    NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>(lpnmh);
    DWORD drawStage = pLVCD->nmcd.dwDrawStage;

    switch (drawStage)
    {
    case CDDS_PREPAINT: return (CDRF_NOTIFYITEMDRAW | CDRF_NOTIFYPOSTPAINT);
    case CDDS_ITEMPREPAINT: return CDRF_NOTIFYSUBITEMDRAW;
    case (CDDS_ITEMPREPAINT | CDDS_SUBITEM): break;
    default: return CDRF_DODEFAULT;
    }

    uint32_t nItem = (uint32_t)pLVCD->nmcd.dwItemSpec;
    uint32_t nSubItem = pLVCD->iSubItem;
    size_t index = m_WatchListCtrl.GetItemData(nItem);
    CScanResult *presult = &m_WatchList[index];

    switch (nSubItem)
    {
    case WatchListCtrl_Col_Address:
        if (presult->m_AddressType == AddressType_Physical)
        {
            // Green if address is physical
            pLVCD->clrText = RGB(0x44, 0x88, 0x44);
        }
        break;
    case WatchListCtrl_Col_Value:
        if (presult->IsStringType())
        {
            pLVCD->clrText = RGB(0, 0, 0);
            if (presult->m_DisplayFormat == DisplayHex)
            {
                // Blue if hex string
                pLVCD->clrText = RGB(0, 0, 255);
            }
        }
        break;
    default:
        pLVCD->clrText = RGB(0, 0, 0);
        break;
    }

    return CDRF_DODEFAULT;
}

LRESULT CDebugMemorySearch::OnWatchListDblClick(LPNMHDR /*lpnmh*/)
{
    LONG iItem = m_WatchListCtrl.GetNextItem(-1, LVNI_SELECTED);
    if (iItem == -1)
    {
        return true;
    }

    int index = m_WatchListCtrl.GetItemData(iItem);
    CScanResult *presult = &m_WatchList[index];

    int nSelectedCol = -1;

    // Hit test for column

    POINT mousePt;
    RECT listRect;
    GetCursorPos(&mousePt);
    m_WatchListCtrl.GetWindowRect(&listRect);

    int mouseX = mousePt.x - listRect.left;

    for (int nCol = 0, colX = 0; nCol < WatchListCtrl_Num_Columns; nCol++)
    {
        int colWidth = m_WatchListCtrl.GetColumnWidth(nCol);
        if (mouseX >= colX && mouseX <= colX + colWidth)
        {
            nSelectedCol = nCol;
            break;
        }
        colX += colWidth;
    }

    switch (nSelectedCol)
    {
    case WatchListCtrl_Col_Lock:
        m_Debugger->Breakpoints()->ToggleMemLock(presult->m_Address);
        break;
    case WatchListCtrl_Col_Address:
        {
            if (m_SetValueDlg.DoModal("Change address", "New address:", stdstr_f("0x%08X", presult->m_Address).c_str()))
            {
                uint32_t newAddr = strtoul(m_SetValueDlg.GetEnteredString().c_str(), nullptr, 0);
                if (presult->SetAddressSafe(newAddr))
                {
                    m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Address, stdstr_f("0x%08X", newAddr).ToUTF16().c_str());
                }
            }
        }
        break;
    case WatchListCtrl_Col_Value:
        {
            char szInitialValue[1024];
            presult->GetMemoryValueString(szInitialValue, 1024);
            if (m_SetValueDlg.DoModal("Change value", "New value:", szInitialValue))
            {
                presult->SetMemoryValueFromString(m_SetValueDlg.GetEnteredString().c_str());
            }
        }
        break;
    case WatchListCtrl_Col_Description:
        {
            if (m_SetValueDlg.DoModal("Set description", "New description:", presult->GetDescription()))
            {
                stdstr EnteredString = m_SetValueDlg.GetEnteredString();
                presult->SetDescription(EnteredString.c_str());
                m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Description, EnteredString.ToUTF16().c_str());
            }
        }
        break;
    case WatchListCtrl_Col_Type:
        {
            if (m_SetValueDlg.DoModal("Change type", "New type:", presult->GetType(), ModalChangeTypeItems))
            {
                ValueType t = (ValueType) m_SetValueDlg.GetEnteredData();
                presult->SetType(t);

                if (t == ValueType_string)
                {
                    if (m_SetValueDlg.DoModal("String length", "New string length:", stdstr_f("%d", presult->GetStrLength()).c_str()))
                    {
                        int length = atoi(m_SetValueDlg.GetEnteredString().c_str());
                        presult->SetStrLengthSafe(length);
                    }

                    m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Type, stdstr_f("char[%d]", presult->GetStrLength()).ToUTF16().c_str());
                }
                else
                {
                    m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Type, stdstr(presult->GetTypeName()).ToUTF16().c_str());
                }
            }
        }
        break;
    }

    return true;
}

LRESULT CDebugMemorySearch::OnWatchListRClick(LPNMHDR /*lpnmh*/)
{
    LONG iItem = m_WatchListCtrl.GetNextItem(-1, LVNI_SELECTED);
    if (iItem == -1)
    {
        return true;
    }

    int index = m_WatchListCtrl.GetItemData(iItem);
    CScanResult *presult = &m_WatchList[index];

    // Load the menu
    HMENU hMenu = LoadMenu(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDR_MEM_WATCHLIST));
    HMENU hPopupMenu = GetSubMenu(hMenu, 0);

    // Get the current mouse location
    POINT Mouse;
    GetCursorPos(&Mouse);

    CBreakpoints* breakpoints = m_Debugger->Breakpoints();

    bool bHaveLock = breakpoints->MemLockExists(presult->m_Address, 1);
    bool bHaveReadBP = (breakpoints->ReadBPExists8(presult->m_Address) != CBreakpoints::BPSTATE::BP_NOT_SET);
    bool bHaveWriteBP = (breakpoints->WriteBPExists8(presult->m_Address) != CBreakpoints::BPSTATE::BP_NOT_SET);
    bool bHex = (presult->m_DisplayFormat == DisplayHex);

    CheckMenuItem(hPopupMenu, ID_WATCHLIST_LOCKVALUE, bHaveLock ? MF_CHECKED : MF_UNCHECKED);
    CheckMenuItem(hPopupMenu, ID_WATCHLIST_READBP, bHaveReadBP ? MF_CHECKED : MF_UNCHECKED);
    CheckMenuItem(hPopupMenu, ID_WATCHLIST_WRITEBP, bHaveWriteBP ? MF_CHECKED : MF_UNCHECKED);
    CheckMenuItem(hPopupMenu, ID_WATCHLIST_HEXADECIMAL, bHex ? MF_CHECKED : MF_UNCHECKED);

    // Show the menu
    TrackPopupMenu(hPopupMenu, 0, Mouse.x, Mouse.y, 0, m_hWnd, nullptr);
    DestroyMenu(hMenu);
    return true;
}

LRESULT CDebugMemorySearch::OnWatchListPopupViewMemory(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* presult = GetFirstSelectedWatchListResult();

    if (presult == nullptr)
    {
        return FALSE;
    }

    bool bUseVaddr = (presult->m_AddressType == AddressType_Virtual);
    m_Debugger->Debug_ShowMemoryLocation(presult->m_Address, bUseVaddr);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupLock(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    int nItems = m_WatchListCtrl.GetItemCount();
    for (int iItem = nItems - 1; iItem >= 0; iItem--)
    {
        bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

        if (bSelected)
        {
            int index = m_WatchListCtrl.GetItemData(iItem);
            CScanResult* presult = &m_WatchList[index];
            m_Debugger->Breakpoints()->ToggleMemLock(presult->m_Address);
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupReadBP(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* presult = GetFirstSelectedWatchListResult();

    if (presult == nullptr)
    {
        return FALSE;
    }

    m_Debugger->Breakpoints()->RBPToggle(presult->m_Address);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupWriteBP(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* presult = GetFirstSelectedWatchListResult();

    if (presult == nullptr)
    {
        return FALSE;
    }

    m_Debugger->Breakpoints()->WBPToggle(presult->m_Address);
    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupAddSymbol(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* presult = GetFirstSelectedWatchListResult();

    if (presult == nullptr)
    {
        return FALSE;
    }

    // TODO: fix magic numbers
    int nSymType = 1;

    switch (presult->GetType())
    {
    case ValueType_uint8: nSymType = 2; break;
    case ValueType_uint16: nSymType = 3; break;
    case ValueType_uint32: nSymType = 4; break;
    case ValueType_uint64: nSymType = 5; break;
    case ValueType_int8: nSymType = 6; break;
    case ValueType_int16: nSymType = 7; break;
    case ValueType_int32: nSymType = 8; break;
    case ValueType_int64: nSymType = 9; break;
    case ValueType_float: nSymType = 10; break;
    case ValueType_double: nSymType = 11; break;
    }

    m_AddSymbolDlg.DoModal(m_Debugger, presult->GetVirtualAddress(), nSymType);

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupHexadecimal(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    int nItems = m_WatchListCtrl.GetItemCount();
    for (int iItem = 0; iItem < nItems; iItem++)
    {
        bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

        if (bSelected)
        {
            int index = m_WatchListCtrl.GetItemData(iItem);
            CScanResult* presult = &m_WatchList[index];

            if (presult->m_DisplayFormat == DisplayDefault)
            {
                presult->m_DisplayFormat = DisplayHex;
            }
            else
            {
                presult->m_DisplayFormat = DisplayDefault;
            }
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupChangeValue(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* pFirstResult = GetFirstSelectedWatchListResult();

    if (pFirstResult == nullptr)
    {
        return FALSE;
    }

    char szPlaceholder[1024];
    pFirstResult->GetMemoryValueString(szPlaceholder, 1024);

    if (m_SetValueDlg.DoModal("Change value", "New value:", szPlaceholder))
    {
        int nItems = m_WatchListCtrl.GetItemCount();
        for (int iItem = nItems - 1; iItem >= 0; iItem--)
        {
            bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

            if (bSelected)
            {
                int index = m_WatchListCtrl.GetItemData(iItem);
                CScanResult* presult = &m_WatchList[index];
                // TODO: prompt for size change if string is too long
                presult->SetMemoryValueFromString(m_SetValueDlg.GetEnteredString().c_str());
                m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Value, stdstr(m_SetValueDlg.GetEnteredString()).ToUTF16().c_str());
            }
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupChangeDescription(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* pFirstResult = GetFirstSelectedWatchListResult();

    if (pFirstResult == nullptr)
    {
        return FALSE;
    }

    if (m_SetValueDlg.DoModal("Change description", "New description:", pFirstResult->GetDescription()))
    {
        int nItems = m_WatchListCtrl.GetItemCount();
        for (int iItem = 0; iItem < nItems; iItem++)
        {
            bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

            if (bSelected)
            {
                int index = m_WatchListCtrl.GetItemData(iItem);
                CScanResult* presult = &m_WatchList[index];
                stdstr description = m_SetValueDlg.GetEnteredString();
                presult->SetDescription(description.c_str());
                m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Description, description.ToUTF16().c_str());
            }
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupChangeType(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* pFirstResult = GetFirstSelectedWatchListResult();

    if (pFirstResult == nullptr)
    {
        return FALSE;
    }

    if (m_SetValueDlg.DoModal("Change type", "New type:", pFirstResult->GetType(), ModalChangeTypeItems))
    {
        ValueType t = (ValueType)m_SetValueDlg.GetEnteredData();
        int length = 0;

        if (t == ValueType_string)
        {
            if (m_SetValueDlg.DoModal("String length", "New string length:",
                stdstr_f("%d", pFirstResult->GetStrLength()).c_str()))
            {
                length = atoi(m_SetValueDlg.GetEnteredString().c_str());

                if (length <= 0)
                {
                    length = 1;
                }
            }
        }

        int nItems = m_WatchListCtrl.GetItemCount();
        for (int iItem = 0; iItem < nItems; iItem++)
        {
            bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

            if (bSelected)
            {
                int index = m_WatchListCtrl.GetItemData(iItem);
                CScanResult* presult = &m_WatchList[index];

                if (presult->IsStringType())
                {
                    presult->SetStrLengthSafe(length);

                    m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Type,
                        stdstr_f("char[%d]", presult->GetStrLength()).ToUTF16().c_str());
                }
                else
                {
                    m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Type, stdstr(presult->GetTypeName()).ToUTF16().c_str());
                }
            }
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupChangeAddress(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* pFirstResult = GetFirstSelectedWatchListResult();

    if (pFirstResult == nullptr)
    {
        return FALSE;
    }

    if (m_SetValueDlg.DoModal("Change address", "New address:", stdstr_f("0x%08X", pFirstResult->m_Address).c_str()))
    {
        uint32_t newAddr = strtoul(m_SetValueDlg.GetEnteredString().c_str(), nullptr, 0);
        stdstr newAddrStr = stdstr_f("0x%08X", newAddr);

        int nItems = m_WatchListCtrl.GetItemCount();
        for (int iItem = 0; iItem < nItems; iItem++)
        {
            bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

            if (bSelected)
            {
                int index = m_WatchListCtrl.GetItemData(iItem);
                CScanResult* presult = &m_WatchList[index];
                presult->SetAddressSafe(newAddr);
                m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Address, newAddrStr.ToUTF16().c_str());
            }
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupChangeAddressBy(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    CScanResult* pFirstResult = GetFirstSelectedWatchListResult();

    if (pFirstResult == nullptr)
    {
        return FALSE;
    }

    if (m_SetValueDlg.DoModal("Adjust address", "Address offset (+/-):", "0"))
    {
        int offset = atoi(m_SetValueDlg.GetEnteredString().c_str());

        int nItems = m_WatchListCtrl.GetItemCount();
        for (int iItem = 0; iItem < nItems; iItem++)
        {
            bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

            if (bSelected)
            {
                int index = m_WatchListCtrl.GetItemData(iItem);
                CScanResult* presult = &m_WatchList[index];
                uint32_t newAddr = presult->m_Address + offset;
                if (presult->SetAddressSafe(newAddr))
                {
                    stdstr newAddrStr = stdstr_f("0x%08X", presult->m_Address);
                    m_WatchListCtrl.SetItemText(iItem, WatchListCtrl_Col_Address, newAddrStr.ToUTF16().c_str());
                }
            }
        }
    }

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupRemove(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    RemoveSelectedWatchListItems();
    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupRemoveAll(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    ClearWatchList();
    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupCopyGamesharkCode(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    int numWatchListItems = m_WatchListCtrl.GetItemCount();

    stdstr strGSCode = "";

    for (int i = 0; i < numWatchListItems; i++)
    {
        if (m_WatchListCtrl.GetItemState(i, LVNI_SELECTED) == 0)
        {
            continue;
        }

        size_t index = m_WatchListCtrl.GetItemData(i);
        CScanResult* presult = &m_WatchList[index];

        uint32_t vaddr = presult->GetVirtualAddress();

        if (presult->IsStringType())
        {
            int length = presult->GetStrLength();

            char str[1024];
            presult->GetMemoryValueString(str, 1024, true);

            bool haveOddLength = (length & 1) != 0;
            int evenLength = length & ~1;

            if (length >= 2)
            {
                for (int j = 0; j < evenLength; j += 2)
                {
                    strGSCode += stdstr_f("%08X %02X%02X\n", (vaddr + j) | GS_TWOBYTE, (uint8_t)str[j], (uint8_t)str[j + 1]);
                }
            }

            if (haveOddLength)
            {
                strGSCode += stdstr_f("%08X 00%02X\n", (vaddr + length - 1), (uint8_t)str[length - 1]);
            }
        }
        else
        {
            CMixed value;
            presult->GetMemoryValue(&value);

            switch (presult->GetType())
            {
            case ValueType_uint8:
            case ValueType_int8:
                strGSCode += stdstr_f("%08X 00%02X\n", vaddr, value.m_Value._uint8);
                break;
            case ValueType_uint16:
            case ValueType_int16:
                strGSCode += stdstr_f("%08X %04X\n", vaddr | GS_TWOBYTE, value.m_Value._uint16);
                break;
            case ValueType_uint32:
            case ValueType_int32:
            case ValueType_float:
                strGSCode += stdstr_f("%08X %04X\n", (vaddr + 0) | GS_TWOBYTE, (uint16_t)(value.m_Value._uint32 >> 16));
                strGSCode += stdstr_f("%08X %04X\n", (vaddr + 2) | GS_TWOBYTE, (uint16_t)(value.m_Value._uint32));
                break;
            case ValueType_uint64:
            case ValueType_int64:
            case ValueType_double:
                strGSCode += stdstr_f("%08X %04X\n", (vaddr + 0) | GS_TWOBYTE, (uint16_t)(value.m_Value._uint64 >> 48));
                strGSCode += stdstr_f("%08X %04X\n", (vaddr + 2) | GS_TWOBYTE, (uint16_t)(value.m_Value._uint64 >> 32));
                strGSCode += stdstr_f("%08X %04X\n", (vaddr + 4) | GS_TWOBYTE, (uint16_t)(value.m_Value._uint64 >> 16));
                strGSCode += stdstr_f("%08X %04X\n", (vaddr + 6) | GS_TWOBYTE, (uint16_t)(value.m_Value._uint64));
                break;
            default:
                g_Notify->BreakPoint(__FILE__, __LINE__);
                break;
            }
        }
    }

    if (strGSCode.length() == 0)
    {
        return FALSE;
    }

    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, strGSCode.length());
    strncpy((char*)GlobalLock(hMem), strGSCode.c_str(), strGSCode.length() - 1);
    GlobalUnlock(hMem);
    OpenClipboard();
    EmptyClipboard();
    SetClipboardData(CF_TEXT, hMem);
    CloseClipboard();

    return FALSE;
}

LRESULT CDebugMemorySearch::OnWatchListPopupCopyAddressAndDescription(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    int numWatchListItems = m_WatchListCtrl.GetItemCount();

    stdstr str = "";

    for (int i = 0; i < numWatchListItems; i++)
    {
        if (m_WatchListCtrl.GetItemState(i, LVNI_SELECTED) == 0)
        {
            continue;
        }

        size_t index = m_WatchListCtrl.GetItemData(i);
        CScanResult* presult = &m_WatchList[index];

        str += stdstr_f("%08X %s\n", presult->m_Address, presult->GetDescription());
    }

    if (str.length() == 0)
    {
        return FALSE;
    }

    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, str.length());

    strncpy((char*)GlobalLock(hMem), str.c_str(), str.length() - 1);
    GlobalUnlock(hMem);
    OpenClipboard();
    EmptyClipboard();
    SetClipboardData(CF_TEXT, hMem);
    CloseClipboard();

    return FALSE;
}

LRESULT CDebugMemorySearch::OnSetFont(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    // Set row height for the results list and watch list
    CClientDC dc(m_hWnd);
    dc.SelectFont((HFONT)wParam);
    TEXTMETRIC tm;
    dc.GetTextMetrics(&tm);
    m_ListCtrlRowHeight = tm.tmHeight + tm.tmExternalLeading;
    return FALSE;
}

LRESULT CDebugMemorySearch::OnMeasureItem(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)
{
    if (wParam == IDC_LST_RESULTS || wParam == IDC_LST_WATCHLIST)
    {
        MEASUREITEMSTRUCT* lpMeasureItem = (MEASUREITEMSTRUCT*)lParam;
        lpMeasureItem->itemHeight = m_ListCtrlRowHeight;
    }
    return FALSE;
}

LRESULT CDebugMemorySearch::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 = scrollInfo.nPos - 1;
        break;
    case SB_LINEDOWN:
        newPos = scrollInfo.nPos + 1;
        break;
    case SB_THUMBTRACK:
        newPos = scrollInfo.nTrackPos;
        break;
    default:
        return 0;
    }

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

    if (scrlId == IDC_SCRL_RESULTS)
    {
        UpdateResultsList();
    }
    else if (scrlId == IDC_SCRL_WATCHLIST)
    {
        UpdateWatchList();
    }

    return 0;
}

LRESULT CDebugMemorySearch::OnMouseDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    if (MouseHovering(IDC_SEPARATOR, 0, 1))
    {
        ::GetWindowRect(GetDlgItem(IDC_SEPARATOR), &m_LastSeparatorRect);
        ScreenToClient(&m_LastSeparatorRect);
        m_bDraggingSeparator = true;
    }
    return 0;
}

LRESULT CDebugMemorySearch::OnMouseUp(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    if (m_bDraggingSeparator)
    {
        UpdateResultsList(true, false);
    }

    m_bDraggingSeparator = false;
    return 0;
}

void CDebugMemorySearch::OnInterceptMouseMove(WPARAM /*wParam*/, LPARAM /*lParam*/)
{
    if (MouseHovering(IDC_SEPARATOR, 0, 1) || m_bDraggingSeparator)
    {
        SetCursor(m_hCursorSizeNS);
    }

    CPoint cursorPos;
    GetCursorPos(&cursorPos);
    ScreenToClient(&cursorPos);

    if (m_bDraggingSeparator && cursorPos.y >= m_InitialSeparatorRect.top)
    {
        CRect sepRect, windowRect;
        int yChange = cursorPos.y - m_LastSeparatorRect.top;

        // Move separator
        HWND hSeparator = GetDlgItem(IDC_SEPARATOR);
        ::GetWindowRect(hSeparator, &sepRect);
        ScreenToClient(&sepRect);
        ::SetWindowPos(hSeparator, nullptr, sepRect.left, cursorPos.y, 0, 0,
            SWP_NOSIZE | SWP_NOZORDER);
        ::InvalidateRect(hSeparator, nullptr, true);

        // Move and resize controls
        SeparatorMoveCtrl(IDC_LST_WATCHLIST, yChange, false);
        SeparatorMoveCtrl(IDC_SCRL_WATCHLIST, yChange, false);
        SeparatorMoveCtrl(IDC_NUM_RESULTS, yChange, true);
        SeparatorMoveCtrl(IDC_LST_RESULTS, yChange, true);
        SeparatorMoveCtrl(IDC_SCRL_RESULTS, yChange, true);

        // Adjust window height
        GetWindowRect(&windowRect);
        windowRect.bottom += yChange;
        SetWindowPos(HWND_TOP, &windowRect, SWP_NOMOVE);

        // Save separator position
        ::GetWindowRect(hSeparator, &m_LastSeparatorRect);
        ScreenToClient(&m_LastSeparatorRect);
    }
}

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

    if (MouseHovering(IDC_LST_RESULTS) || MouseHovering(IDC_SCRL_RESULTS))
    {
        // Scroll results list
        int scrollPos = m_ResultsScrollbar.GetScrollPos();
        m_ResultsScrollbar.SetScrollPos(scrollPos + nScroll);
        UpdateResultsList();
    }
    else if (MouseHovering(IDC_LST_WATCHLIST) || MouseHovering(IDC_SCRL_WATCHLIST))
    {
        // Scroll watch list
        int scrollPos = m_WatchListScrollbar.GetScrollPos();
        m_WatchListScrollbar.SetScrollPos(scrollPos + nScroll);
        UpdateWatchList();
    }
}

// Utility

void CDebugMemorySearch::ClearWatchList(void)
{
    for (size_t i = 0; i < m_WatchList.size(); i++)
    {
        m_WatchList[i].DeleteDescription();
    }

    m_WatchList.clear();
    UpdateWatchList(true);
}

void CDebugMemorySearch::RemoveWatchListItem(int index)
{
    m_WatchList[index].DeleteDescription();
    m_WatchList.erase(m_WatchList.begin() + index);
}

void CDebugMemorySearch::RemoveSelectedWatchListItems(void)
{
    int nItems = m_WatchListCtrl.GetItemCount();
    for (int iItem = nItems - 1; iItem >= 0; iItem--)
    {
        bool bSelected = (m_WatchListCtrl.GetItemState(iItem, LVNI_SELECTED) != 0);

        if (bSelected)
        {
            int index = m_WatchListCtrl.GetItemData(iItem);
            RemoveWatchListItem(index);
        }
    }

    UpdateWatchList();
}

void CDebugMemorySearch::AddResultToWatchList(int resultIndex)
{
    CScanResult result = *m_MemoryScanner.GetResult(resultIndex);
    m_WatchList.push_back(result);
}

CScanResult* CDebugMemorySearch::GetFirstSelectedScanResult(void)
{
    LONG iItem = m_ResultsListCtrl.GetNextItem(-1, LVNI_SELECTED);

    if (iItem == -1)
    {
        return nullptr;
    }

    int index = m_ResultsListCtrl.GetItemData(iItem);
    CScanResult *presult = m_MemoryScanner.GetResult(index);
    return presult;
}

CScanResult* CDebugMemorySearch::GetFirstSelectedWatchListResult(void)
{
    LONG iItem = m_WatchListCtrl.GetNextItem(-1, LVNI_SELECTED);

    if (iItem == -1)
    {
        return nullptr;
    }

    int index = m_WatchListCtrl.GetItemData(iItem);
    CScanResult *presult = &m_WatchList[index];
    return presult;
}

void CDebugMemorySearch::SetComboBoxSelByData(CComboBox& cb, DWORD_PTR data)
{
    int numOptions = cb.GetCount();

    for (int i = 0; i < numOptions; i++)
    {
        if (cb.GetItemData(i) == data)
        {
            cb.SetCurSel(i);
            break;
        }
    }
}

bool CDebugMemorySearch::MouseHovering(WORD ctrlId, int hMargin, int vMargin)
{
    CRect ctrlRect;
    POINT pointerPos;

    ::GetCursorPos(&pointerPos);

    HWND hWnd = WindowFromPoint(pointerPos);

    if (hWnd != m_hWnd && ::GetDlgCtrlID(hWnd) != ctrlId)
    {
        return false;
    }

    ::GetWindowRect(GetDlgItem(ctrlId), &ctrlRect);

    return (
        pointerPos.x >= ctrlRect.left - hMargin &&
        pointerPos.x <= ctrlRect.right + hMargin &&
        pointerPos.y >= ctrlRect.top - vMargin &&
        pointerPos.y <= ctrlRect.bottom + vMargin);
}

void CDebugMemorySearch::SeparatorMoveCtrl(WORD ctrlId, int yChange, bool bResize)
{
    CRect rect;
    HWND hControl = GetDlgItem(ctrlId);
    ::GetWindowRect(hControl, &rect);
    ScreenToClient(&rect);

    if (bResize) // Resize control
    {
        ::SetWindowPos(hControl, nullptr, 0, 0, rect.Width(), rect.Height() + yChange,
            SWP_NOMOVE | SWP_NOZORDER);
    }
    else // Move control
    {
        ::SetWindowPos(hControl, nullptr, rect.left, rect.top + yChange, 0, 0,
            SWP_NOSIZE | SWP_NOZORDER);
    }

    ::InvalidateRect(hControl, nullptr, true);
}

int CDebugMemorySearch::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_ListCtrlRowHeight);
}

void CDebugMemorySearch::ResetResults(void)
{
    ::SetWindowTextA(GetDlgItem(IDC_NUM_RESULTS), "Results");
    m_MemoryScanner.Reset();
    UpdateResultsList(true);
    UpdateOptions();
}

void CDebugMemorySearch::Search(void)
{
    ValueType valueType = (ValueType) m_ValueTypeOptions.GetItemData(m_ValueTypeOptions.GetCurSel());
    SearchType searchType = (SearchType) m_SearchTypeOptions.GetItemData(m_SearchTypeOptions.GetCurSel());
    DWORD startAddress = m_AddrStart.GetValue();
    DWORD endAddress = m_AddrEnd.GetValue();

    bool bHexChecked = (m_HexCheckbox.GetCheck() == BST_CHECKED);
    bool bPhysicalChecked = (m_PhysicalCheckbox.GetCheck() == BST_CHECKED);

    MixedValue value;
    int stringValueLength;
    bool bUseSearchValue;

    if (valueType == ValueType_string)
    {
        if (m_UnkEncodingCheckbox.GetCheck() == BST_CHECKED)
        {
            valueType = ValueType_unkstring;
        }
        else if (m_IgnoreCaseCheckbox.GetCheck() == BST_CHECKED)
        {
            valueType = ValueType_istring;
        }
    }
    else if (m_UnsignedCheckbox.GetCheck() == BST_CHECKED)
    {
        switch (valueType)
        {
        case ValueType_int8:  valueType = ValueType_uint8; break;
        case ValueType_int16: valueType = ValueType_uint16; break;
        case ValueType_int32: valueType = ValueType_uint32; break;
        case ValueType_int64: valueType = ValueType_uint64; break;
        }
    }

    switch (searchType)
    {
    case SearchType_UnknownValue:
    case SearchType_ChangedValue:
    case SearchType_UnchangedValue:
    case SearchType_IncreasedValue:
    case SearchType_DecreasedValue:
        bUseSearchValue = false;
        break;
    default:
        bUseSearchValue = true;
        m_SearchValue.SetType(valueType);
        break;
    }

    m_MemoryScanner.SetSearchType(searchType);
    m_MemoryScanner.SetValueType(valueType);

    if (bUseSearchValue)
    {
        switch (valueType)
        {
        case ValueType_uint8:
            if (!m_SearchValue.GetValue(value._uint8))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<uint8_t>(value._uint8);
            break;
        case ValueType_int8:
            if (!m_SearchValue.GetValue(value._sint8))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<int8_t>(value._sint8);
            break;
        case ValueType_uint16:
            if (!m_SearchValue.GetValue(value._uint16))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<uint16_t>(value._uint16);
            break;
        case ValueType_int16:
            if (!m_SearchValue.GetValue(value._sint16))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<int16_t>(value._sint16);
            break;
        case ValueType_uint32:
            if (!m_SearchValue.GetValue(value._uint32))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<uint32_t>(value._uint32);
            break;
        case ValueType_int32:
            if (!m_SearchValue.GetValue(value._sint32))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<int32_t>(value._sint32);
            break;
        case ValueType_uint64:
            if (!m_SearchValue.GetValue(value._uint64))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<uint64_t>(value._uint64);
            break;
        case ValueType_int64:
            if (!m_SearchValue.GetValue(value._sint64))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<int64_t>(value._sint64);
            break;
        case ValueType_float:
            if (!m_SearchValue.GetValue(value._float))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<float>(value._float);
            break;
        case ValueType_double:
            if (!m_SearchValue.GetValue(value._double))
            {
                goto value_parse_error;
            }
            m_MemoryScanner.SetValue<double>(value._double);
            break;
        case ValueType_string:
            if (bHexChecked)
            {
                if (!m_SearchValue.GetValueHexString(value._string, stringValueLength))
                {
                    goto value_parse_error;
                }
            }
            else
            {
                if (!m_SearchValue.GetValueString(value._string, stringValueLength))
                {
                    goto value_parse_error;
                }
            }
            m_MemoryScanner.SetValue<const wchar_t*>(value._string);
            m_MemoryScanner.SetStringValueLength(stringValueLength);
            break;
        case ValueType_unkstring:
        case ValueType_istring:
            if (!m_SearchValue.GetValueString(value._string, stringValueLength))
            {
                goto value_parse_error;
            }

            m_MemoryScanner.SetValue<const wchar_t*>(value._string);
            m_MemoryScanner.SetStringValueLength(stringValueLength);
            break;
        default:
            MessageBox(L"Unimplemented value type", L"Unimplemented", MB_OK);
            return;
        }
    }

    if (!m_MemoryScanner.DidFirstScan())
    {
        m_MemoryScanner.SetAddressType(bPhysicalChecked ? AddressType_Physical : AddressType_Virtual);
        bool bAddressRangeValid = m_MemoryScanner.SetAddressRange(startAddress, endAddress);

        if (!bAddressRangeValid)
        {
            MessageBox(L"Invalid address range", L"Invalid address range");
            return;
        }

        m_MemoryScanner.FirstScan(m_SearchValue.GetDisplayFormat());
        UpdateOptions();
    }
    else
    {
        m_MemoryScanner.NextScan();

        if (m_MemoryScanner.GetNumResults() == 0)
        {
            UpdateOptions();
        }
    }

    UpdateResultsList(true);
    return;

value_parse_error:
    MessageBox(L"Invalid value", L"Invalid value", MB_OK);
    return;
}

void CDebugMemorySearch::UpdateOptions(void)
{
    bool bDidFirstScan = m_MemoryScanner.DidFirstScan();
    bool bHaveResults = (m_MemoryScanner.GetNumResults() > 0);

    m_ValueTypeOptions.EnableWindow(!bDidFirstScan);
    ::EnableWindow(GetDlgItem(IDC_BTN_RESET), bDidFirstScan);
    ::EnableWindow(GetDlgItem(IDC_BTN_RDRAM), !bDidFirstScan);
    ::EnableWindow(GetDlgItem(IDC_BTN_ROM), !bDidFirstScan);
    ::EnableWindow(GetDlgItem(IDC_BTN_SPMEM), !bDidFirstScan);
    m_PhysicalCheckbox.EnableWindow(!bDidFirstScan);
    m_AddrStart.EnableWindow(!bDidFirstScan);
    m_AddrEnd.EnableWindow(!bDidFirstScan);

    m_UnsignedCheckbox.EnableWindow(!bDidFirstScan);
    m_IgnoreCaseCheckbox.EnableWindow(!bDidFirstScan);
    m_UnkEncodingCheckbox.EnableWindow(!bDidFirstScan);

    SearchType searchType = (SearchType)m_SearchTypeOptions.GetItemData(m_SearchTypeOptions.GetCurSel());
    ValueType valueType = (ValueType)m_ValueTypeOptions.GetItemData(m_ValueTypeOptions.GetCurSel());

    if (!bDidFirstScan)
    {
        m_SearchTypeOptions.EnableWindow(TRUE);

        CComboBox & cb = m_SearchTypeOptions;
        cb.ResetContent();
        cb.SetItemData(cb.AddString(L"Exact value"), SearchType_ExactValue);
        cb.SetItemData(cb.AddString(L"Unknown initial value"), SearchType_UnknownValue);
        cb.SetItemData(cb.AddString(L"Greater than..."), SearchType_GreaterThanValue);
        cb.SetItemData(cb.AddString(L"Less than..."), SearchType_LessThanValue);
        cb.SetItemData(cb.AddString(L"JAL to..."), SearchType_JalTo);
        cb.SetCurSel(0);

        if (valueType == ValueType_string)
        {
            m_SearchTypeOptions.EnableWindow(false);
        }

        m_SearchValue.EnableWindow(TRUE);

        ::EnableWindow(GetDlgItem(IDC_BTN_SEARCH), true);
    }
    else
    {
        CComboBox & cb = m_SearchTypeOptions;
        cb.ResetContent();
        cb.SetItemData(cb.AddString(L"Exact value"), SearchType_ExactValue);
        cb.SetItemData(cb.AddString(L"Changed value"), SearchType_ChangedValue);
        cb.SetItemData(cb.AddString(L"Unchanged value"), SearchType_UnchangedValue);
        cb.SetItemData(cb.AddString(L"Greater than..."), SearchType_GreaterThanValue);
        cb.SetItemData(cb.AddString(L"Less than..."), SearchType_LessThanValue);
        cb.SetItemData(cb.AddString(L"Increased value"), SearchType_IncreasedValue);
        cb.SetItemData(cb.AddString(L"Decreased value"), SearchType_DecreasedValue);
        cb.SetCurSel(0);

        if (m_bJalSelected)
        {
            m_HexCheckbox.SetCheck(m_bJalHexWasChecked ? BST_CHECKED : BST_UNCHECKED);
            m_UnsignedCheckbox.SetCheck(m_bJalUnsignedWasChecked ? BST_CHECKED : BST_UNCHECKED);
            m_bJalSelected = false;
            m_bJalHexWasChecked = false;
            m_bJalUnsignedWasChecked = false;
        }

        m_HexCheckbox.EnableWindow(TRUE);
        m_UnsignedCheckbox.EnableWindow(TRUE);

        m_SearchValue.EnableWindow(TRUE);

        if (searchType == SearchType_JalTo ||
            valueType == ValueType_string ||
            valueType == ValueType_istring ||
            valueType == ValueType_unkstring)
        {
            // Complex search types, disable next search
            ::EnableWindow(GetDlgItem(IDC_BTN_SEARCH), false);
            m_SearchTypeOptions.EnableWindow(FALSE);
        }
        else
        {
            m_UnsignedCheckbox.ShowWindow(SW_SHOW);
            ::EnableWindow(GetDlgItem(IDC_BTN_SEARCH), bHaveResults);
            m_SearchTypeOptions.EnableWindow(bHaveResults);
        }
    }
}

void CDebugMemorySearch::UpdateResultsList(bool bUpdateScrollbar, bool bResetScrollPos)
{
    size_t numResults = m_MemoryScanner.GetNumResults();
    size_t numVisibleRows = GetNumVisibleRows(m_ResultsListCtrl);

    if (bUpdateScrollbar)
    {
        bool bCanDisplayAll = (numVisibleRows >= numResults);
        int scrollRangeMax = bCanDisplayAll ? 0 : numResults - numVisibleRows;

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

        if (bResetScrollPos)
        {
            m_ResultsScrollbar.SetScrollPos(0, true);
        }
    }

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

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

    m_ResultsListCtrl.SetRedraw(FALSE);
    m_ResultsListCtrl.DeleteAllItems();

    int nItem = 0;

    for (size_t index = start; index < end; index++)
    {
        char szAddress[32];
        char szCurrentValue[1024];
        char szValue[1024];

        CScanResult *presult = m_MemoryScanner.GetResult(index);

        presult->GetAddressString(szAddress);
        presult->GetMemoryValueString(szCurrentValue, 1024);
        presult->GetValueString(szValue, 1024);

        m_ResultsListCtrl.AddItem(nItem, ResultsListCtrl_Col_Address, stdstr(szAddress).ToUTF16().c_str());
        m_ResultsListCtrl.SetItemText(nItem, ResultsListCtrl_Col_Value, stdstr(szCurrentValue).ToUTF16().c_str());
        m_ResultsListCtrl.SetItemText(nItem, ResultsListCtrl_Col_Previous, stdstr(szValue).ToUTF16().c_str());
        m_ResultsListCtrl.SetItemData(nItem, index);

        nItem++;
    }

    m_ResultsListCtrl.SetRedraw(TRUE);

    char szNumResults[32];
    sprintf(szNumResults, "Results (%d)", numResults);
    ::SetWindowTextA(GetDlgItem(IDC_NUM_RESULTS), szNumResults);
}

void CDebugMemorySearch::UpdateWatchList(bool bUpdateScrollbar)
{
    size_t numEntries = m_WatchList.size();
    size_t numVisibleRows = GetNumVisibleRows(m_WatchListCtrl);

    if (bUpdateScrollbar)
    {
        bool bCanDisplayAll = (numVisibleRows >= numEntries);
        int scrollRangeMax = bCanDisplayAll ? 0 : numEntries - numVisibleRows;

        m_WatchListScrollbar.SetScrollRange(0, scrollRangeMax, false);
        m_WatchListScrollbar.SetScrollPos(0, true);
        m_WatchListScrollbar.EnableWindow(!bCanDisplayAll);
    }

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

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

    m_WatchListCtrl.SetRedraw(FALSE);
    m_WatchListCtrl.DeleteAllItems();

    int nItem = 0;

    for (size_t index = start; index < end; index++)
    {
        CScanResult *presult = &m_WatchList[index];
        char szAddress[16];
        char szValue[1024];
        char szValueType[32];
        const char* pSzValueType;
        const char* pSzDescription;

        presult->GetAddressString(szAddress);
        presult->GetMemoryValueString(szValue, 1024);
        pSzValueType = presult->GetTypeName();
        pSzDescription = presult->GetDescription();

        switch(presult->GetType())
        {
        case ValueType_string:
        case ValueType_istring:
        case ValueType_unkstring:
            sprintf(szValueType, "%s[%d]", pSzValueType, presult->GetStrLength());
            pSzValueType = szValueType;
            break;
        }

        m_WatchListCtrl.AddItem(nItem, WatchListCtrl_Col_Lock, L"");
        m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_BP, L"");
        m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_Address, stdstr(szAddress).ToUTF16().c_str());
        m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_Description, stdstr(pSzDescription).ToUTF16().c_str());
        m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_Type, stdstr(pSzValueType).ToUTF16().c_str());
        m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_Value, stdstr(szValue).ToUTF16().c_str());
        m_WatchListCtrl.SetItemData(nItem, index);

        //if (presult->IsSelected())
        //{
        //    m_WatchListCtrl.SetItemState(nItem, LVIS_SELECTED, LVIS_SELECTED);
        //}

        nItem++;
    }

    m_WatchListCtrl.SetRedraw(TRUE);
}

void CDebugMemorySearch::RefreshResultsListValues(void)
{
    if (!g_MMU || m_bDraggingSeparator)
    {
        return;
    }

    int numShownResults = m_ResultsListCtrl.GetItemCount();

    if (numShownResults > 0)
    {
        m_ResultsListCtrl.SetRedraw(FALSE);

        for (int nItem = 0; nItem < numShownResults; nItem++)
        {
            size_t index = m_ResultsListCtrl.GetItemData(nItem);
            CScanResult *presult = m_MemoryScanner.GetResult(index);

            if (presult == nullptr)
            {
                g_Notify->BreakPoint(__FILE__, __LINE__);
                break;
            }

            char szCurrentValue[1024];
            presult->GetMemoryValueString(szCurrentValue, 1024);

            m_ResultsListCtrl.SetItemText(nItem, ResultsListCtrl_Col_Value, stdstr(szCurrentValue).ToUTF16().c_str());
        }

        m_ResultsListCtrl.SetRedraw(TRUE);
    }
}

void CDebugMemorySearch::RefreshWatchListValues(void)
{
    if (!g_MMU || m_bDraggingSeparator)
    {
        return;
    }

    int numShownWatchListEntries = m_WatchListCtrl.GetItemCount();

    if (numShownWatchListEntries > 0)
    {
        m_WatchListCtrl.SetRedraw(FALSE);

        for (int nItem = 0; nItem < numShownWatchListEntries; nItem++)
        {
            size_t index = m_WatchListCtrl.GetItemData(nItem);
            CScanResult *presult = &m_WatchList[index];

            CBreakpoints *breakpoints = m_Debugger->Breakpoints();

            bool bHaveLock = breakpoints->MemLockExists(presult->m_Address, 1);
            bool bHaveReadBP = (breakpoints->ReadBPExists8(presult->m_Address) != CBreakpoints::BPSTATE::BP_NOT_SET);
            bool bHaveWriteBP = (breakpoints->WriteBPExists8(presult->m_Address) != CBreakpoints::BPSTATE::BP_NOT_SET);
            bool bHaveExecBP = (breakpoints->ExecutionBPExists(presult->m_Address) != CBreakpoints::BPSTATE::BP_NOT_SET);

            char szBPStates[4];
            sprintf(szBPStates, "%s%s%s",
                bHaveReadBP ? "R" : "",
                bHaveWriteBP ? "W" : "",
                bHaveExecBP ? "E" : "");

            char szCurrentValue[1024];
            presult->GetMemoryValueString(szCurrentValue, 1024);

            m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_Lock, (bHaveLock ? L"X" : L""));
            m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_BP, stdstr(szBPStates).ToUTF16().c_str());
            m_WatchListCtrl.SetItemText(nItem, WatchListCtrl_Col_Value, stdstr(szCurrentValue).ToUTF16().c_str());
        }

        m_WatchListCtrl.SetRedraw(TRUE);
    }
}

void CDebugMemorySearch::FlushWatchList(void)
{
    if (m_WatchList.size() == 0)
    {
        return;
    }

    CPath wlPath = GetWatchListPath();
    m_WatchListFile.Open(wlPath, CFileBase::modeCreate | CFile::modeWrite);

    if (!m_WatchListFile.IsOpen())
    {
        return;
    }

    size_t numWatchListEntries = m_WatchList.size();

    for (size_t i = 0; i < numWatchListEntries; i++)
    {
        CScanResult* presult = &m_WatchList[i];

        char szValueType[32];
        const char* pSzValueType;

        pSzValueType = presult->GetTypeName();

        switch (presult->GetType())
        {
        case ValueType_string:
        case ValueType_istring:
        case ValueType_unkstring:
            sprintf(szValueType, "%s[%d]", pSzValueType, presult->GetStrLength());
            pSzValueType = szValueType;
            break;
        }

        char cAddrType = (presult->m_AddressType == AddressType_Physical) ? 'p' : 'v';
        const char* szDisplayFormat = (presult->m_DisplayFormat == DisplayDefault) ? "def" : "hex";

        stdstr line = stdstr_f("%c,%08X,%s,%s,%s\n",
            cAddrType, presult->m_Address, pSzValueType, szDisplayFormat, presult->GetDescription());

        m_WatchListFile.Write(line.c_str(), line.length());
    }

    m_WatchListFile.Close();

    return;
}

void CDebugMemorySearch::LoadWatchList(void)
{
    if (m_hWnd)
    {
        ClearWatchList();
    }

    CPath wlPath = GetWatchListPath();
    m_WatchListFile.Open(wlPath, CFileBase::modeRead);

    if (!m_WatchListFile.IsOpen())
    {
        return;
    }

    uint32_t length = m_WatchListFile.GetLength();
    char* szWlFile = new char[length + 1];

    m_WatchListFile.SeekToBegin();
    m_WatchListFile.Read(szWlFile, length);
    szWlFile[length] = '\0';

    m_WatchListFile.Close();

    char* p = szWlFile;

    while (*p)
    {
        CScanResult result(AddressType_Virtual, DisplayDefault);

        char* szAddrType, *szAddress, *szValueType, *szDisplayFormat, *szDescription;

        szAddrType = p; p = strchr(p, ','); *p++ = '\0';
        szAddress = p; p = strchr(p, ','); *p++ = '\0';
        szValueType = p; p = strchr(p, ','); *p++ = '\0';
        szDisplayFormat = p; p = strchr(p, ','); *p++ = '\0';
        szDescription = p; p = strchr(p, '\n'); *p++ = '\0';

        switch (szAddrType[0])
        {
        case 'v': result.m_AddressType = AddressType_Virtual; break;
        case 'p': result.m_AddressType = AddressType_Physical; break;
        default: goto parse_error;
        }

        uint32_t address = strtoul(szAddress, nullptr, 16);
        result.m_Address = address;

        ValueType type;
        int charArrayLength = 0;
        type = CMixed::GetTypeFromString(szValueType, &charArrayLength);

        if (type == ValueType_invalid)
        {
            goto parse_error;
        }

        result.SetType(type);

        if (result.IsStringType())
        {
            // g_MMU is null here, can't use SetStrLengthSafe
            // TODO: fix
            result.SetStrLength(charArrayLength);
        }

        if (strcmp(szDisplayFormat, "hex") == 0)
        {
            result.m_DisplayFormat = DisplayHex;
        }
        else if (strcmp(szDisplayFormat, "def") == 0)
        {
            result.m_DisplayFormat = DisplayDefault;
        }
        else
        {
            goto parse_error;
        }

        if (strlen(szDescription) > 0)
        {
            result.SetDescription(szDescription);
        }

        m_WatchList.push_back(result);
    }

    UpdateWatchList();

    parse_error:
    delete[] szWlFile;

}

void CDebugMemorySearch::FixListHeader(CListViewCtrl& listCtrl)
{
    CRect listRect, headRect;
    CHeaderCtrl listHead = listCtrl.GetHeader();

    listCtrl.GetWindowRect(&listRect);
    listHead.GetWindowRect(&headRect);

    listHead.ResizeClient(listRect.Width(), headRect.Height());
}

CPath CDebugMemorySearch::GetWatchListPath(void)
{
    stdstr strSaveDir = g_Settings->LoadStringVal(Directory_NativeSave);

    stdstr wlFileName = stdstr_f("%s.wlst", m_StrGame.c_str());

    CPath wlFilePath(strSaveDir.c_str(), wlFileName.c_str());

    if (g_Settings->LoadBool(Setting_UniqueSaveDir))
    {
        stdstr strUniqueSaveDir = g_Settings->LoadStringVal(Game_UniqueSaveDir);
        wlFilePath.AppendDirectory(strUniqueSaveDir.c_str());
    }

    wlFilePath.NormalizePath(CPath(CPath::MODULE_DIRECTORY));

    if (!wlFilePath.DirectoryExists())
    {
        wlFilePath.DirectoryCreate();
    }

    return wlFilePath;
}

INT_PTR CSetValueDlg::DoModal(const char* caption, const char* label, const char* initialText)
{
    m_Mode = Mode_TextBox;
    m_Caption = caption;
    m_Label = label;
    m_InitialText = initialText;
    return CDialogImpl<CSetValueDlg>::DoModal();
}

INT_PTR CSetValueDlg::DoModal(const char* caption, const char* label, DWORD_PTR initialData, const ComboItem items[])
{
    m_Mode = Mode_ComboBox;
    m_Caption = caption;
    m_Label = label;
    m_InitialData = initialData;
    m_ComboItems = items;
    return CDialogImpl<CSetValueDlg>::DoModal();
}

const std::string & CSetValueDlg::GetEnteredString(void)
{
    return m_EnteredString;
}

DWORD_PTR CSetValueDlg::GetEnteredData(void)
{
    if (m_Mode != Mode_ComboBox)
    {
        g_Notify->BreakPoint(__FILE__, __LINE__);
    }

    return m_EnteredData;
}

LRESULT CSetValueDlg::OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    SetWindowText(stdstr(m_Caption).ToUTF16().c_str());
    CenterWindow();
    m_Value.Attach(GetDlgItem(IDC_EDIT_VALUE));
    m_CmbValue.Attach(GetDlgItem(IDC_CMB_VALUE));
    m_Prompt.Attach(GetDlgItem(IDC_LBL_PROMPT));

    m_Prompt.SetWindowText(stdstr(m_Label).ToUTF16().c_str());

    if (m_Mode == Mode_TextBox)
    {
        m_CmbValue.ShowWindow(SW_HIDE);
        m_Value.SetWindowText(stdstr(m_InitialText).ToUTF16().c_str());
        m_Value.SetFocus();
        m_Value.SetSelAll();
    }
    else if (m_Mode == Mode_ComboBox)
    {
        m_Value.ShowWindow(SW_HIDE);

        for (int i = 0; m_ComboItems[i].str != nullptr; i++)
        {
            int idx = m_CmbValue.AddString(stdstr(m_ComboItems[i].str).ToUTF16().c_str());
            m_CmbValue.SetItemData(idx, m_ComboItems[i].data);
            if (m_ComboItems[i].data == m_InitialData)
            {
                m_CmbValue.SetCurSel(idx);
            }
        }
    }

    return FALSE;
}

LRESULT CSetValueDlg::OnDestroy(void)
{
    m_Value.Detach();
    m_CmbValue.Detach();
    m_Prompt.Detach();
    return 0;
}

LRESULT CSetValueDlg::OnOK(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    m_EnteredString.clear();
    if (m_Mode == Mode_TextBox)
    {
        m_EnteredString = GetCWindowText(m_Value);
    }
    else if (m_Mode == Mode_ComboBox)
    {
        m_EnteredString = GetCWindowText(m_CmbValue);
        m_EnteredData = m_CmbValue.GetItemData(m_CmbValue.GetCurSel());
    }

    EndDialog(TRUE);
    return FALSE;
}

LRESULT CSetValueDlg::OnCancel(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hwnd*/, BOOL& /*bHandled*/)
{
    EndDialog(FALSE);
    return FALSE;
}

CSetValueDlg::CSetValueDlg(void)
{
}

CSetValueDlg::~CSetValueDlg(void)
{
}

CEditMixed::CEditMixed(void) :
    m_String(nullptr)
{
}

CEditMixed::~CEditMixed(void)
{
}

DisplayFormat CEditMixed::GetDisplayFormat(void)
{
    return m_DisplayFormat;
}

void CEditMixed::SetDisplayFormat(DisplayFormat fmt)
{
    m_DisplayFormat = fmt;
}

void CEditMixed::ReloadString(void)
{
    if (m_String != nullptr)
    {
        delete[] m_String;
    }

    m_StringLength = GetWindowTextLength();
    m_String = new wchar_t[m_StringLength + 1];
    GetWindowText(m_String, m_StringLength + 1);
}

BOOL CEditMixed::Attach(HWND hWndNew)
{
    return SubclassWindow(hWndNew);
}

bool CEditMixed::GetValue(uint8_t& value)
{
    uint64_t valueU64;
    bool bValid = GetValue(valueU64);

    if (!bValid || valueU64 > UINT8_MAX)
    {
        return false;
    }

    value = (uint8_t)(valueU64 & 0xFF);
    return true;
}

bool CEditMixed::GetValue(int8_t& value)
{
    if (m_DisplayFormat == DisplayHex)
    {
        return GetValue((uint8_t&)value);
    }

    int64_t valueS64;
    bool bValid = GetValue(valueS64);

    if (!bValid || valueS64 > INT8_MAX || valueS64 < INT8_MIN)
    {
        return false;
    }

    value = (int8_t)(valueS64 & 0xFF);
    return true;
}

bool CEditMixed::GetValue(uint16_t& value)
{
    uint64_t valueU64;
    bool bValid = GetValue(valueU64);

    if (!bValid || valueU64 > UINT16_MAX)
    {
        return false;
    }

    value = (uint16_t)(valueU64 & 0xFFFF);
    return true;
}

bool CEditMixed::GetValue(int16_t& value)
{
    if (m_DisplayFormat == DisplayHex)
    {
        return GetValue((uint16_t&)value);
    }

    int64_t valueS64;
    bool bValid = GetValue(valueS64);

    if (!bValid || valueS64 > INT16_MAX || valueS64 < INT16_MIN)
    {
        return false;
    }

    value = (int16_t)(valueS64 & 0xFFFF);
    return true;
}

bool CEditMixed::GetValue(uint32_t& value)
{
    uint64_t valueU64;
    bool bValid = GetValue(valueU64);

    if (!bValid || valueU64 > UINT32_MAX)
    {
        return false;
    }

    value = (uint32_t)(valueU64 & 0xFFFFFFFF);
    return true;
}

bool CEditMixed::GetValue(int32_t& value)
{
    if (m_DisplayFormat == DisplayHex)
    {
        return GetValue((uint32_t&)value);
    }

    int64_t valueS64;
    bool bValid = GetValue(valueS64);

    if (!bValid || (valueS64 > INT32_MAX) || (valueS64 < INT32_MIN))
    {
        return false;
    }

    value = (int32_t)(valueS64 & 0xFFFFFFFF);
    return true;
}

bool CEditMixed::GetValue(uint64_t& value)
{
    ReloadString();

    char *end;
    uint64_t res = strtoull(stdstr().FromUTF16(m_String).c_str(), &end, m_DisplayFormat == DisplayHex ? 16 : 10);

    if (*end != '\0')
    {
        return false; // Parse failure
    }

    value = res;
    return true;
}

bool CEditMixed::GetValue(int64_t& value)
{
    if (m_DisplayFormat == DisplayHex)
    {
        return GetValue((uint64_t&)value);
    }

    ReloadString();

    char *end;
    uint64_t res = strtoll(stdstr().FromUTF16(m_String).c_str(), &end, m_DisplayFormat == DisplayHex ? 16 : 10);

    if (*end != '\0')
    {
        return false; // Parse failure
    }

    value = res;
    return true;
}

bool CEditMixed::GetValue(float& value)
{
    if (m_DisplayFormat == DisplayHex)
    {
        return GetValue((uint32_t&)value);
    }

    ReloadString();

    float valueF32;
    char *end;

    valueF32 = strtof(stdstr().FromUTF16(m_String).c_str(), &end);

    if (*end != '\0')
    {
        return false;
    }

    value = valueF32;
    return true;
}

bool CEditMixed::GetValue(double& value)
{
    if (m_DisplayFormat == DisplayHex)
    {
        return GetValue((uint64_t&)value);
    }

    ReloadString();

    double valueF64;
    char *end;

    valueF64 = strtod(stdstr().FromUTF16(m_String).c_str(), &end);

    if (*end != '\0')
    {
        return false;
    }

    value = valueF64;
    return true;
}

bool CEditMixed::GetValueString(const wchar_t*& value, int& length)
{
    ReloadString();

    if (m_StringLength == 0)
    {
        return false;
    }

    value = m_String;
    length = m_StringLength;
    return true;
}

bool CEditMixed::GetValueHexString(const wchar_t*& value, int& length)
{
    ReloadString();

    if (m_StringLength == 0)
    {
        return false;
    }

    stdstr string = stdstr().FromUTF16(m_String);
    int numBytes = CMemoryScanner::ParseHexString(nullptr, string.c_str());

    if (numBytes == 0)
    {
        return false;
    }

    
    char *hexString = new char[numBytes];
    wchar_t *wchexString = new wchar_t[numBytes];

    CMemoryScanner::ParseHexString(hexString, string.c_str());
    wcscpy(wchexString, stdstr(hexString).ToUTF16().c_str());

    delete[] hexString;
    delete[] m_String;

    m_String = wchexString;
    m_StringLength = numBytes;

    value = m_String;
    length = m_StringLength;
    return true;
}