///////////////////////////////////////////////////////////////////////////////
// Name:        src/msw/bmpbndl.cpp
// Purpose:     MSW-specific part of wxBitmapBundle class.
// Author:      Vadim Zeitlin
// Created:     2021-09-22
// Copyright:   (c) 2021 Vadim Zeitlin <vadim@wxwidgets.org>
// Licence:     wxWindows licence
///////////////////////////////////////////////////////////////////////////////

// ============================================================================
// declarations
// ============================================================================

// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------

// for compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"

#ifndef WX_PRECOMP
    #include "wx/log.h"

    #include "wx/msw/wrapwin.h"
#endif // WX_PRECOMP

#include "wx/bmpbndl.h"

#include "wx/private/bmpbndl.h"

#include <algorithm>

// ----------------------------------------------------------------------------
// private helpers
// ----------------------------------------------------------------------------

namespace
{

// Helper function, which will probably need to be factored out as it's likely
// to be useful elsewhere, returning the scale determined by the given suffix,
// i.e. it returns 2 for the suffix of "_2x" or 1.5 for "_1_5x".
//
// Returns 1 if the suffix is empty or 0 if it doesn't look like a valid scale
// suffix at all.
double GetScaleFromSuffix(const wxString& suffix)
{
    if ( suffix.empty() )
        return 1.0;

    if ( *suffix.begin() != '_' )
        return 0;

    if ( *suffix.rbegin() != 'x' )
        return 0;

    wxString factor = suffix.substr(1, suffix.length() - 2);
    if ( factor.empty() )
        return 0;

    factor.Replace(wxS("_"), wxS("."));

    double d;
    if ( !factor.ToCDouble(&d) )
        return 0;

    return d;
}

// Just a bitmap name and the corresponding scale for some resource.
struct ResourceInfo
{
    ResourceInfo(const wxString& name_, double scale_)
        : name(name_), scale(scale_)
    {
    }

    wxString name;
    double scale;
};

// Predicate for std::sort() comparing resource infos using their scale.
struct ScaleComparator
{
    bool operator()(const ResourceInfo& info1, const ResourceInfo& info2) const
    {
        return info1.scale < info2.scale;
    }
};

typedef wxVector<ResourceInfo> ResourceInfos;

// Used to pass information to EnumRCBitmaps() callback.
struct RCEnumCallbackData
{
    explicit RCEnumCallbackData(const wxString& name_)
        : name(name_.Lower())
    {
    }

    // The base name of all resources, passed to the callback.
    wxString name;

    // All the existing bitmaps starting with this name and using a valid scale
    // suffix, filled by the callback.
    ResourceInfos resourceInfos;
};

BOOL CALLBACK EnumRCBitmaps(HMODULE WXUNUSED(hModule),
                            LPCTSTR WXUNUSED(lpszType),
                            LPTSTR lpszName,
                            LONG_PTR lParam)
{
    wxString name(lpszName);
    name.MakeLower(); // resource names are case insensitive

    RCEnumCallbackData *data = reinterpret_cast<RCEnumCallbackData*>(lParam);

    wxString suffix;
    if ( name.StartsWith(data->name, &suffix) )
    {
        const double scale = GetScaleFromSuffix(suffix);
        if ( scale )
            data->resourceInfos.push_back(ResourceInfo(name, scale));
    }

    return TRUE; // continue enumeration
}

// Bundle implementation using PNG bitmaps from Windows resources.
class wxBitmapBundleImplRC : public wxBitmapBundleImpl
{
public:
    // Ctor takes the vector containing all the existing resources starting
    // with the given name and the valid bitmap corresponding to the base name
    // resource itself.
    wxBitmapBundleImplRC(const ResourceInfos& resourceInfos,
                         const wxBitmap& bitmap);

    virtual wxSize GetDefaultSize() const wxOVERRIDE;
    virtual wxSize GetPreferredSizeAtScale(double scale) const wxOVERRIDE;
    virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE;

private:
    // Load the bitmap from the given resource and add it m_bitmaps, after
    // rescaling it to the given size if necessary, i.e. if the needed size is
    // different from the size of this bitmap itself.
    //
    // The expected size is used to confirm that the resource really has the
    // size we expect it to have.
    //
    // Return the just loaded bitmap.
    wxBitmap
    AddBitmap(const ResourceInfo& info,
              const wxSize& sizeExpected,
              const wxSize& sizeNeeded);


    // All the available resources.
    const ResourceInfos m_resourceInfos;

    // All already loaded bitmaps sorted by their size.
    //
    // This vector is never empty and its first element is the bitmap using the
    // default size.
    wxVector<wxBitmap> m_bitmaps;

    wxDECLARE_NO_COPY_CLASS(wxBitmapBundleImplRC);
};

} // anonymous namespace

// ============================================================================
// wxBitmapBundleImplRC implementation
// ============================================================================

wxBitmapBundleImplRC::wxBitmapBundleImplRC(const ResourceInfos& resourceInfos,
                                           const wxBitmap& bitmap)
    : m_resourceInfos(resourceInfos)
{
    m_bitmaps.push_back(bitmap);
}

wxSize wxBitmapBundleImplRC::GetDefaultSize() const
{
    return m_bitmaps[0].GetSize();
}

