# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# Copyright (c) 2008-2022 pyglet contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of pyglet nor the names of its
#    contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------

from pyglet.com import pIUnknown
from pyglet.image import *
from pyglet.image.codecs import *
from pyglet.libs.win32.constants import *
from pyglet.libs.win32.types import *
from pyglet.libs.win32 import _kernel32 as kernel32
from pyglet.libs.win32 import _ole32 as ole32


gdiplus = windll.gdiplus

REAL = c_float

PixelFormat1bppIndexed    = 196865
PixelFormat4bppIndexed    = 197634
PixelFormat8bppIndexed    = 198659
PixelFormat16bppGrayScale = 1052676
PixelFormat16bppRGB555    = 135173
PixelFormat16bppRGB565    = 135174
PixelFormat16bppARGB1555  = 397319
PixelFormat24bppRGB       = 137224
PixelFormat32bppRGB       = 139273
PixelFormat32bppARGB      = 2498570
PixelFormat32bppPARGB     = 925707
PixelFormat48bppRGB       = 1060876
PixelFormat64bppARGB      = 3424269
PixelFormat64bppPARGB     = 29622286
PixelFormatMax            = 15

ImageLockModeRead = 1
ImageLockModeWrite = 2
ImageLockModeUserInputBuf = 4

PropertyTagFrameDelay = 0x5100


class GdiplusStartupInput(Structure):
    _fields_ = [
        ('GdiplusVersion', c_uint32),
        ('DebugEventCallback', c_void_p),
        ('SuppressBackgroundThread', BOOL),
        ('SuppressExternalCodecs', BOOL)
    ]


class GdiplusStartupOutput(Structure):
    _fields = [
        ('NotificationHookProc', c_void_p),
        ('NotificationUnhookProc', c_void_p)
    ]


class BitmapData(Structure):
    _fields_ = [
        ('Width', c_uint),
        ('Height', c_uint),
        ('Stride', c_int),
        ('PixelFormat', c_int),
        ('Scan0', POINTER(c_byte)),
        ('Reserved', POINTER(c_uint))
    ]


class Rect(Structure):
    _fields_ = [
        ('X', c_int),
        ('Y', c_int),
        ('Width', c_int),
        ('Height', c_int)
    ]


class PropertyItem(Structure):
    _fields_ = [
        ('id', c_uint),
        ('length', c_ulong),
        ('type', c_short),
        ('value', c_void_p)
    ]


INT_PTR = POINTER(INT)
UINT_PTR = POINTER(UINT)


