/*
 * Copyright 2014 Michael Müller
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "wined3d_private.h"

#include <wine/library.h>

WINE_DEFAULT_DEBUG_CHANNEL(d3d);

static void* txc_dxtn_handle;
static void (*pfetch_2d_texel_rgba_dxt1)(int srcRowStride, const BYTE *pixData, int i, int j, DWORD *texel);
static void (*pfetch_2d_texel_rgba_dxt3)(int srcRowStride, const BYTE *pixData, int i, int j, DWORD *texel);
static void (*pfetch_2d_texel_rgba_dxt5)(int srcRowStride, const BYTE *pixData, int i, int j, DWORD *texel);
static void (*ptx_compress_dxtn)(int comps, int width, int height, const BYTE *srcPixData,
                                 GLenum destformat, BYTE *dest, int dstRowStride);

static inline BOOL dxt1_to_x8r8g8b8(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, BOOL alpha)
{
    unsigned int x, y;
    DWORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    for (y = 0; y < h; ++y)
    {
        DWORD *dst_line = (DWORD *)(dst + y * pitch_out);
        for (x = 0; x < w; ++x)
        {
            /* pfetch_2d_texel_rgba_dxt1 doesn't correctly handle pitch */
            pfetch_2d_texel_rgba_dxt1(0, src + (y / 4) * pitch_in + (x / 4) * 8,
                                      x & 3, y & 3, &color);
            if (alpha)
            {
                dst_line[x] = (color & 0xff00ff00) | ((color & 0xff) << 16) |
                              ((color & 0xff0000) >> 16);
            }
            else
            {
                dst_line[x] = 0xff000000 | ((color & 0xff) << 16) |
                              (color & 0xff00) | ((color & 0xff0000) >> 16);
            }
        }
    }

    return TRUE;
}

static inline BOOL dxt1_to_x4r4g4b4(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, BOOL alpha)
{
    unsigned int x, y;
    DWORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    for (y = 0; y < h; ++y)
    {
        WORD *dst_line = (WORD *)(dst + y * pitch_out);
        for (x = 0; x < w; ++x)
        {
            /* pfetch_2d_texel_rgba_dxt1 doesn't correctly handle pitch */
            pfetch_2d_texel_rgba_dxt1(0, src + (y / 4) * pitch_in + (x / 4) * 16,
                                      x & 3, y & 3, &color);
            if (alpha)
            {
                dst_line[x] = ((color & 0xf0000000) >> 16) | ((color & 0xf00000) >> 20) |
                              ((color & 0xf000) >> 8) | ((color & 0xf0) << 4);
            }
            else
            {
                dst_line[x] = 0xf000  | ((color & 0xf00000) >> 20) |
                              ((color & 0xf000) >> 8) | ((color & 0xf0) << 4);
            }
        }
    }

    return TRUE;
}

static inline BOOL dxt1_to_x1r5g5b5(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, BOOL alpha)
{
    unsigned int x, y;
    DWORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    for (y = 0; y < h; ++y)
    {
        WORD *dst_line = (WORD *)(dst + y * pitch_out);
        for (x = 0; x < w; ++x)
        {
            /* pfetch_2d_texel_rgba_dxt1 doesn't correctly handle pitch */
            pfetch_2d_texel_rgba_dxt1(0, src + (y / 4) * pitch_in + (x / 4) * 16,
                                      x & 3, y & 3, &color);
            if (alpha)
            {
                dst_line[x] = ((color & 0x80000000) >> 16) | ((color & 0xf80000) >> 19) |
                              ((color & 0xf800) >> 6) | ((color & 0xf8) << 7);
            }
            else
            {
                dst_line[x] = 0x8000 | ((color & 0xf80000) >> 19) |
                              ((color & 0xf800) >> 6) | ((color & 0xf8) << 7);
            }
        }
    }

    return TRUE;
}

static inline BOOL dxt3_to_x8r8g8b8(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, BOOL alpha)
{
    unsigned int x, y;
    DWORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    for (y = 0; y < h; ++y)
    {
        DWORD *dst_line = (DWORD *)(dst + y * pitch_out);
        for (x = 0; x < w; ++x)
        {
            /* pfetch_2d_texel_rgba_dxt3 doesn't correctly handle pitch */
            pfetch_2d_texel_rgba_dxt3(0, src + (y / 4) * pitch_in + (x / 4) * 16,
                                      x & 3, y & 3, &color);
            if (alpha)
            {
                dst_line[x] = (color & 0xff00ff00) | ((color & 0xff) << 16) |
                              ((color & 0xff0000) >> 16);
            }
            else
            {
                dst_line[x] = 0xff000000 | ((color & 0xff) << 16) |
                              (color & 0xff00) | ((color & 0xff0000) >> 16);
            }
        }
    }

    return TRUE;
}

