/*
 * PROJECT:     ReactOS Picture and Fax Viewer
 * LICENSE:     GPL-2.0 (https://spdx.org/licenses/GPL-2.0)
 * PURPOSE:     Image file browsing and manipulation
 * COPYRIGHT:   Copyright Dmitry Chapyshev (dmitry@reactos.org)
 *              Copyright 2018-2023 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
 *              Copyright 2025 Whindmar Saksit <whindsaks@proton.me>
 */

#include "shimgvw.h"
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <shlobj.h>
#include <shellapi.h>

EXTERN_C PCWSTR GetExtraExtensionsGdipList(VOID);
EXTERN_C HRESULT LoadImageFromPath(LPCWSTR Path, GpImage** ppImage);

/* Toolbar image size */
#define TB_IMAGE_WIDTH  16
#define TB_IMAGE_HEIGHT 16

/* Slide show timer */
#define SLIDESHOW_TIMER_ID          0xFACE
#define SLIDESHOW_TIMER_INTERVAL    5000 /* 5 seconds */

HINSTANCE           g_hInstance         = NULL;
HWND                g_hMainWnd          = NULL;
HWND                g_hwndFullscreen    = NULL;
SHIMGVW_FILENODE *  g_pCurrentFile      = NULL;
GpImage *           g_pImage            = NULL;
SHIMGVW_SETTINGS    g_Settings;

static const UINT s_ZoomSteps[] =
{
    5, 10, 25, 50, 100, 200, 300, 500, 1000, 2000, 4000
};

#define MIN_ZOOM s_ZoomSteps[0]
#define MAX_ZOOM s_ZoomSteps[_countof(s_ZoomSteps) - 1]

    /* iBitmap,       idCommand,   fsState,         fsStyle,     bReserved[2], dwData, iString */