gdiplus.GdipBitmapLockBits.restype = c_int
gdiplus.GdipBitmapLockBits.argtypes = [c_void_p, c_void_p, UINT, c_int, c_void_p]
gdiplus.GdipBitmapUnlockBits.restype = c_int
gdiplus.GdipBitmapUnlockBits.argtypes = [c_void_p, c_void_p]
gdiplus.GdipCloneStringFormat.restype = c_int
gdiplus.GdipCloneStringFormat.argtypes = [c_void_p, c_void_p]
gdiplus.GdipCreateBitmapFromScan0.restype = c_int
gdiplus.GdipCreateBitmapFromScan0.argtypes = [c_int, c_int, c_int, c_int, POINTER(BYTE), c_void_p]
gdiplus.GdipCreateBitmapFromStream.restype = c_int
gdiplus.GdipCreateBitmapFromStream.argtypes = [c_void_p, c_void_p]
gdiplus.GdipCreateFont.restype = c_int
gdiplus.GdipCreateFont.argtypes = [c_void_p, REAL, INT, c_int, c_void_p]
gdiplus.GdipCreateFontFamilyFromName.restype = c_int
gdiplus.GdipCreateFontFamilyFromName.argtypes = [c_wchar_p, c_void_p, c_void_p]
gdiplus.GdipCreateMatrix.restype = None
gdiplus.GdipCreateMatrix.argtypes = [c_void_p]
gdiplus.GdipCreateSolidFill.restype = c_int
gdiplus.GdipCreateSolidFill.argtypes = [c_int, c_void_p]  # ARGB
gdiplus.GdipDisposeImage.restype = c_int
gdiplus.GdipDisposeImage.argtypes = [c_void_p]
gdiplus.GdipDrawString.restype = c_int
gdiplus.GdipDrawString.argtypes = [c_void_p, c_wchar_p, c_int, c_void_p, c_void_p, c_void_p, c_void_p]
gdiplus.GdipGetFamilyName.restype = c_int
gdiplus.GdipGetFamilyName.argtypes = [LONG_PTR, c_wchar_p, c_wchar]
gdiplus.GdipFlush.restype = c_int
gdiplus.GdipFlush.argtypes = [c_void_p, c_int]
gdiplus.GdipGetFontCollectionFamilyCount.restype = c_int
gdiplus.GdipGetFontCollectionFamilyCount.argtypes = [c_void_p, INT_PTR]
gdiplus.GdipGetFontCollectionFamilyList.restype = c_int
gdiplus.GdipGetFontCollectionFamilyList.argtypes = [c_void_p, INT, c_void_p, INT_PTR]
gdiplus.GdipGetImageDimension.restype = c_int
gdiplus.GdipGetImageDimension.argtypes = [c_void_p, POINTER(REAL), POINTER(REAL)]
gdiplus.GdipGetImageGraphicsContext.restype = c_int
gdiplus.GdipGetImageGraphicsContext.argtypes = [c_void_p, c_void_p]
gdiplus.GdipGetImagePixelFormat.restype = c_int
gdiplus.GdipGetImagePixelFormat.argtypes = [c_void_p, c_void_p]
gdiplus.GdipGetPropertyItem.restype = c_int
gdiplus.GdipGetPropertyItem.argtypes = [c_void_p, c_uint, c_uint, c_void_p]
gdiplus.GdipGetPropertyItemSize.restype = c_int
gdiplus.GdipGetPropertyItemSize.argtypes = [c_void_p, c_uint, UINT_PTR]
gdiplus.GdipGraphicsClear.restype = c_int
gdiplus.GdipGraphicsClear.argtypes = [c_void_p, c_int]  # ARGB
gdiplus.GdipImageGetFrameCount.restype = c_int
gdiplus.GdipImageGetFrameCount.argtypes = [c_void_p, c_void_p, UINT_PTR]
gdiplus.GdipImageGetFrameDimensionsCount.restype = c_int
gdiplus.GdipImageGetFrameDimensionsCount.argtypes = [c_void_p, UINT_PTR]
gdiplus.GdipImageGetFrameDimensionsList.restype = c_int
gdiplus.GdipImageGetFrameDimensionsList.argtypes = [c_void_p, c_void_p, UINT]
gdiplus.GdipImageSelectActiveFrame.restype = c_int
gdiplus.GdipImageSelectActiveFrame.argtypes = [c_void_p, c_void_p, UINT]
gdiplus.GdipMeasureString.restype = c_int
gdiplus.GdipMeasureString.argtypes = [c_void_p, c_wchar_p, c_int, c_void_p, c_void_p, c_void_p, c_void_p, INT_PTR, INT_PTR]
gdiplus.GdipNewPrivateFontCollection.restype = c_int
gdiplus.GdipNewPrivateFontCollection.argtypes = [c_void_p]
gdiplus.GdipPrivateAddMemoryFont.restype = c_int
gdiplus.GdipPrivateAddMemoryFont.argtypes = [c_void_p, c_void_p, c_int]
gdiplus.GdipSetPageUnit.restype = c_int
gdiplus.GdipSetPageUnit.argtypes = [c_void_p, c_int]
gdiplus.GdipSetStringFormatFlags.restype = c_int
gdiplus.GdipSetStringFormatFlags.argtypes = [c_void_p, c_int]
gdiplus.GdipSetTextRenderingHint.restype = c_int
gdiplus.GdipSetTextRenderingHint.argtypes = [c_void_p, c_int]
gdiplus.GdipStringFormatGetGenericTypographic.restype = c_int
gdiplus.GdipStringFormatGetGenericTypographic.argtypes = [c_void_p]
gdiplus.GdiplusShutdown.restype = None
gdiplus.GdiplusShutdown.argtypes = [POINTER(ULONG)]
gdiplus.GdiplusStartup.restype = c_int
gdiplus.GdiplusStartup.argtypes = [c_void_p, c_void_p, c_void_p]