static inline BOOL dxt3_to_x4r4g4b4(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, BOOL alpha)
{
    unsigned int x, y;
    DWORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    for (y = 0; y < h; ++y)
    {
        WORD *dst_line = (WORD *)(dst + y * pitch_out);
        for (x = 0; x < w; ++x)
        {
            /* pfetch_2d_texel_rgba_dxt3 doesn't correctly handle pitch */
            pfetch_2d_texel_rgba_dxt3(0, src + (y / 4) * pitch_in + (x / 4) * 16,
                                      x & 3, y & 3, &color);
            if (alpha)
            {
                dst_line[x] = ((color & 0xf0000000) >> 16) | ((color & 0xf00000) >> 20) |
                              ((color & 0xf000) >> 8) | ((color & 0xf0) << 4);
            }
            else
            {
                dst_line[x] = 0xf000  | ((color & 0xf00000) >> 20) |
                              ((color & 0xf000) >> 8) | ((color & 0xf0) << 4);
            }
        }
    }

    return TRUE;
}

static inline BOOL dxt5_to_x8r8g8b8(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, BOOL alpha)
{
    unsigned int x, y;
    DWORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    for (y = 0; y < h; ++y)
    {
        DWORD *dst_line = (DWORD *)(dst + y * pitch_out);
        for (x = 0; x < w; ++x)
        {
            /* pfetch_2d_texel_rgba_dxt5 doesn't correctly handle pitch */
            pfetch_2d_texel_rgba_dxt5(0, src + (y / 4) * pitch_in + (x / 4) * 16,
                                      x & 3, y & 3, &color);
            if (alpha)
            {
                dst_line[x] = (color & 0xff00ff00) | ((color & 0xff) << 16) |
                              ((color & 0xff0000) >> 16);
            }
            else
            {
                dst_line[x] = 0xff000000 | ((color & 0xff) << 16) |
                              (color & 0xff00) | ((color & 0xff0000) >> 16);
            }
        }
    }

    return TRUE;
}

static inline BOOL x8r8g8b8_to_dxtn(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, GLenum destformat, BOOL alpha)
{
    unsigned int x, y;
    DWORD color, *tmp;

    TRACE("Converting %ux%u pixels, pitches %u %u\n", w, h, pitch_in, pitch_out);

    tmp = HeapAlloc(GetProcessHeap(), 0, h * w * sizeof(DWORD));
    if (!tmp)
    {
        ERR("Failed to allocate memory for conversion\n");
        return FALSE;
    }

    for (y = 0; y < h; ++y)
    {
        const DWORD *src_line = (const DWORD *)(src + y * pitch_in);
        DWORD *dst_line = tmp + y * w;
        for (x = 0; x < w; ++x)
        {
            color = src_line[x];
            if (alpha)
            {
                dst_line[x] = (color & 0xff00ff00) | ((color & 0xff) << 16) |
                              ((color & 0xff0000) >> 16);
            }
            else
            {
                dst_line[x] = 0xff000000 | ((color & 0xff) << 16) |
                              (color & 0xff00) | ((color & 0xff0000) >> 16);
            }
        }
    }

    ptx_compress_dxtn(4, w, h, (BYTE *)tmp, destformat, dst, pitch_out);
    HeapFree(GetProcessHeap(), 0, tmp);
    return TRUE;
}