#define DEFINE_BTN_INFO(_name) \
    { TBICON_##_name, IDC_##_name, TBSTATE_ENABLED, BTNS_BUTTON, {0}, 0, 0 }

#define DEFINE_BTN_SEPARATOR \
    { -1,             0,           TBSTATE_ENABLED, BTNS_SEP,    {0}, 0, 0 }

/* ToolBar Buttons */
static const TBBUTTON s_Buttons[] =
{
    DEFINE_BTN_INFO(PREV_PIC),
    DEFINE_BTN_INFO(NEXT_PIC),
    DEFINE_BTN_SEPARATOR,
    DEFINE_BTN_INFO(BEST_FIT),
    DEFINE_BTN_INFO(REAL_SIZE),
    DEFINE_BTN_INFO(SLIDE_SHOW),
    DEFINE_BTN_SEPARATOR,
    DEFINE_BTN_INFO(ZOOM_IN),
    DEFINE_BTN_INFO(ZOOM_OUT),
    DEFINE_BTN_SEPARATOR,
    DEFINE_BTN_INFO(ROT_CLOCKW),
    DEFINE_BTN_INFO(ROT_COUNCW),
    DEFINE_BTN_SEPARATOR,
    DEFINE_BTN_INFO(ROT_CWSAVE),
    DEFINE_BTN_INFO(ROT_CCWSAVE),
    DEFINE_BTN_SEPARATOR,
    DEFINE_BTN_INFO(DELETE),
    DEFINE_BTN_INFO(PRINT),
    DEFINE_BTN_INFO(SAVEAS),
    DEFINE_BTN_INFO(MODIFY),
    DEFINE_BTN_SEPARATOR,
    DEFINE_BTN_INFO(HELP_TOC)
};

/* ToolBar Button configuration */
typedef struct
{
    DWORD idb;  /* Index to bitmap */
    DWORD ids;  /* Index to tooltip */
} TB_BUTTON_CONFIG;

#define DEFINE_BTN_CONFIG(_name) { IDB_##_name, IDS_TOOLTIP_##_name }

static const TB_BUTTON_CONFIG s_ButtonConfig[] =
{
    DEFINE_BTN_CONFIG(PREV_PIC),
    DEFINE_BTN_CONFIG(NEXT_PIC),
    DEFINE_BTN_CONFIG(BEST_FIT),
    DEFINE_BTN_CONFIG(REAL_SIZE),
    DEFINE_BTN_CONFIG(SLIDE_SHOW),
    DEFINE_BTN_CONFIG(ZOOM_IN),
    DEFINE_BTN_CONFIG(ZOOM_OUT),
    DEFINE_BTN_CONFIG(ROT_CLOCKW),
    DEFINE_BTN_CONFIG(ROT_COUNCW),
    DEFINE_BTN_CONFIG(ROT_CWSAVE),
    DEFINE_BTN_CONFIG(ROT_CCWSAVE),
    DEFINE_BTN_CONFIG(DELETE),
    DEFINE_BTN_CONFIG(PRINT),
    DEFINE_BTN_CONFIG(SAVEAS),
    DEFINE_BTN_CONFIG(MODIFY),
    DEFINE_BTN_CONFIG(HELP_TOC),
};

typedef struct tagPREVIEW_DATA
{
    HWND m_hwnd;
    HWND m_hwndZoom;
    HWND m_hwndToolBar;
    INT m_nZoomPercents;
    ANIME m_Anime; /* Animation */
    INT m_xScrollOffset;
    INT m_yScrollOffset;
    UINT m_nMouseDownMsg;
    POINT m_ptOrigin;
    WCHAR m_szFile[MAX_PATH];
} PREVIEW_DATA, *PPREVIEW_DATA;

static inline PPREVIEW_DATA
Preview_GetData(HWND hwnd)
{
    return (PPREVIEW_DATA)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
}

static inline BOOL
Preview_IsMainWnd(HWND hwnd)
{
    return hwnd == g_hMainWnd;
}

static VOID
Preview_RestartTimer(HWND hwnd)
{
    if (!Preview_IsMainWnd(hwnd))
    {
        KillTimer(hwnd, SLIDESHOW_TIMER_ID);
        SetTimer(hwnd, SLIDESHOW_TIMER_ID, SLIDESHOW_TIMER_INTERVAL, NULL);
    }
}

static VOID
ZoomWnd_UpdateScroll(PPREVIEW_DATA pData, HWND hwnd, BOOL bResetPos)
{
    RECT rcClient;
    UINT ImageWidth, ImageHeight, ZoomedWidth, ZoomedHeight;
    SCROLLINFO si;
    BOOL bShowHorz, bShowVert;

    if (bResetPos)
        pData->m_xScrollOffset = pData->m_yScrollOffset = 0;

    if (!g_pImage)
    {
        ShowScrollBar(hwnd, SB_BOTH, FALSE);
        InvalidateRect(hwnd, NULL, TRUE);
        return;
    }

    GdipGetImageWidth(g_pImage, &ImageWidth);
    GdipGetImageHeight(g_pImage, &ImageHeight);

    ZoomedWidth  = (ImageWidth  * pData->m_nZoomPercents) / 100;
    ZoomedHeight = (ImageHeight * pData->m_nZoomPercents) / 100;

    GetClientRect(hwnd, &rcClient);

    bShowHorz = (rcClient.right < ZoomedWidth);
    bShowVert = (rcClient.bottom < ZoomedHeight);
    ShowScrollBar(hwnd, SB_HORZ, bShowHorz);
    ShowScrollBar(hwnd, SB_VERT, bShowVert);

    GetClientRect(hwnd, &rcClient);

    ZeroMemory(&si, sizeof(si));
    si.cbSize = sizeof(si);
    si.fMask = SIF_ALL;

    if (bShowHorz)
    {
        GetScrollInfo(hwnd, SB_HORZ, &si);
        si.nPage = rcClient.right;
        si.nMin = 0;
        si.nMax = ZoomedWidth;
        si.nPos = (ZoomedWidth - rcClient.right) / 2 + pData->m_xScrollOffset;
        si.nPos = max(min(si.nPos, si.nMax - (INT)si.nPage), si.nMin);
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
        pData->m_xScrollOffset = si.nPos - (ZoomedWidth - rcClient.right) / 2;
    }
    else
    {
        pData->m_xScrollOffset = 0;
    }

    if (bShowVert)
    {
        GetScrollInfo(hwnd, SB_VERT, &si);
        si.nPage = rcClient.bottom;
        si.nMin = 0;
        si.nMax = ZoomedHeight;
        si.nPos = (ZoomedHeight - rcClient.bottom) / 2 + pData->m_yScrollOffset;
        si.nPos = max(min(si.nPos, si.nMax - (INT)si.nPage), si.nMin);
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
        pData->m_yScrollOffset = si.nPos - (ZoomedHeight - rcClient.bottom) / 2;
    }
    else
    {
        pData->m_yScrollOffset = 0;
    }

    InvalidateRect(hwnd, NULL, TRUE);
}

static VOID
Preview_UpdateZoom(PPREVIEW_DATA pData, UINT NewZoom, BOOL bEnableBestFit, BOOL bEnableRealSize)
{
    BOOL bEnableZoomIn, bEnableZoomOut;
    HWND hToolBar = pData->m_hwndToolBar;

    pData->m_nZoomPercents = NewZoom;

    /* Check if a zoom button of the toolbar must be grayed */
    bEnableZoomIn  = (NewZoom < MAX_ZOOM);
    bEnableZoomOut = (NewZoom > MIN_ZOOM);

    /* Update toolbar buttons */
    PostMessageW(hToolBar, TB_ENABLEBUTTON, IDC_ZOOM_OUT, bEnableZoomOut);
    PostMessageW(hToolBar, TB_ENABLEBUTTON, IDC_ZOOM_IN,  bEnableZoomIn);
    PostMessageW(hToolBar, TB_ENABLEBUTTON, IDC_BEST_FIT, bEnableBestFit);
    PostMessageW(hToolBar, TB_ENABLEBUTTON, IDC_REAL_SIZE, bEnableRealSize);

    /* Redraw the display window */
    InvalidateRect(pData->m_hwndZoom, NULL, TRUE);

    /* Restart timer if necessary */
    Preview_RestartTimer(pData->m_hwnd);

    /* Update scroll info */
    ZoomWnd_UpdateScroll(pData, pData->m_hwndZoom, FALSE);
}

static VOID
Preview_ZoomInOrOut(PPREVIEW_DATA pData, BOOL bZoomIn)
{
    UINT i, NewZoom;

    if (g_pImage == NULL)
        return;

    if (bZoomIn)    /* zoom in */
    {
        /* find next step */
        for (i = 0; i < _countof(s_ZoomSteps); ++i)
        {
            if (pData->m_nZoomPercents < s_ZoomSteps[i])
                break;
        }
        NewZoom = ((i >= _countof(s_ZoomSteps)) ? MAX_ZOOM : s_ZoomSteps[i]);
    }
    else            /* zoom out */
    {
        /* find previous step */
        for (i = _countof(s_ZoomSteps); i > 0; )
        {
            --i;
            if (s_ZoomSteps[i] < pData->m_nZoomPercents)
                break;
        }
        NewZoom = ((i < 0) ? MIN_ZOOM : s_ZoomSteps[i]);
    }

    /* Update toolbar and refresh screen */
    Preview_UpdateZoom(pData, NewZoom, TRUE, TRUE);
}

static VOID
Preview_ResetZoom(PPREVIEW_DATA pData)
{
    RECT Rect;
    UINT ImageWidth, ImageHeight, NewZoom;

    if (g_pImage == NULL)
        return;

    /* get disp window size and image size */
    GetClientRect(pData->m_hwndZoom, &Rect);
    GdipGetImageWidth(g_pImage, &ImageWidth);
    GdipGetImageHeight(g_pImage, &ImageHeight);

    /* compare two aspect rates. same as
       (ImageHeight / ImageWidth < Rect.bottom / Rect.right) in real */
    if (ImageHeight * Rect.right < Rect.bottom * ImageWidth)
    {
        if (Rect.right < ImageWidth)
        {
            /* it's large, shrink it */
            NewZoom = (Rect.right * 100) / ImageWidth;
        }
        else
        {
            /* it's small. show as original size */
            NewZoom = 100;
        }
    }
    else
    {
        if (Rect.bottom < ImageHeight)
        {
            /* it's large, shrink it */
            NewZoom = (Rect.bottom * 100) / ImageHeight;
        }
        else
        {
            /* it's small. show as original size */
            NewZoom = 100;
        }
    }

    Preview_UpdateZoom(pData, NewZoom, FALSE, TRUE);
}

static VOID
Preview_UpdateTitle(PPREVIEW_DATA pData, LPCWSTR FileName)
{
    WCHAR szText[MAX_PATH + 100];
    LPWSTR pchFileTitle;

    LoadStringW(g_hInstance, IDS_APPTITLE, szText, _countof(szText));

    pchFileTitle = PathFindFileNameW(FileName);
    if (pchFileTitle && *pchFileTitle)
    {
        StringCchCatW(szText, _countof(szText), L" - ");
        StringCchCatW(szText, _countof(szText), pchFileTitle);
    }

    SetWindowTextW(pData->m_hwnd, szText);
}

static VOID
Preview_pFreeImage(PPREVIEW_DATA pData)
{
    Anime_FreeInfo(&pData->m_Anime);

    if (g_pImage)
    {
        GdipDisposeImage(g_pImage);
        g_pImage = NULL;
    }

    pData->m_szFile[0] = UNICODE_NULL;
}

static VOID
Preview_pLoadImage(PPREVIEW_DATA pData, LPCWSTR szOpenFileName)
{
    HRESULT hr;
    Preview_pFreeImage(pData);
    InvalidateRect(pData->m_hwnd, NULL, FALSE); /* Schedule redraw in case we change to "No preview" */

    hr = LoadImageFromPath(szOpenFileName, &g_pImage);
    if (FAILED(hr))
    {
        DPRINT1("GdipLoadImageFromStream() failed, %d\n", hr);
        Preview_pFreeImage(pData);
        Preview_UpdateTitle(pData, NULL);
        return;
    }

    Anime_LoadInfo(&pData->m_Anime);

    GetFullPathNameW(szOpenFileName, _countof(pData->m_szFile), pData->m_szFile, NULL);
    SHAddToRecentDocs(SHARD_PATHW, pData->m_szFile);

    /* Reset zoom and redraw display */
    Preview_ResetZoom(pData);

    Preview_UpdateTitle(pData, szOpenFileName);
}

static VOID
Preview_pLoadImageFromNode(PPREVIEW_DATA pData, SHIMGVW_FILENODE *pNode)
{
    Preview_pLoadImage(pData, (pNode ? pNode->FileName : NULL));
}

static BOOL
Preview_pSaveImage(PPREVIEW_DATA pData, LPCWSTR pszFile)
{
    ImageCodecInfo *codecInfo;
    GUID rawFormat;
    UINT j, num, nFilterIndex, size;
    BOOL ret = FALSE;

    if (g_pImage == NULL)
        return FALSE;

    GdipGetImageEncodersSize(&num, &size);
    codecInfo = QuickAlloc(size, FALSE);
    if (!codecInfo)
    {
        DPRINT1("QuickAlloc() failed in pSaveImage()\n");
        return FALSE;
    }
    GdipGetImageEncoders(num, size, codecInfo);

    GdipGetImageRawFormat(g_pImage, &rawFormat);
    if (IsEqualGUID(&rawFormat, &ImageFormatMemoryBMP))
        rawFormat = ImageFormatBMP;

    nFilterIndex = 0;
    for (j = 0; j < num; ++j)
    {
        if (IsEqualGUID(&rawFormat, &codecInfo[j].FormatID))
        {
            nFilterIndex = j + 1;
            break;
        }
    }

    Anime_Pause(&pData->m_Anime);

    ret = (nFilterIndex > 0) &&
          (GdipSaveImageToFile(g_pImage, pszFile, &codecInfo[nFilterIndex - 1].Clsid, NULL) == Ok);
    if (!ret)
        DPRINT1("GdipSaveImageToFile() failed\n");

    Anime_Start(&pData->m_Anime, 0);

    QuickFree(codecInfo);
    return ret;
}

static VOID
Preview_pSaveImageAs(PPREVIEW_DATA pData)
{
    OPENFILENAMEW sfn;
    ImageCodecInfo *codecInfo;
    WCHAR szSaveFileName[MAX_PATH];
    WCHAR *szFilterMask;
    GUID rawFormat;
    UINT num, size, j;
    size_t sizeRemain;
    WCHAR *c;
    HWND hwnd = pData->m_hwnd;

    if (g_pImage == NULL)
        return;

    GdipGetImageEncodersSize(&num, &size);
    codecInfo = QuickAlloc(size, FALSE);
    if (!codecInfo)
    {
        DPRINT1("QuickAlloc() failed in pSaveImageAs()\n");
        return;
    }

    GdipGetImageEncoders(num, size, codecInfo);

    GdipGetImageRawFormat(g_pImage, &rawFormat);
    if (IsEqualGUID(&rawFormat, &ImageFormatMemoryBMP))
        rawFormat = ImageFormatBMP;

    sizeRemain = 0;
    for (j = 0; j < num; ++j)
    {
        // Every pair needs space for the Description, twice the Extensions, 1 char for the space, 2 for the braces and 2 for the NULL terminators.
        sizeRemain = sizeRemain + (((wcslen(codecInfo[j].FormatDescription) + (wcslen(codecInfo[j].FilenameExtension) * 2) + 5) * sizeof(WCHAR)));
    }

    /* Add two more chars for the last terminator */
    sizeRemain += (sizeof(WCHAR) * 2);

    szFilterMask = QuickAlloc(sizeRemain, FALSE);
    if (!szFilterMask)
    {
        DPRINT1("cannot allocate memory for filter mask in pSaveImageAs()");
        QuickFree(codecInfo);
        return;
    }

    ZeroMemory(szSaveFileName, sizeof(szSaveFileName));
    ZeroMemory(szFilterMask, sizeRemain);
    ZeroMemory(&sfn, sizeof(sfn));
    sfn.lStructSize = sizeof(sfn);
    sfn.hwndOwner   = hwnd;
    sfn.lpstrFile   = szSaveFileName;
    sfn.lpstrFilter = szFilterMask;
    sfn.nMaxFile    = _countof(szSaveFileName);
    sfn.Flags       = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY;
    sfn.lpstrDefExt = L"png";

    c = szFilterMask;

    for (j = 0; j < num; ++j)
    {
        StringCbPrintfExW(c, sizeRemain, &c, &sizeRemain, 0, L"%ls (%ls)", codecInfo[j].FormatDescription, codecInfo[j].FilenameExtension);

        /* Skip the NULL character */
        c++;
        sizeRemain -= sizeof(*c);

        StringCbPrintfExW(c, sizeRemain, &c, &sizeRemain, 0, L"%ls", codecInfo[j].FilenameExtension);

        /* Skip the NULL character */
        c++;
        sizeRemain -= sizeof(*c);

        if (IsEqualGUID(&rawFormat, &codecInfo[j].FormatID))
        {
            sfn.nFilterIndex = j + 1;
        }
    }

    if (GetSaveFileNameW(&sfn) && sfn.nFilterIndex > 0)
    {
        Anime_Pause(&pData->m_Anime);

        if (GdipSaveImageToFile(g_pImage, szSaveFileName, &codecInfo[sfn.nFilterIndex - 1].Clsid, NULL) != Ok)
        {
            DPRINT1("GdipSaveImageToFile() failed\n");
        }

        Anime_Start(&pData->m_Anime, 0);
    }

    QuickFree(szFilterMask);
    QuickFree(codecInfo);
}

static VOID
Preview_pPrintImage(PPREVIEW_DATA pData)
{
    /* FIXME */
}

static VOID
Preview_UpdateUI(PPREVIEW_DATA pData)
{
    BOOL bEnable = (g_pImage != NULL);
    PostMessageW(pData->m_hwndToolBar, TB_ENABLEBUTTON, IDC_SAVEAS, bEnable);
    PostMessageW(pData->m_hwndToolBar, TB_ENABLEBUTTON, IDC_PRINT, bEnable);
}

static VOID
Preview_UpdateImage(PPREVIEW_DATA pData)
{
    if (!Preview_IsMainWnd(pData->m_hwnd))
        Preview_ResetZoom(pData);

    ZoomWnd_UpdateScroll(pData, pData->m_hwndZoom, TRUE);
}

static SHIMGVW_FILENODE*
pBuildFileList(LPCWSTR szFirstFile)
{
    HANDLE hFindHandle;
    WCHAR *extension, *buffer;
    WCHAR szSearchPath[MAX_PATH];
    WCHAR szSearchMask[MAX_PATH];
    WCHAR szFileTypes[MAX_PATH];
    WIN32_FIND_DATAW findData;
    SHIMGVW_FILENODE *currentNode = NULL;
    SHIMGVW_FILENODE *root = NULL;
    SHIMGVW_FILENODE *conductor = NULL;
    ImageCodecInfo *codecInfo;
    UINT num = 0, size = 0, ExtraSize = 0;
    UINT j;

    const PCWSTR ExtraExtensions = GetExtraExtensionsGdipList();
    const UINT ExtraCount = ExtraExtensions[0] ? 1 : 0;
    if (ExtraCount)
        ExtraSize += sizeof(*codecInfo) + (wcslen(ExtraExtensions) + 1) * sizeof(WCHAR);

    StringCbCopyW(szSearchPath, sizeof(szSearchPath), szFirstFile);
    PathRemoveFileSpecW(szSearchPath);

    GdipGetImageDecodersSize(&num, &size);
    codecInfo = QuickAlloc(size + ExtraSize, FALSE);
    if (!codecInfo)
    {
        DPRINT1("QuickAlloc() failed in pLoadFileList()\n");
        return NULL;
    }

    GdipGetImageDecoders(num, size, codecInfo);
    buffer = (PWSTR)((UINT_PTR)codecInfo + size + (sizeof(*codecInfo) * ExtraCount));
    if (ExtraCount)
        codecInfo[num].FilenameExtension = wcscpy(buffer, ExtraExtensions);
    num += ExtraCount;

    root = QuickAlloc(sizeof(SHIMGVW_FILENODE), FALSE);
    if (!root)
    {
        DPRINT1("QuickAlloc() failed in pLoadFileList()\n");
        QuickFree(codecInfo);
        return NULL;
    }

    conductor = root;

    for (j = 0; j < num; ++j)
    {
        // FIXME: Parse each FilenameExtension list to bypass szFileTypes limit
        StringCbCopyW(szFileTypes, sizeof(szFileTypes), codecInfo[j].FilenameExtension);

        extension = wcstok(szFileTypes, L";");
        while (extension != NULL)
        {
            PathCombineW(szSearchMask, szSearchPath, extension);

            hFindHandle = FindFirstFileW(szSearchMask, &findData);
            if (hFindHandle != INVALID_HANDLE_VALUE)
            {
                do
                {
                    PathCombineW(conductor->FileName, szSearchPath, findData.cFileName);

                    // compare the name of the requested file with the one currently found.
                    // if the name matches, the current node is returned by the function.
                    if (_wcsicmp(szFirstFile, conductor->FileName) == 0)
                    {
                        currentNode = conductor;
                    }

                    conductor->Next = QuickAlloc(sizeof(SHIMGVW_FILENODE), FALSE);

                    // if QuickAlloc fails, make circular what we have and return it
                    if (!conductor->Next)
                    {
                        DPRINT1("QuickAlloc() failed in pLoadFileList()\n");

                        conductor->Next = root;
                        root->Prev = conductor;

                        FindClose(hFindHandle);
                        QuickFree(codecInfo);
                        return conductor;
                    }

                    conductor->Next->Prev = conductor;
                    conductor = conductor->Next;
                }
                while (FindNextFileW(hFindHandle, &findData) != 0);

                FindClose(hFindHandle);
            }

            extension = wcstok(NULL, L";");
        }
    }

    // we now have a node too much in the list. In case the requested file was not found,
    // we use this node to store the name of it, otherwise we free it.
    if (currentNode == NULL)
    {
        StringCchCopyW(conductor->FileName, MAX_PATH, szFirstFile);
        currentNode = conductor;
    }
    else
    {
        conductor = conductor->Prev;
        QuickFree(conductor->Next);
    }

    // link the last node with the first one to make the list circular
    conductor->Next = root;
    root->Prev = conductor;
    conductor = currentNode;

    QuickFree(codecInfo);

    return conductor;
}

static VOID
pFreeFileList(SHIMGVW_FILENODE *root)
{
    SHIMGVW_FILENODE *conductor;

    if (!root)
        return;

    root->Prev->Next = NULL;
    root->Prev = NULL;

    while (root)
    {
        conductor = root;
        root = conductor->Next;
        QuickFree(conductor);
    }
}

static HBRUSH CreateCheckerBoardBrush(VOID)
{
    static const CHAR pattern[] =
        "\x28\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x01\x00\x04\x00\x00\x00"
        "\x00\x00\x80\x00\x00\x00\x23\x2E\x00\x00\x23\x2E\x00\x00\x10\x00\x00\x00"
        "\x00\x00\x00\x00\x99\x99\x99\x00\xCC\xCC\xCC\x00\x00\x00\x00\x00\x00\x00"
        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11\x11\x11"
        "\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00"
        "\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00"
        "\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11"
        "\x11\x11\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00"
        "\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11"
        "\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11"
        "\x00\x00\x00\x00\x11\x11\x11\x11\x00\x00\x00\x00\x11\x11\x11\x11";

    return CreateDIBPatternBrushPt(pattern, DIB_RGB_COLORS);
}

static VOID
ZoomWnd_OnDraw(
    PPREVIEW_DATA pData,
    HDC hdc,
    LPRECT prcPaint,
    LPRECT prcClient)
{
    GpGraphics *graphics;
    INT ZoomedWidth, ZoomedHeight;
    RECT rect, rcClient = *prcClient;
    HDC hdcMem;
    HBRUSH hBrush;
    HPEN hPen;
    HGDIOBJ hbrOld, hbmOld, hPenOld;
    UINT uFlags;
    HBITMAP hbmMem;
    SIZE paintSize = { prcPaint->right - prcPaint->left, prcPaint->bottom - prcPaint->top };
    COLORREF color0, color1;
    GpImageAttributes *imageAttributes;

    /* We use a memory bitmap to reduce flickering */
    hdcMem = CreateCompatibleDC(hdc);
    hbmMem = CreateCompatibleBitmap(hdc, paintSize.cx, paintSize.cy);
    hbmOld = SelectObject(hdcMem, hbmMem);

    /* Choose colors */
    if (Preview_IsMainWnd(pData->m_hwnd))
    {
        color0 = GetSysColor(COLOR_WINDOW);
        color1 = GetSysColor(COLOR_WINDOWTEXT);
    }
    else
    {
        color0 = RGB(0, 0, 0);
        color1 = RGB(255, 255, 255);
    }

    hBrush = CreateSolidBrush(color0);
    SetBkColor(hdcMem, color0);

    hPen = CreatePen(PS_SOLID, 1, color1);
    SetTextColor(hdcMem, color1);

    /* Fill background */
    SetRect(&rect, 0, 0, paintSize.cx, paintSize.cy);
    FillRect(hdcMem, &rect, hBrush);

    DeleteObject(hBrush);

    if (g_pImage == NULL)
    {
        WCHAR szText[128];
        LoadStringW(g_hInstance, IDS_NOPREVIEW, szText, _countof(szText));

        SelectObject(hdcMem, GetStockFont(DEFAULT_GUI_FONT));
        OffsetRect(&rcClient, -prcPaint->left, -prcPaint->top);
        DrawTextW(hdcMem, szText, -1, &rcClient, DT_SINGLELINE | DT_CENTER | DT_VCENTER |
                                                 DT_NOPREFIX);
    }
    else
    {
        UINT ImageWidth, ImageHeight;

        GdipGetImageWidth(g_pImage, &ImageWidth);
        GdipGetImageHeight(g_pImage, &ImageHeight);

        ZoomedWidth  = (ImageWidth  * pData->m_nZoomPercents) / 100;
        ZoomedHeight = (ImageHeight * pData->m_nZoomPercents) / 100;

        GdipCreateFromHDC(hdcMem, &graphics);
        if (!graphics)
        {
            DPRINT1("error: GdipCreateFromHDC\n");
            return;
        }

        GdipGetImageFlags(g_pImage, &uFlags);

        if (pData->m_nZoomPercents % 100 == 0)
        {
            GdipSetInterpolationMode(graphics, InterpolationModeNearestNeighbor);
            GdipSetSmoothingMode(graphics, SmoothingModeNone);
        }
        else
        {
            GdipSetInterpolationMode(graphics, InterpolationModeHighQualityBilinear);
            GdipSetSmoothingMode(graphics, SmoothingModeHighQuality);
        }

        rect.left   = (rcClient.right  - ZoomedWidth ) / 2;
        rect.top    = (rcClient.bottom - ZoomedHeight) / 2;
        rect.right  = rect.left + ZoomedWidth;
        rect.bottom = rect.top  + ZoomedHeight;
        OffsetRect(&rect,
                   -prcPaint->left - pData->m_xScrollOffset,
                   -prcPaint->top  - pData->m_yScrollOffset);

        InflateRect(&rect, +1, +1); /* Add Rectangle() pen width */

        /* Draw a rectangle. Fill by checker board if necessary */
        if (uFlags & (ImageFlagsHasAlpha | ImageFlagsHasTranslucent))
            hbrOld = SelectObject(hdcMem, CreateCheckerBoardBrush());
        else
            hbrOld = SelectObject(hdcMem, GetStockBrush(NULL_BRUSH));
        hPenOld = SelectObject(hdcMem, hPen);
        Rectangle(hdcMem, rect.left, rect.top, rect.right, rect.bottom);
        DeleteObject(SelectObject(hdcMem, hbrOld));
        DeleteObject(SelectObject(hdcMem, hPenOld));

        InflateRect(&rect, -1, -1); /* Subtract Rectangle() pen width */

        /* Image attributes are required to draw image correctly */
        GdipCreateImageAttributes(&imageAttributes);
        GdipSetImageAttributesWrapMode(imageAttributes, WrapModeTile,
                                       GetBkColor(hdcMem) | 0xFF000000, TRUE);

        /* Draw image. -0.5f is used for interpolation */
        GdipDrawImageRectRect(graphics, g_pImage,
                              rect.left, rect.top,
                              rect.right - rect.left, rect.bottom - rect.top,
                              -0.5f, -0.5f, ImageWidth, ImageHeight,
                              UnitPixel, imageAttributes, NULL, NULL);

        GdipDisposeImageAttributes(imageAttributes);
        GdipDeleteGraphics(graphics);
    }

    BitBlt(hdc, prcPaint->left, prcPaint->top, paintSize.cx, paintSize.cy, hdcMem, 0, 0, SRCCOPY);
    DeleteObject(SelectObject(hdcMem, hbmOld));
    DeleteDC(hdcMem);
}

static VOID
ZoomWnd_OnPaint(PPREVIEW_DATA pData, HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hDC;
    RECT rcClient;

    hDC = BeginPaint(hwnd, &ps);
    if (hDC)
    {
        GetClientRect(hwnd, &rcClient);
        ZoomWnd_OnDraw(pData, hDC, &ps.rcPaint, &rcClient);
        EndPaint(hwnd, &ps);
    }
}

static VOID
ImageView_ResetSettings(VOID)
{
    g_Settings.Maximized = FALSE;
    g_Settings.X         = CW_USEDEFAULT;
    g_Settings.Y         = CW_USEDEFAULT;
    g_Settings.Width     = 520;
    g_Settings.Height    = 400;
}

static BOOL
ImageView_LoadSettings(VOID)
{
    HKEY hKey;
    DWORD dwSize;
    LSTATUS nError;

    nError = RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\ReactOS\\shimgvw", 0, KEY_READ, &hKey);
    if (nError != ERROR_SUCCESS)
        return FALSE;

    dwSize = sizeof(g_Settings);
    nError = RegQueryValueExW(hKey, L"Settings", NULL, NULL, (LPBYTE)&g_Settings, &dwSize);
    RegCloseKey(hKey);

    return ((nError == ERROR_SUCCESS) && (dwSize == sizeof(g_Settings)));
}

static VOID
ImageView_SaveSettings(VOID)
{
    HKEY hKey;
    LSTATUS nError;

    nError = RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\ReactOS\\shimgvw",
                             0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL);
    if (nError != ERROR_SUCCESS)
        return;

    RegSetValueExW(hKey, L"Settings", 0, REG_BINARY, (LPBYTE)&g_Settings, sizeof(g_Settings));
    RegCloseKey(hKey);
}

