#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
**Project Name:**      MakeHuman

**Product Home Page:** http://www.makehumancommunity.org/

**Github Code Home Page:**    https://github.com/makehumancommunity/

**Authors:**           Glynn Clements

**Copyright(c):**      MakeHuman Team 2001-2020

**Licensing:**         AGPL3

    This file is part of MakeHuman Community (www.makehumancommunity.org).

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.


Abstract
--------

TODO
"""

import os.path
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL.ARB.texture_non_power_of_two import *
from core import G
from image import Image
import log
from getpath import getSysDataPath

NOTFOUND_TEXTURE = getSysDataPath('textures/texture_notfound.png')

class Texture(object):
    _npot = None
    _powers = None

    def __new__(cls, *args, **kwargs):
        self = super(Texture, cls).__new__(cls)

        if cls._npot is None:
            cls._npot = glInitTextureNonPowerOfTwoARB()
            try:
                import debugdump
                debugdump.dump.appendMessage("GL.EXTENSION: GL_ARB_texture_non_power_of_two %s" % ("enabled" if cls._npot else "not available"))
            except Exception as e:
                log.error("Failed to write GL debug info to debug dump: %s", format(str(e)))
        if cls._powers is None:
            cls._powers = [2**i for i in range(20)]

        self.textureId = glGenTextures(1)
        self.width = 0
        self.height = 0
        self.modified = None

        return self

    def __init__(self, image = None, size = None, components = 4):
        if image is not None:
            self.loadImage(image)
        elif size is not None:
            width, height = size
            self.initTexture(width, height, components)

    def __del__(self):
        try:
            glDeleteTextures(self.textureId)
        except Exception:
            pass

    @staticmethod
    def getFormat(components):
        if components == 1:
            return (GL_ALPHA8, GL_ALPHA)
        elif components == 3:
            return (3, GL_RGB)
        elif components == 4:
            return (4, GL_RGBA)
        else:
            raise RuntimeError("Unsupported pixel format")

    def initTexture(self, width, height, components = 4, pixels = None):
        internalFormat, format = self.getFormat(components)

        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)

        use_mipmaps = False
        if not (width in self._powers and height in self._powers) and not self._npot:
            log.debug("Non-power-of-two textures not supported, building mipmaps for image with dimensions %sx%s.", width, height)
            use_mipmaps = True
        if use_mipmaps and pixels is None:
            raise RuntimeError("Non-power-of-two textures not supported")

        if pixels is None:
            # Zero fill pixel data to allocate
            import numpy as np
            pixels = np.zeros(width*height*components, dtype=np.uint8)

        if height == 1:
            glBindTexture(GL_TEXTURE_1D, self.textureId)

            if not use_mipmaps:
                glTexImage1D(GL_PROXY_TEXTURE_1D, 0, internalFormat, width, 0, format, GL_UNSIGNED_BYTE, pixels)
                if not glGetTexLevelParameteriv(GL_PROXY_TEXTURE_1D, 0, GL_TEXTURE_WIDTH):
                    log.notice('texture size (%d) too large, building mipmaps', width)
                    use_mipmaps = True

            if use_mipmaps:
                gluBuild1DMipmaps(GL_TEXTURE_1D, internalFormat, width, format, GL_UNSIGNED_BYTE, pixels)
                # glGetTexLevelParameter is broken on X11
                # width  = glGetTexLevelParameteriv(GL_TEXTURE_1D, 0, GL_TEXTURE_WIDTH)
            else:
                glTexImage1D(GL_TEXTURE_1D, 0, internalFormat, width, 0, format, GL_UNSIGNED_BYTE, pixels)

            glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_T, GL_REPEAT)

            glBindTexture(GL_TEXTURE_1D, 0)
        else:
            glBindTexture(GL_TEXTURE_2D, self.textureId)

            if not use_mipmaps:
                glTexImage2D(GL_PROXY_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, pixels)
                if not glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH):
                    log.notice('texture size (%d x %d) too large, building mipmaps', width, height)
                    use_mipmaps = True

            if use_mipmaps:
                gluBuild2DMipmaps(GL_TEXTURE_2D, internalFormat, width, height, format, GL_UNSIGNED_BYTE, pixels)
                # glGetTexLevelParameter is broken on X11
                # width  = glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH)
                # height = glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT)
            else:
                glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, pixels)

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)

            glBindTexture(GL_TEXTURE_2D, 0)
        self.width, self.height = width, height
        log.debug('initTexture: %s, %s, %s', width, height, use_mipmaps)

    def loadImage(self, image):
        if isinstance(image, str):
            image = Image(image)

        pixels = image.flip_vertical().data

        self.initTexture(image.width, image.height, image.components, pixels)

    def loadSubImage(self, image, x, y):
        if not self.textureId:
            raise RuntimeError("Texture is empty, cannot load a sub texture into it")

        if isinstance(image, str):
            image = Image(image)

        internalFormat, format = self.getFormat(image.components)

        pixels = image.flip_vertical().data

        if image.height == 1:
            glBindTexture(GL_TEXTURE_1D, self.textureId)
            glTexSubImage1D(GL_TEXTURE_1D, 0, x, image.width, format, GL_UNSIGNED_BYTE, pixels)
            glBindTexture(GL_TEXTURE_1D, 0)
        else:
            glBindTexture(GL_TEXTURE_2D, self.textureId)
            glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, image.width, image.height, format, GL_UNSIGNED_BYTE, pixels)
            glBindTexture(GL_TEXTURE_2D, 0)

_textureCache = {}

def getTexture(path, cache=None):
    texture = None
    cache = cache or _textureCache

    if isinstance(path, Image):
        img = path
        if hasattr(img, 'sourcePath'):
            if img.sourcePath in cache:
                texture = cache[img.sourcePath]
                if not (hasattr(img, 'modified') and img.modified > texture.modified):
                    return texture
        else:
            log.warning("Image used as texture does not contain a \"sourcePath\" attribute, making it impossible to cache it. This could cause slow rendering (always creates new texture).")
            return Texture(img)

        import time
        if img.sourcePath in cache:
            log.debug("Reloading texture for dynamic image %s.", img.sourcePath)
            texture = cache[img.sourcePath]
            texture.loadImage(img)
        else:
            log.debug("Creating new texture for dynamic image %s.", img.sourcePath)
            texture = Texture(img)
        if hasattr(img, 'modified'):
            texture.modified = img.modified
        else:
            texture.modified = time.time()
        cache[img.sourcePath] = texture
        return texture

    elif not os.path.isfile(path):
        log.error('Cannot get texture for file path %s, no such file.', path)
        return None

    if path in cache:
        texture = cache[path]
        if texture is False:
            return texture

        if os.path.getmtime(path) > texture.modified:
            log.message('Reloading texture %s.', path)   # TL: unicode problems unbracketed

            try:
                img = Image(path=path)
                texture.loadImage(img)
            except RuntimeError as text:
                log.error("%s", text, exc_info=True)
                return
            else:
                texture.modified = os.path.getmtime(path)
    else:
        try:
            log.debug("Creating new texture for image %s.", path)
            img = Image(path=path)
            texture = Texture(img)
        except RuntimeError as text:
            log.error("Error loading texture %s", path, exc_info=True)
            texture = False
        else:
            texture.modified = os.path.getmtime(path)
        cache[path] = texture

    return texture
    
def reloadTextures():
    """
    Clear the entire texture cache, resulting in removing all contained textures
    from the GPU memory (unless other references are kept to the texture 
    objects).
    """
    log.message('Reloading all textures')
    for path in _textureCache:
        try:
            _textureCache[path].loadImage(path)
        except RuntimeError as _:
            log.error("Error loading texture %s", path, exc_info=True)

def reloadTexture(path):
    """
    Remove a texture from the texture cache. Removing a texture from cache will
    result in unloading the texture from the GPU memory, unless another
    reference to it is kept.
    """
    log.message('Reloading texture %s', path)
    if path not in _textureCache:
        log.error('Cannot reload non-existing texture %s', path)
        return
    try:
        _textureCache[path].loadImage(path)
    except RuntimeError as text:
        log.error("Error loading texture %s", path, exc_info=True)