static inline BOOL x1r5g5b5_to_dxtn(const BYTE *src, BYTE *dst, DWORD pitch_in,
        DWORD pitch_out, unsigned int w, unsigned int h, GLenum destformat, BOOL alpha)
{
    static const unsigned char convert_5to8[] =
    {
        0x00, 0x08, 0x10, 0x19, 0x21, 0x29, 0x31, 0x3a,
        0x42, 0x4a, 0x52, 0x5a, 0x63, 0x6b, 0x73, 0x7b,
        0x84, 0x8c, 0x94, 0x9c, 0xa5, 0xad, 0xb5, 0xbd,
        0xc5, 0xce, 0xd6, 0xde, 0xe6, 0xef, 0xf7, 0xff,
    };
    unsigned int x, y;
    DWORD *tmp;
    WORD color;

    TRACE("Converting %ux%u pixels, pitches %u %u.\n", w, h, pitch_in, pitch_out);

    tmp = HeapAlloc(GetProcessHeap(), 0, h * w * sizeof(DWORD));
    if (!tmp)
    {
        ERR("Failed to allocate memory for conversion\n");
        return FALSE;
    }

    for (y = 0; y < h; ++y)
    {
        const WORD *src_line = (const WORD *)(src + y * pitch_in);
        DWORD *dst_line = tmp + y * w;
        for (x = 0; x < w; ++x)
        {
            color = src_line[x];
            if (alpha)
            {
                dst_line[x] = ((color & 0x8000) ? 0xff000000 : 0) |
                              convert_5to8[(color & 0x001f)] << 16 |
                              convert_5to8[(color & 0x03e0) >> 5] << 8 |
                              convert_5to8[(color & 0x7c00) >> 10];
            }
            else
            {
                dst_line[x] = 0xff000000 |
                              convert_5to8[(color & 0x001f)] << 16 |
                              convert_5to8[(color & 0x03e0) >> 5] << 8 |
                              convert_5to8[(color & 0x7c00) >> 10];
            }
        }
    }

    ptx_compress_dxtn(4, w, h, (BYTE *)tmp, destformat, dst, pitch_out);
    HeapFree(GetProcessHeap(), 0, tmp);
    return TRUE;
}

BOOL wined3d_dxt1_decode(const BYTE *src, BYTE *dst, DWORD pitch_in, DWORD pitch_out,
        enum wined3d_format_id format, unsigned int w, unsigned int h)
{
    if (!txc_dxtn_handle)
        return FALSE;

    switch (format)
    {
        case WINED3DFMT_B8G8R8A8_UNORM:
            return dxt1_to_x8r8g8b8(src, dst, pitch_in, pitch_out, w, h, TRUE);
        case WINED3DFMT_B8G8R8X8_UNORM:
            return dxt1_to_x8r8g8b8(src, dst, pitch_in, pitch_out, w, h, FALSE);
        case WINED3DFMT_B4G4R4A4_UNORM:
            return dxt1_to_x4r4g4b4(src, dst, pitch_in, pitch_out, w, h, TRUE);
        case WINED3DFMT_B4G4R4X4_UNORM:
            return dxt1_to_x4r4g4b4(src, dst, pitch_in, pitch_out, w, h, FALSE);
        case WINED3DFMT_B5G5R5A1_UNORM:
            return dxt1_to_x1r5g5b5(src, dst, pitch_in, pitch_out, w, h, TRUE);
        case WINED3DFMT_B5G5R5X1_UNORM:
            return dxt1_to_x1r5g5b5(src, dst, pitch_in, pitch_out, w, h, FALSE);
        default:
            break;
    }

    FIXME("Cannot find a conversion function from format DXT1 to %s.\n", debug_d3dformat(format));
    return FALSE;
}

BOOL wined3d_dxt3_decode(const BYTE *src, BYTE *dst, DWORD pitch_in, DWORD pitch_out,
        enum wined3d_format_id format, unsigned int w, unsigned int h)
{
    if (!txc_dxtn_handle)
        return FALSE;

    switch (format)
    {
        case WINED3DFMT_B8G8R8A8_UNORM:
            return dxt3_to_x8r8g8b8(src, dst, pitch_in, pitch_out, w, h, TRUE);
        case WINED3DFMT_B8G8R8X8_UNORM:
            return dxt3_to_x8r8g8b8(src, dst, pitch_in, pitch_out, w, h, FALSE);
        case WINED3DFMT_B4G4R4A4_UNORM:
            return dxt3_to_x4r4g4b4(src, dst, pitch_in, pitch_out, w, h, TRUE);
        case WINED3DFMT_B4G4R4X4_UNORM:
            return dxt3_to_x4r4g4b4(src, dst, pitch_in, pitch_out, w, h, FALSE);
        default:
            break;
    }

    FIXME("Cannot find a conversion function from format DXT3 to %s.\n", debug_d3dformat(format));
    return FALSE;
}

BOOL wined3d_dxt5_decode(const BYTE *src, BYTE *dst, DWORD pitch_in, DWORD pitch_out,
        enum wined3d_format_id format, unsigned int w, unsigned int h)
{
    if (!txc_dxtn_handle)
        return FALSE;

    switch (format)
    {
        case WINED3DFMT_B8G8R8A8_UNORM:
            return dxt5_to_x8r8g8b8(src, dst, pitch_in, pitch_out, w, h, TRUE);
        case WINED3DFMT_B8G8R8X8_UNORM:
            return dxt5_to_x8r8g8b8(src, dst, pitch_in, pitch_out, w, h, FALSE);
        default:
            break;
    }