static BOOL
Preview_CreateToolBar(PPREVIEW_DATA pData)
{
    HWND hwndToolBar;
    HIMAGELIST hImageList, hOldImageList;
    DWORD style = WS_CHILD | WS_VISIBLE | TBSTYLE_FLAT | TBSTYLE_TOOLTIPS;

    if (!Preview_IsMainWnd(pData->m_hwnd))
        return TRUE; /* FIXME */

    style |= CCS_BOTTOM;
    hwndToolBar = CreateWindowExW(0, TOOLBARCLASSNAMEW, NULL, style,
                                  0, 0, 0, 0, pData->m_hwnd, NULL, g_hInstance, NULL);
    if (!hwndToolBar)
        return FALSE;

    pData->m_hwndToolBar = hwndToolBar;

    SendMessageW(hwndToolBar, TB_BUTTONSTRUCTSIZE, sizeof(s_Buttons[0]), 0);
    SendMessageW(hwndToolBar, TB_SETEXTENDEDSTYLE, 0, TBSTYLE_EX_HIDECLIPPEDBUTTONS);

    hImageList = ImageList_Create(TB_IMAGE_WIDTH, TB_IMAGE_HEIGHT, ILC_MASK | ILC_COLOR24, 1, 1);
    if (hImageList == NULL)
        return FALSE;

    for (UINT n = 0; n < _countof(s_ButtonConfig); n++)
    {
        HBITMAP hBitmap = LoadBitmapW(g_hInstance, MAKEINTRESOURCEW(s_ButtonConfig[n].idb));
        ImageList_AddMasked(hImageList, hBitmap, RGB(255, 255, 255));
        DeleteObject(hBitmap);
    }

    hOldImageList = (HIMAGELIST)SendMessageW(hwndToolBar, TB_SETIMAGELIST, 0, (LPARAM)hImageList);
    ImageList_Destroy(hOldImageList);

    SendMessageW(hwndToolBar, TB_ADDBUTTONS, _countof(s_Buttons), (LPARAM)s_Buttons);

    return TRUE;
}