wxSize wxBitmapBundleImplRC::GetPreferredSizeAtScale(double scale) const
{
    // Optimistically assume we're going to use this exact scale by default.
    double scalePreferred = scale;

    for ( size_t i = 0; ; ++i )
    {
        if ( i == m_resourceInfos.size() )
        {
            // The requested scale is bigger than anything we have, so use the
            // biggest available one.
            scalePreferred = m_resourceInfos[i - 1].scale;
            break;
        }

        const double scaleThis = m_resourceInfos[i].scale;

        // Keep looking for the exact match which we still can hope to find
        // while the current scale is smaller.
        if ( scaleThis < scale )
            continue;

        // If we've found the exact match, just use it.
        if ( scaleThis == scale )
            break;

        // We've found the closest bigger scale.

        // If there is no smaller one, we have no choice but to use this one.
        if ( i == 0 )
            break;

        // Decide whether we should use this one or the previous smaller one
        // depending on which of them is closer to the target scale, breaking
        // the tie in favour of the bigger one.
        const double scaleLast = m_resourceInfos[i - 1].scale;

        scalePreferred = scaleThis - scale <= scale - scaleLast
                            ? scaleThis
                            : scaleLast;
        break;
    }

    return GetDefaultSize()*scalePreferred;
}

wxBitmap
wxBitmapBundleImplRC::AddBitmap(const ResourceInfo& info,
                                const wxSize& sizeExpected,
                                const wxSize& sizeNeeded)
{
    // First load the bitmap, it shouldn't fail, but we can't do much if it
    // does fail -- we're just going to return an invalid bitmap in this case.
    wxBitmap bitmap(info.name, wxBITMAP_TYPE_PNG_RESOURCE);

    // We rely on suffixes really corresponding to the bitmap sizes and bad
    // things will happen if they don't.
    wxASSERT_MSG
    (
        bitmap.GetSize() == sizeExpected,
        wxString::Format(wxS("Bitmap \"%s\" should have size %d*%d."),
                         sizeExpected.x, sizeExpected.y)
    );

    if ( sizeNeeded != sizeExpected )
        wxBitmap::Rescale(bitmap, sizeNeeded);


    // Now cache the bitmap for future use by inserting it into our sorted
    // vector of bitmaps. We don't bother with the binary search here because
    // the vector is typically very small (< 10 elements).
    const wxSize sizeThis = bitmap.GetSize();
    for ( size_t i = 0; ; ++i )
    {
        if ( i == m_bitmaps.size() )
        {
            // This is the biggest bitmap we have, put it at the end.
            m_bitmaps.push_back(bitmap);
            break;
        }

        if ( m_bitmaps[i].GetSize().y > sizeThis.y )
        {
            m_bitmaps.insert(m_bitmaps.begin() + i, bitmap);
            break;
        }
    }

    return bitmap;
}

wxBitmap wxBitmapBundleImplRC::GetBitmap(const wxSize& size)
{
    // First check if we already have the bitmap of this size: we're only
    // interested in the exact match here.
    for ( size_t i = 0; i < m_bitmaps.size(); ++i )
    {
        const wxBitmap& bitmap = m_bitmaps[i];

        if ( bitmap.GetSize() == size )
            return bitmap;
    }

    // If we don't have one, create it using one of the available resources.
    // Here we try to find the exact match (ideal) or the next larger size
    // (downscaling results in better appearance than upscaling, generally
    // speaking).
    const wxSize sizeDef = GetDefaultSize();
    for ( size_t i = 0; i < m_resourceInfos.size(); ++i )
    {
        const ResourceInfo& info = m_resourceInfos[i];

        const wxSize sizeThis = sizeDef*info.scale;

        // Use this bitmap if it's the first one bigger than the requested size
        // or if it's the last item as in this case we're not going to find any
        // bitmap bigger than the given one anyhow and we don't have any choice
        // but to upscale the largest one we have.
        if ( sizeThis.y >= size.y || i + 1 == m_resourceInfos.size() )
            return AddBitmap(info, sizeThis, size);
    }

    wxFAIL_MSG( wxS("unreachable") );

    return wxBitmap();
}

// ============================================================================
// wxBitmapBundle::FromResources() implementation
// ============================================================================

/* static */
wxBitmapBundle wxBitmapBundle::FromResources(const wxString& name)
{
    // First of all, find all resources starting with this name.
    RCEnumCallbackData data(name);

    if ( !::EnumResourceNames(NULL, // this HMODULE
                              RT_RCDATA,
                              EnumRCBitmaps,
                              reinterpret_cast<LONG_PTR>(&data)) )
    {
        const DWORD err = ::GetLastError();
        if ( err != NO_ERROR && err != ERROR_RESOURCE_TYPE_NOT_FOUND )
        {
            wxLogSysError(wxS("Couldn't enumerate RCDATA resources"));
        }
    }

    ResourceInfos& resourceInfos = data.resourceInfos;

    if ( resourceInfos.empty() )
        return wxBitmapBundle();

    // We need to load the base bitmap to get the default size anyhow, so do it
    // now and also check that it works.
    wxBitmap bitmap(name, wxBITMAP_TYPE_PNG_RESOURCE);
    if ( !bitmap.IsOk() )
        return wxBitmapBundle();

    // Sort the resources in the order of increasing sizes to simplify the code
    // of wxBitmapBundleImplRC::GetBitmap().
    std::sort(resourceInfos.begin(), resourceInfos.end(), ScaleComparator());

    return wxBitmapBundle(new wxBitmapBundleImplRC(resourceInfos, bitmap));
}