    FIXME("Cannot find a conversion function from format DXT5 to %s.\n", debug_d3dformat(format));
    return FALSE;
}

BOOL wined3d_dxt1_encode(const BYTE *src, BYTE *dst, DWORD pitch_in, DWORD pitch_out,
        enum wined3d_format_id format, unsigned int w, unsigned int h)
{
    if (!txc_dxtn_handle)
        return FALSE;

    switch (format)
    {
        case WINED3DFMT_B8G8R8A8_UNORM:
            return x8r8g8b8_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, TRUE);
        case WINED3DFMT_B8G8R8X8_UNORM:
            return x8r8g8b8_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGB_S3TC_DXT1_EXT, FALSE);
        case WINED3DFMT_B5G5R5A1_UNORM:
            return x1r5g5b5_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, TRUE);
        case WINED3DFMT_B5G5R5X1_UNORM:
            return x1r5g5b5_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGB_S3TC_DXT1_EXT, FALSE);
        default:
            break;
    }

    FIXME("Cannot find a conversion function from format %s to DXT1.\n", debug_d3dformat(format));
    return FALSE;
}

BOOL wined3d_dxt3_encode(const BYTE *src, BYTE *dst, DWORD pitch_in, DWORD pitch_out,
        enum wined3d_format_id format, unsigned int w, unsigned int h)
{
    if (!txc_dxtn_handle)
        return FALSE;

    switch (format)
    {
        case WINED3DFMT_B8G8R8A8_UNORM:
            return x8r8g8b8_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, TRUE);
        case WINED3DFMT_B8G8R8X8_UNORM:
            return x8r8g8b8_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, FALSE);
        default:
            break;
    }

    FIXME("Cannot find a conversion function from format %s to DXT3.\n", debug_d3dformat(format));
    return FALSE;
}

BOOL wined3d_dxt5_encode(const BYTE *src, BYTE *dst, DWORD pitch_in, DWORD pitch_out,
        enum wined3d_format_id format, unsigned int w, unsigned int h)
{
    if (!txc_dxtn_handle)
        return FALSE;

    switch (format)
    {
        case WINED3DFMT_B8G8R8A8_UNORM:
            return x8r8g8b8_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, TRUE);
        case WINED3DFMT_B8G8R8X8_UNORM:
            return x8r8g8b8_to_dxtn(src, dst, pitch_in, pitch_out, w, h,
                                    GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, FALSE);
        default:
            break;
    }

    FIXME("Cannot find a conversion function from format %s to DXT5.\n", debug_d3dformat(format));
    return FALSE;
}

BOOL wined3d_dxtn_init(void)
{
    static const char *soname[] =
    {
#ifdef SONAME_LIBTXC_DXTN
        SONAME_LIBTXC_DXTN,
#endif
#ifdef __APPLE__
        "libtxc_dxtn.dylib",
        "libtxc_dxtn_s2tc.dylib",
#endif
        "libtxc_dxtn.so",
        "libtxc_dxtn_s2tc.so.0"
    };
    int i;

    for (i = 0; i < sizeof(soname)/sizeof(soname[0]); i++)
    {
        txc_dxtn_handle = wine_dlopen(soname[i], RTLD_NOW, NULL, 0);
        if (txc_dxtn_handle) break;
    }

    if (!txc_dxtn_handle)
    {
        FIXME("Wine cannot find the txc_dxtn library, DXTn software support unavailable.\n");
        return FALSE;
    }

    #define LOAD_FUNCPTR(f) \
        if (!(p##f = wine_dlsym(txc_dxtn_handle, #f, NULL, 0))) \
        { \
            ERR("Can't find symbol %s , DXTn software support unavailable.\n", #f); \
            goto error; \
        }

    LOAD_FUNCPTR(fetch_2d_texel_rgba_dxt1);
    LOAD_FUNCPTR(fetch_2d_texel_rgba_dxt3);
    LOAD_FUNCPTR(fetch_2d_texel_rgba_dxt5);
    LOAD_FUNCPTR(tx_compress_dxtn);

    #undef LOAD_FUNCPTR
    return TRUE;

error:
    wine_dlclose(txc_dxtn_handle, NULL, 0);
    txc_dxtn_handle = NULL;
    return FALSE;
}

BOOL wined3d_dxtn_supported(void)
{
    return (txc_dxtn_handle != NULL);
}

void wined3d_dxtn_free(void)
{
    if (txc_dxtn_handle)
        wine_dlclose(txc_dxtn_handle, NULL, 0);
}