static VOID
Preview_EndSlideShow(HWND hwnd)
{
    if (Preview_IsMainWnd(hwnd))
        return;

    KillTimer(hwnd, SLIDESHOW_TIMER_ID);
    ShowWindow(hwnd, SW_HIDE);
    ShowWindow(g_hMainWnd, SW_SHOWNORMAL);
    Preview_ResetZoom(Preview_GetData(g_hMainWnd));
}

static VOID
ZoomWnd_OnButtonDown(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    HWND hParent = GetParent(hwnd);
    if ((uMsg == WM_LBUTTONDOWN) || (uMsg == WM_RBUTTONDOWN))
    {
        if (!Preview_IsMainWnd(hParent))
            Preview_EndSlideShow(hParent);
        return;
    }

    pData->m_nMouseDownMsg = uMsg;
    pData->m_ptOrigin.x = GET_X_LPARAM(lParam);
    pData->m_ptOrigin.y = GET_Y_LPARAM(lParam);
    SetCapture(hwnd);
    SetCursor(LoadCursorW(g_hInstance, MAKEINTRESOURCEW(IDC_HANDDRAG)));
}

static VOID
ZoomWnd_OnMouseMove(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };

    if (pData->m_nMouseDownMsg == WM_MBUTTONDOWN)
    {
        INT x = GetScrollPos(hwnd, SB_HORZ) - (pt.x - pData->m_ptOrigin.x);
        INT y = GetScrollPos(hwnd, SB_VERT) - (pt.y - pData->m_ptOrigin.y);
        SendMessageW(hwnd, WM_HSCROLL, MAKEWPARAM(SB_THUMBPOSITION, x), 0);
        SendMessageW(hwnd, WM_VSCROLL, MAKEWPARAM(SB_THUMBPOSITION, y), 0);
        pData->m_ptOrigin = pt;
    }
}