class GDIPlusDecoder(ImageDecoder):
    def get_file_extensions(self):
        return ['.bmp', '.gif', '.jpg', '.jpeg', '.exif', '.png', '.tif', '.tiff']

    def get_animation_file_extensions(self):
        # TIFF also supported as a multi-page image; but that's not really an
        # animation, is it?
        return ['.gif']

    def _load_bitmap(self, filename, file):
        data = file.read()

        # Create a HGLOBAL with image data
        hglob = kernel32.GlobalAlloc(GMEM_MOVEABLE, len(data))
        ptr = kernel32.GlobalLock(hglob)
        memmove(ptr, data, len(data))
        kernel32.GlobalUnlock(hglob)

        # Create IStream for the HGLOBAL
        self.stream = pIUnknown()
        ole32.CreateStreamOnHGlobal(hglob, True, byref(self.stream))

        # Load image from stream
        bitmap = c_void_p()
        status = gdiplus.GdipCreateBitmapFromStream(self.stream, byref(bitmap))
        if status != 0:
            self.stream.Release()
            raise ImageDecodeException('GDI+ cannot load %r' % (filename or file))

        return bitmap

    @staticmethod
    def _get_image(bitmap):
        # Get size of image (Bitmap subclasses Image)
        width = REAL()
        height = REAL()
        gdiplus.GdipGetImageDimension(bitmap, byref(width), byref(height))
        width = int(width.value)
        height = int(height.value)

        # Get image pixel format
        pf = c_int()
        gdiplus.GdipGetImagePixelFormat(bitmap, byref(pf))
        pf = pf.value

        # Reverse from what's documented because of Intel little-endianness.
        fmt = 'BGRA'
        if pf == PixelFormat24bppRGB:
            fmt = 'BGR'
        elif pf == PixelFormat32bppRGB:
            pass
        elif pf == PixelFormat32bppARGB:
            pass
        elif pf in (PixelFormat16bppARGB1555, PixelFormat32bppPARGB,
                    PixelFormat64bppARGB, PixelFormat64bppPARGB):
            pf = PixelFormat32bppARGB
        else:
            fmt = 'BGR'
            pf = PixelFormat24bppRGB

        # Lock pixel data in best format
        rect = Rect()
        rect.X = 0
        rect.Y = 0
        rect.Width = width
        rect.Height = height
        bitmap_data = BitmapData()
        gdiplus.GdipBitmapLockBits(bitmap, byref(rect), ImageLockModeRead, pf, byref(bitmap_data))
        
        # Create buffer for RawImage
        buffer = create_string_buffer(bitmap_data.Stride * height)
        memmove(buffer, bitmap_data.Scan0, len(buffer))
        
        # Unlock data
        gdiplus.GdipBitmapUnlockBits(bitmap, byref(bitmap_data))

        return ImageData(width, height, fmt, buffer, -bitmap_data.Stride)

    def _delete_bitmap(self, bitmap):
        # Release image and stream
        gdiplus.GdipDisposeImage(bitmap)
        self.stream.Release()

    def decode(self, filename, file):
        if not file:
            file = open(filename, 'rb')
        bitmap = self._load_bitmap(filename, file)
        image = self._get_image(bitmap)
        self._delete_bitmap(bitmap)
        return image

    def decode_animation(self, filename, file):
        if not file:
            file = open(filename, 'rb')
        bitmap = self._load_bitmap(filename, file)
        
        dimension_count = c_uint()
        gdiplus.GdipImageGetFrameDimensionsCount(bitmap, byref(dimension_count))
        if dimension_count.value < 1:
            self._delete_bitmap(bitmap)
            raise ImageDecodeException('Image has no frame dimensions')
        
        # XXX Make sure this dimension is time?
        dimensions = (c_void_p * dimension_count.value)()
        gdiplus.GdipImageGetFrameDimensionsList(bitmap, dimensions, dimension_count.value)

        frame_count = c_uint()
        gdiplus.GdipImageGetFrameCount(bitmap, dimensions, byref(frame_count))

        prop_id = PropertyTagFrameDelay
        prop_size = c_uint()
        gdiplus.GdipGetPropertyItemSize(bitmap, prop_id, byref(prop_size))

        prop_buffer = c_buffer(prop_size.value)
        prop_item = cast(prop_buffer, POINTER(PropertyItem)).contents 
        gdiplus.GdipGetPropertyItem(bitmap, prop_id, prop_size.value, prop_buffer)

        n_delays = prop_item.length // sizeof(c_long)
        delays = cast(prop_item.value, POINTER(c_long * n_delays)).contents

        frames = []
        
        for i in range(frame_count.value):
            gdiplus.GdipImageSelectActiveFrame(bitmap, dimensions, i)
            image = self._get_image(bitmap)

            delay = delays[i]
            if delay <= 1:
                delay = 10
            frames.append(AnimationFrame(image, delay/100.))

        self._delete_bitmap(bitmap)

        return Animation(frames)


def get_decoders():
    return [GDIPlusDecoder()]


def get_encoders():
    return []


def init():
    token = c_ulong()
    startup_in = GdiplusStartupInput()
    startup_in.GdiplusVersion = 1
    startup_out = GdiplusStartupOutput()
    gdiplus.GdiplusStartup(byref(token), byref(startup_in), byref(startup_out))

    # Shutdown later?
    # gdiplus.GdiplusShutdown(token)


init()