static BOOL
ZoomWnd_OnSetCursor(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    if (pData->m_nMouseDownMsg == WM_MBUTTONDOWN)
    {
        SetCursor(LoadCursorW(g_hInstance, MAKEINTRESOURCEW(IDC_HANDDRAG)));
        return TRUE;
    }
    return FALSE;
}

static VOID
ZoomWnd_OnButtonUp(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    pData->m_nMouseDownMsg = 0;
    ReleaseCapture();
}

static VOID
ZoomWnd_OnHVScroll(PPREVIEW_DATA pData, HWND hwnd, WPARAM wParam, BOOL bVertical)
{
    UINT ImageWidth, ImageHeight, ZoomedWidth, ZoomedHeight;
    RECT rcClient;
    UINT nBar = (bVertical ? SB_VERT : SB_HORZ);
    SCROLLINFO si = { sizeof(si), SIF_ALL };
    GetScrollInfo(hwnd, nBar, &si);

    if (!g_pImage)
        return;

    if (bVertical)
    {
        if (!(GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_VSCROLL))
            return;
    }
    else
    {
        if (!(GetWindowLongPtrW(hwnd, GWL_STYLE) & WS_HSCROLL))
            return;
    }

    switch (LOWORD(wParam))
    {
        case SB_THUMBTRACK:
        case SB_THUMBPOSITION:
            si.nPos = (SHORT)HIWORD(wParam);
            break;
        case SB_LINELEFT:
            si.nPos -= 48;
            break;
        case SB_LINERIGHT:
            si.nPos += 48;
            break;
        case SB_PAGELEFT:
            si.nPos -= si.nPage;
            break;
        case SB_PAGERIGHT:
            si.nPos += si.nPage;
            break;
    }

    si.fMask = SIF_POS;
    SetScrollInfo(hwnd, nBar, &si, TRUE);
    GetScrollInfo(hwnd, nBar, &si);

    GetClientRect(hwnd, &rcClient);

    if (bVertical)
    {
        GdipGetImageHeight(g_pImage, &ImageHeight);
        ZoomedHeight = (ImageHeight * pData->m_nZoomPercents) / 100;
        pData->m_yScrollOffset = si.nPos - (ZoomedHeight - rcClient.bottom) / 2;
    }
    else
    {
        GdipGetImageWidth(g_pImage, &ImageWidth);
        ZoomedWidth = (ImageWidth  * pData->m_nZoomPercents) / 100;
        pData->m_xScrollOffset = si.nPos - (ZoomedWidth - rcClient.right) / 2;
    }

    InvalidateRect(hwnd, NULL, TRUE);
}

static VOID
ZoomWnd_OnMouseWheel(HWND hwnd, INT x, INT y, INT zDelta, UINT fwKeys)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    if (zDelta == 0)
        return;

    if (GetKeyState(VK_CONTROL) < 0)
    {
        Preview_ZoomInOrOut(pData, zDelta > 0);
    }
    else if (GetKeyState(VK_SHIFT) < 0)
    {
        if (zDelta > 0)
            SendMessageW(hwnd, WM_HSCROLL, SB_LINELEFT, 0);
        else
            SendMessageW(hwnd, WM_HSCROLL, SB_LINERIGHT, 0);
    }
    else
    {
        if (zDelta > 0)
            SendMessageW(hwnd, WM_VSCROLL, SB_LINEUP, 0);
        else
            SendMessageW(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
    }
}

LRESULT CALLBACK
ZoomWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    switch (uMsg)
    {
        case WM_LBUTTONDOWN:
        case WM_MBUTTONDOWN:
        case WM_RBUTTONDOWN:
        {
            ZoomWnd_OnButtonDown(hwnd, uMsg, wParam, lParam);
            break;
        }
        case WM_MOUSEMOVE:
        {
            ZoomWnd_OnMouseMove(hwnd, uMsg, wParam, lParam);
            break;
        }
        case WM_SETCURSOR:
        {
            if (!ZoomWnd_OnSetCursor(hwnd, uMsg, wParam, lParam))
                return DefWindowProcW(hwnd, uMsg, wParam, lParam);
        }
        case WM_LBUTTONUP:
        case WM_MBUTTONUP:
        case WM_RBUTTONUP:
        {
            ZoomWnd_OnButtonUp(hwnd, uMsg, wParam, lParam);
            break;
        }
        case WM_PAINT:
        {
            ZoomWnd_OnPaint(pData, hwnd);
            break;
        }
        case WM_MOUSEWHEEL:
        {
            ZoomWnd_OnMouseWheel(hwnd, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam),
                                 (SHORT)HIWORD(wParam), (UINT)LOWORD(wParam));
            break;
        }
        case WM_HSCROLL:
        case WM_VSCROLL:
            ZoomWnd_OnHVScroll(pData, hwnd, wParam, uMsg == WM_VSCROLL);
            break;
        case WM_TIMER:
        {
            if (Anime_OnTimer(&pData->m_Anime, wParam))
                InvalidateRect(hwnd, NULL, FALSE);
            break;
        }
        default:
        {
            return DefWindowProcW(hwnd, uMsg, wParam, lParam);
        }
    }
    return 0;
}

static BOOL
Preview_OnCreate(HWND hwnd, LPCREATESTRUCT pCS)
{
    DWORD exstyle = 0;
    HWND hwndZoom;
    PPREVIEW_DATA pData = QuickAlloc(sizeof(PREVIEW_DATA), TRUE);
    pData->m_hwnd = hwnd;
    SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)pData);

    DragAcceptFiles(hwnd, TRUE);

    if (g_hMainWnd == NULL)
    {
        g_hMainWnd = hwnd;
        exstyle |= WS_EX_CLIENTEDGE;
    }
    else if (g_hwndFullscreen == NULL)
    {
        g_hwndFullscreen = hwnd;
    }
    else
    {
        return FALSE;
    }

    hwndZoom = CreateWindowExW(exstyle, WC_ZOOM, NULL, WS_CHILD | WS_VISIBLE,
                               0, 0, 0, 0, hwnd, NULL, g_hInstance, NULL);
    if (!hwndZoom)
    {
        QuickFree(pData);
        return FALSE;
    }

    pData->m_hwndZoom = hwndZoom;
    SetWindowLongPtrW(hwndZoom, GWLP_USERDATA, (LONG_PTR)pData);
    Anime_SetTimerWnd(&pData->m_Anime, pData->m_hwndZoom);

    if (!Preview_CreateToolBar(pData))
    {
        QuickFree(pData);
        return FALSE;
    }

    if (pCS && pCS->lpCreateParams)
    {
        LPCWSTR pszFileName = (LPCWSTR)pCS->lpCreateParams;
        WCHAR szFile[MAX_PATH];

        /* Make sure the path has no quotes on it */
        StringCchCopyW(szFile, _countof(szFile), pszFileName);
        PathUnquoteSpacesW(szFile);

        g_pCurrentFile = pBuildFileList(szFile);
        Preview_pLoadImageFromNode(pData, g_pCurrentFile);
        Preview_UpdateImage(pData);
        Preview_UpdateUI(pData);
    }

    return TRUE;
}

static VOID
Preview_OnMoveSize(HWND hwnd)
{
    WINDOWPLACEMENT wp;
    RECT *prc;

    if (IsIconic(hwnd) || !Preview_IsMainWnd(hwnd))
        return;

    wp.length = sizeof(WINDOWPLACEMENT);
    GetWindowPlacement(hwnd, &wp);

    /* Remember window position and size */
    prc = &wp.rcNormalPosition;
    g_Settings.X = prc->left;
    g_Settings.Y = prc->top;
    g_Settings.Width = prc->right - prc->left;
    g_Settings.Height = prc->bottom - prc->top;
    g_Settings.Maximized = IsZoomed(hwnd);
}

static VOID
Preview_OnSize(HWND hwnd)
{
    RECT rc, rcClient;
    PPREVIEW_DATA pData = Preview_GetData(hwnd);
    HWND hToolBar = pData->m_hwndToolBar;
    INT cx, cy;

    /* We want 32-bit values. Don't use WM_SIZE lParam */
    GetClientRect(hwnd, &rcClient);
    cx = rcClient.right;
    cy = rcClient.bottom;

    if (Preview_IsMainWnd(pData->m_hwnd))
    {
        SendMessageW(hToolBar, TB_AUTOSIZE, 0, 0);
        GetWindowRect(hToolBar, &rc);

        MoveWindow(pData->m_hwndZoom, 0, 0, cx, cy - (rc.bottom - rc.top), TRUE);

        if (!IsIconic(hwnd)) /* Is it not minimized? */
            Preview_ResetZoom(pData);

        Preview_OnMoveSize(hwnd);
    }
    else
    {
        MoveWindow(pData->m_hwndZoom, 0, 0, cx, cy, TRUE);
    }
}

static VOID
Preview_Delete(PPREVIEW_DATA pData)
{
    WCHAR szCurFile[MAX_PATH + 1], szNextFile[MAX_PATH];
    HWND hwnd = pData->m_hwnd;
    SHFILEOPSTRUCTW FileOp = { hwnd, FO_DELETE };

    if (!pData->m_szFile[0])
        return;

    /* FileOp.pFrom must be double-null-terminated */
    GetFullPathNameW(pData->m_szFile, _countof(szCurFile) - 1, szCurFile, NULL);
    szCurFile[_countof(szCurFile) - 2] = UNICODE_NULL; /* Avoid buffer overrun */
    szCurFile[lstrlenW(szCurFile) + 1] = UNICODE_NULL;

    szNextFile[0] = UNICODE_NULL;
    if (g_pCurrentFile)
    {
        GetFullPathNameW(g_pCurrentFile->Next->FileName, _countof(szNextFile), szNextFile, NULL);
        szNextFile[_countof(szNextFile) - 1] = UNICODE_NULL; /* Avoid buffer overrun */
    }

    /* Confirm file deletion and delete if allowed */
    FileOp.pFrom = szCurFile;
    FileOp.fFlags = FOF_ALLOWUNDO;
    if (SHFileOperationW(&FileOp) != 0)
    {
        DPRINT("Preview_Delete: SHFileOperationW() failed or canceled\n");
        return;
    }

    /* Reload the file list and go next file */
    pFreeFileList(g_pCurrentFile);
    g_pCurrentFile = pBuildFileList(szNextFile);
    Preview_pLoadImageFromNode(pData, g_pCurrentFile);
}

static VOID
Preview_Edit(HWND hwnd)
{
    SHELLEXECUTEINFOW sei;
    PPREVIEW_DATA pData = Preview_GetData(hwnd);

    if (!pData->m_szFile[0])
        return;

    ZeroMemory(&sei, sizeof(sei));
    sei.cbSize = sizeof(sei);
    sei.lpVerb = L"edit";
    sei.lpFile = pData->m_szFile;
    sei.nShow = SW_SHOWNORMAL;
    if (!ShellExecuteExW(&sei))
    {
        DPRINT1("Preview_Edit: ShellExecuteExW() failed with code %ld\n", GetLastError());
    }
    else
    {
        // Destroy the window to quit the application
        DestroyWindow(hwnd);
    }
}

static VOID
Preview_ToggleSlideShow(PPREVIEW_DATA pData)
{
    if (!IsWindow(g_hwndFullscreen))
    {
        DWORD style = WS_POPUP | WS_CLIPSIBLINGS, exstyle = WS_EX_TOPMOST;
        WCHAR szTitle[256];
        LoadStringW(g_hInstance, IDS_APPTITLE, szTitle, _countof(szTitle));
        g_hwndFullscreen = CreateWindowExW(exstyle, WC_PREVIEW, szTitle, style,
                                           0, 0, 0, 0, NULL, NULL, g_hInstance, NULL);
    }

    if (IsWindowVisible(g_hwndFullscreen))
    {
        ShowWindow(g_hwndFullscreen, SW_HIDE);
        ShowWindow(g_hMainWnd, SW_SHOWNORMAL);
        KillTimer(g_hwndFullscreen, SLIDESHOW_TIMER_ID);
    }
    else
    {
        ShowWindow(g_hMainWnd, SW_HIDE);
        ShowWindow(g_hwndFullscreen, SW_SHOWMAXIMIZED);
        Preview_RestartTimer(g_hwndFullscreen);
    }
}

static VOID
Preview_GoNextPic(PPREVIEW_DATA pData, BOOL bNext)
{
    Preview_RestartTimer(pData->m_hwnd);
    if (g_pCurrentFile)
    {
        if (bNext)
            g_pCurrentFile = g_pCurrentFile->Next;
        else
            g_pCurrentFile = g_pCurrentFile->Prev;
        Preview_pLoadImageFromNode(pData, g_pCurrentFile);
        Preview_UpdateImage(pData);
        Preview_UpdateUI(pData);
    }
}

static VOID
Preview_OnCommand(HWND hwnd, UINT nCommandID)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);

    switch (nCommandID)
    {
        case IDC_PREV_PIC:
            Preview_GoNextPic(pData, FALSE);
            break;

        case IDC_NEXT_PIC:
            Preview_GoNextPic(pData, TRUE);
            break;

        case IDC_BEST_FIT:
            Preview_ResetZoom(pData);
            break;

        case IDC_REAL_SIZE:
            Preview_UpdateZoom(pData, 100, TRUE, FALSE);
            break;

        case IDC_SLIDE_SHOW:
            Preview_ToggleSlideShow(pData);
            break;

        case IDC_ZOOM_IN:
            Preview_ZoomInOrOut(pData, TRUE);
            break;

        case IDC_ZOOM_OUT:
            Preview_ZoomInOrOut(pData, FALSE);
            break;

        case IDC_ENDSLIDESHOW:
            Preview_EndSlideShow(hwnd);
            break;

        default:
            break;
    }

    if (!Preview_IsMainWnd(hwnd))
        return;

    // The following commands are for main window only:
    switch (nCommandID)
    {
        case IDC_SAVEAS:
            Preview_pSaveImageAs(pData);
            break;

        case IDC_PRINT:
            Preview_pPrintImage(pData);
            break;

        case IDC_ROT_CLOCKW:
            if (g_pImage)
            {
                GdipImageRotateFlip(g_pImage, Rotate270FlipNone);
                Preview_UpdateImage(pData);
            }
            break;

        case IDC_ROT_COUNCW:
            if (g_pImage)
            {
                GdipImageRotateFlip(g_pImage, Rotate90FlipNone);
                Preview_UpdateImage(pData);
            }
            break;

        case IDC_ROT_CWSAVE:
            if (g_pImage)
            {
                GdipImageRotateFlip(g_pImage, Rotate270FlipNone);
                Preview_pSaveImage(pData, pData->m_szFile);
                Preview_UpdateImage(pData);
            }
            break;

        case IDC_ROT_CCWSAVE:
            if (g_pImage)
            {
                GdipImageRotateFlip(g_pImage, Rotate90FlipNone);
                Preview_pSaveImage(pData, pData->m_szFile);
                Preview_UpdateImage(pData);
            }
            break;

        case IDC_DELETE:
            Preview_Delete(pData);
            Preview_UpdateImage(pData);
            Preview_UpdateUI(pData);
            break;

        case IDC_MODIFY:
            Preview_Edit(hwnd);
            break;

        default:
            break;
    }
}

static LRESULT
Preview_OnNotify(HWND hwnd, LPNMHDR pnmhdr)
{
    switch (pnmhdr->code)
    {
        case TTN_GETDISPINFOW:
        {
            LPTOOLTIPTEXTW lpttt = (LPTOOLTIPTEXTW)pnmhdr;
            lpttt->hinst = g_hInstance;
            lpttt->lpszText = MAKEINTRESOURCEW(s_ButtonConfig[lpttt->hdr.idFrom - IDC_TOOL_BASE].ids);
            break;
        }
    }
    return 0;
}

static VOID
Preview_OnDestroy(HWND hwnd)
{
    PPREVIEW_DATA pData = Preview_GetData(hwnd);

    KillTimer(hwnd, SLIDESHOW_TIMER_ID);

    pFreeFileList(g_pCurrentFile);
    g_pCurrentFile = NULL;

    Preview_pFreeImage(pData);

    SetWindowLongPtrW(pData->m_hwndZoom, GWLP_USERDATA, 0);
    DestroyWindow(pData->m_hwndZoom);
    pData->m_hwndZoom = NULL;

    DestroyWindow(pData->m_hwndToolBar);
    pData->m_hwndToolBar = NULL;

    SetWindowLongPtrW(pData->m_hwnd, GWLP_USERDATA, 0);
    QuickFree(pData);

    PostQuitMessage(0);
}

static VOID
Preview_OnDropFiles(HWND hwnd, HDROP hDrop)
{
    WCHAR szFile[MAX_PATH];
    PPREVIEW_DATA pData = Preview_GetData(hwnd);

    DragQueryFileW(hDrop, 0, szFile, _countof(szFile));

    pFreeFileList(g_pCurrentFile);
    g_pCurrentFile = pBuildFileList(szFile);
    Preview_pLoadImageFromNode(pData, g_pCurrentFile);

    DragFinish(hDrop);
}

LRESULT CALLBACK
PreviewWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_CREATE:
        {
            if (!Preview_OnCreate(hwnd, (LPCREATESTRUCT)lParam))
                return -1;
            break;
        }
        case WM_COMMAND:
        {
            Preview_OnCommand(hwnd, LOWORD(wParam));
            break;
        }
        case WM_NOTIFY:
        {
            return Preview_OnNotify(hwnd, (LPNMHDR)lParam);
        }
        case WM_GETMINMAXINFO:
        {
            MINMAXINFO *pMMI = (MINMAXINFO*)lParam;
            pMMI->ptMinTrackSize.x = 350;
            pMMI->ptMinTrackSize.y = 290;
            break;
        }
        case WM_MOVE:
        {
            Preview_OnMoveSize(hwnd);
            break;
        }
        case WM_SIZE:
        {
            Preview_OnSize(hwnd);
            break;
        }
        case WM_DROPFILES:
        {
            Preview_OnDropFiles(hwnd, (HDROP)wParam);
            break;
        }
        case WM_SYSCOLORCHANGE:
        {
            PPREVIEW_DATA pData = Preview_GetData(hwnd);
            InvalidateRect(pData->m_hwnd, NULL, TRUE);
            InvalidateRect(pData->m_hwndZoom, NULL, TRUE);
            break;
        }
        case WM_DESTROY:
        {
            Preview_OnDestroy(hwnd);
            break;
        }
        case WM_TIMER:
        {
            if (wParam == SLIDESHOW_TIMER_ID)
            {
                PPREVIEW_DATA pData = Preview_GetData(hwnd);
                Preview_GoNextPic(pData, TRUE);
            }
            break;
        }
        default:
        {
            return DefWindowProcW(hwnd, uMsg, wParam, lParam);
        }
    }

    return 0;
}

LONG
ImageView_Main(HWND hwnd, LPCWSTR szFileName)
{
    struct GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    WNDCLASSW WndClass;
    WCHAR szTitle[256];
    HWND hMainWnd;
    MSG msg;
    HACCEL hAccel;
    HRESULT hrCoInit;
    INITCOMMONCONTROLSEX Icc = { .dwSize = sizeof(Icc), .dwICC = ICC_WIN95_CLASSES };

    InitCommonControlsEx(&Icc);

    /* Initialize COM */
    hrCoInit = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    if (FAILED(hrCoInit))
        DPRINT1("Warning, CoInitializeEx failed with code=%08X\n", (int)hrCoInit);

    if (!ImageView_LoadSettings())
        ImageView_ResetSettings();

    /* Initialize GDI+ */
    ZeroMemory(&gdiplusStartupInput, sizeof(gdiplusStartupInput));
    gdiplusStartupInput.GdiplusVersion = 1;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    /* Register window classes */
    ZeroMemory(&WndClass, sizeof(WndClass));
    WndClass.lpszClassName  = WC_PREVIEW;
    WndClass.lpfnWndProc    = PreviewWndProc;
    WndClass.hInstance      = g_hInstance;
    WndClass.style          = CS_HREDRAW | CS_VREDRAW;
    WndClass.hIcon          = LoadIconW(g_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON));
    WndClass.hCursor        = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
    WndClass.hbrBackground  = (HBRUSH)UlongToHandle(COLOR_3DFACE + 1);
    if (!RegisterClassW(&WndClass))
        return -1;
    WndClass.lpszClassName  = WC_ZOOM;
    WndClass.lpfnWndProc    = ZoomWndProc;
    WndClass.style          = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    WndClass.hbrBackground  = GetStockBrush(NULL_BRUSH); /* less flicker */
    if (!RegisterClassW(&WndClass))
        return -1;

    /* Create the main window */
    LoadStringW(g_hInstance, IDS_APPTITLE, szTitle, _countof(szTitle));
    hMainWnd = CreateWindowExW(WS_EX_WINDOWEDGE, WC_PREVIEW, szTitle,
                               WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS,
                               g_Settings.X, g_Settings.Y, g_Settings.Width, g_Settings.Height,
                               NULL, NULL, g_hInstance, (LPVOID)szFileName);

    /* Create accelerator table for keystrokes */
    hAccel = LoadAcceleratorsW(g_hInstance, MAKEINTRESOURCEW(IDR_ACCELERATOR));

    /* Show the main window now */
    if (g_Settings.Maximized)
        ShowWindow(hMainWnd, SW_SHOWMAXIMIZED);
    else
        ShowWindow(hMainWnd, SW_SHOWNORMAL);

    UpdateWindow(hMainWnd);

    /* Message Loop */
    while (GetMessageW(&msg, NULL, 0, 0) > 0)
    {
        if (g_hwndFullscreen && TranslateAcceleratorW(g_hwndFullscreen, hAccel, &msg))
            continue;
        if (TranslateAcceleratorW(hMainWnd, hAccel, &msg))
            continue;

        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    /* Destroy accelerator table */
    DestroyAcceleratorTable(hAccel);

    ImageView_SaveSettings();

    GdiplusShutdown(gdiplusToken);

    /* Release COM resources */
    if (SUCCEEDED(hrCoInit))
        CoUninitialize();

    return 0;
}

VOID WINAPI
ImageView_FullscreenW(HWND hwnd, HINSTANCE hInst, LPCWSTR path, int nShow)
{
    ImageView_Main(hwnd, path);
}

VOID WINAPI
ImageView_Fullscreen(HWND hwnd, HINSTANCE hInst, LPCWSTR path, int nShow)
{
    ImageView_Main(hwnd, path);
}

VOID WINAPI
ImageView_FullscreenA(HWND hwnd, HINSTANCE hInst, LPCSTR path, int nShow)
{
    WCHAR szFile[MAX_PATH];

    if (MultiByteToWideChar(CP_ACP, 0, path, -1, szFile, _countof(szFile)))
    {
        ImageView_Main(hwnd, szFile);
    }
}

VOID WINAPI
ImageView_PrintTo(HWND hwnd, HINSTANCE hInst, LPCWSTR path, int nShow)
{
    DPRINT("ImageView_PrintTo() not implemented\n");
}

VOID WINAPI
ImageView_PrintToA(HWND hwnd, HINSTANCE hInst, LPCSTR path, int nShow)
{
    DPRINT("ImageView_PrintToA() not implemented\n");
}

VOID WINAPI
ImageView_PrintToW(HWND hwnd, HINSTANCE hInst, LPCWSTR path, int nShow)
{
    DPRINT("ImageView_PrintToW() not implemented\n");
}

BOOL WINAPI
DllMain(IN HINSTANCE hinstDLL,
        IN DWORD dwReason,
        IN LPVOID lpvReserved)
{
    switch (dwReason)
    {
        case DLL_PROCESS_ATTACH:
            g_hInstance = hinstDLL;
            break;
    }

    return TRUE;
}
