#!/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:**           Marc Flerackers, 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
--------

Methods that help create texture maps by projection on the model.
Supported creators:
Light map creator
Image map creator
UV map creator

"""

import numpy as np
from core import G
import mh
import log
import matrix
import image_operations
from progress import Progress

def v4to3(v):
    v = np.asarray(v)
    return v[:3,0] / v[3:,0]

def vnormalize(v):
    return v / np.sqrt(np.sum(v ** 2, axis=-1))[...,None]

class Shader(object):
    pass

class UvAlphaShader(Shader):
    def __init__(self, dst, texture, uva):
        self.dst = dst
        self.texture = texture
        self.size = np.array([texture.width, texture.height])
        self.uva = uva

    def shade(self, i, xy, uvw):
        dst = self.dst.data[xy[...,1],xy[...,0]]
        uva = np.sum(self.uva[i][None,None,:,:] * uvw[...,[1,2,0]][:,:,:,None], axis=2)
        ix = np.floor(uva[:,:,:2] * self.size[None,None,:]).astype(int)
        ix = np.minimum(ix, self.size - 1)
        ix = np.maximum(ix, 0)
        src = self.texture.data[ix[...,1], ix[...,0]]
        a = uva[:,:,2]
        return a[:,:,None] * (src.astype(float) - dst) + dst

class ColorShader(Shader):
    def __init__(self, colors):
        self.colors = colors

    def shade(self, i, xy, uvw):
        return np.sum(self.colors[i][None,None,:,:] * uvw[...,[1,2,0]][:,:,:,None], axis=2)

class MaskShader(Shader):
    def __init__(self, color=[255,255,255,255]):
        self.color = color

    def shade(self, i, xy, uvw):
        shape = list(xy.shape)[:-1]
        return np.tile(self.color, shape).reshape(shape + [4])

def RasterizeTriangles(dst, coords, shader):
    """
    Software rasterizer.
    """
    delta = coords - coords[:,[1,2,0],:]
    perp = np.concatenate((delta[:,:,1,None], -delta[:,:,0,None]), axis=-1)
    dist = np.sum(perp[:,0,:] * delta[:,2,:], axis=-1)
    perp /= dist[:,None,None]
    base = np.sum(perp * coords, axis=-1)

    cmin = np.floor(np.amin(coords, axis=1)).astype(int)
    cmax = np.ceil( np.amax(coords, axis=1)).astype(int)

    minx = cmin[:,0]
    maxx = cmax[:,0]
    miny = cmin[:,1]
    maxy = cmax[:,1]

    progress = Progress(len(coords), None).HighFrequency(200)
    for i in range(len(coords)):
        ixy = np.mgrid[miny[i]:maxy[i],minx[i]:maxx[i]].transpose([1,2,0])[:,:,::-1]
        xy = ixy + 0.5
        uvw = np.sum(perp[i,None,None,:,:] * xy[:,:,None,:], axis=-1) - base[i,None,None,:]
        mask = np.all(uvw > 0, axis=-1)
        col = shader.shade(i, ixy, uvw)
        # log.debug('dst: %s', dst.data[miny[i]:maxy[i],minx[i]:maxx[i]].shape)
        # log.debug('src: %s', col.shape)
        dst.data[miny[i]:maxy[i],minx[i]:maxx[i],:][mask] = col[mask]
        progress.step()

def getCamera(mesh):
    ex, ey, ez = G.app.modelCamera.eye
    eye = np.matrix([ex,ey,ez,1]).T
    fx, fy, fz = G.app.modelCamera.focus
    focus = np.matrix([fx,fy,fz,1]).T
    transform = mesh.transform.I
    eye = v4to3(transform * eye)
    focus = v4to3(transform * focus)
    camera = vnormalize(eye - focus)
    # log.debug('%s %s %s', eye, focus, camera)
    return camera

def getFaces(mesh):
    group_mask = np.ones(len(mesh._faceGroups), dtype=bool)
    for g in mesh._faceGroups:
        if g.name.startswith('joint') or g.name.startswith('helper'):
            group_mask[g.idx] = False
    faces = np.argwhere(group_mask[mesh.group])[...,0]
    return faces

def mapImageSoft(srcImg, mesh, leftTop, rightBottom):
    progress = Progress() (0)
    dstImg = mh.Image(G.app.selectedHuman.getTexture())

    dstW = dstImg.width
    dstH = dstImg.height

    srcImg = srcImg.convert(dstImg.components)

    camera = getCamera(mesh)
    faces = getFaces(mesh)

    # log.debug('matrix: %s', G.app.modelCamera.getConvertToScreenMatrix())

    progress(0.05)
    texco = np.asarray([0,dstH])[None,None,:] + mesh.texco[mesh.fuvs[faces]] * np.asarray([dstW,-dstH])[None,None,:]
    matrix_ = np.asarray(G.app.modelCamera.getConvertToScreenMatrix(mesh))
    coord = np.concatenate((mesh.coord[mesh.fvert[faces]], np.ones((len(faces),4,1))), axis=-1)
    # log.debug('texco: %s, coord: %s', texco.shape, coord.shape)
    coord = np.sum(matrix_[None,None,:,:] * coord[:,:,None,:], axis = -1)
    # log.debug('coord: %s', coord.shape)
    coord = coord[:,:,:2] / coord[:,:,3:]
    progress(0.1)
    # log.debug('coord: %s', coord.shape)
    # log.debug('coords: %f-%f, %f-%f',
    #           np.amin(coord[...,0]), np.amax(coord[...,0]),
    #           np.amin(coord[...,1]), np.amax(coord[...,1]))
    # log.debug('rect: %s %s', leftTop, rightBottom)
    coord -= np.asarray([leftTop[0], leftTop[1]])[None,None,:]
    coord /= np.asarray([rightBottom[0] - leftTop[0], rightBottom[1] - leftTop[1]])[None,None,:]
    alpha = np.sum(mesh.vnorm[mesh.fvert[faces]] * camera[None,None,:], axis=-1)
    alpha = np.maximum(0, alpha)
    # alpha[...] = 1 # debug
    # log.debug('alpha: %s', alpha.shape)
    # log.debug('coords: %f-%f, %f-%f',
    #           np.amin(coord[...,0]), np.amax(coord[...,0]),
    #           np.amin(coord[...,1]), np.amax(coord[...,1]))
    progress(0.15)
    uva = np.concatenate((coord, alpha[...,None]), axis=-1)
    # log.debug('uva: %s', uva.shape)
    valid = np.any(alpha >= 0, axis=1)
    # log.debug('valid: %s', valid.shape)
    texco = texco[valid,:,:]
    uva = uva[valid,:,:]

    # log.debug('%s %s', texco.shape, uva.shape)
    # log.debug('src: %s, dst: %s', srcImg.data.shape, dstImg.data.shape)

    log.debug("mapImage: begin render")

    progress(0.2, 0.6)
    RasterizeTriangles(dstImg, texco[:,[0,1,2],:], UvAlphaShader(dstImg, srcImg, uva[:,[0,1,2],:]))
    progress(0.6, 0.99)
    RasterizeTriangles(dstImg, texco[:,[2,3,0],:], UvAlphaShader(dstImg, srcImg, uva[:,[2,3,0],:]))
    progress.finish()

    log.debug("mapImage: end render")

    return dstImg

def mapImageGL(srcImg, mesh, leftTop, rightBottom):
    progress = Progress() (0)
    log.debug("mapImageGL: 1")

    dstImg = G.app.selectedHuman.meshData.object3d.textureTex

    dstW = dstImg.width
    dstH = dstImg.height

    left, top = leftTop
    right, bottom = rightBottom

    camera = getCamera(mesh)

    coords = mesh.r_texco

    texmat = G.app.modelCamera.getConvertToScreenMatrix(mesh)
    texmat = matrix.scale((1/(right - left), 1/(top - bottom), 1)) * matrix.translate((-left, -bottom, 0)) * texmat
    texmat = np.asarray(texmat)

    texco = mesh.r_coord

    alpha = np.sum(mesh.r_vnorm * camera[None,:], axis=-1)
    alpha = np.maximum(alpha, 0)
    color = (np.array([0, 0, 0, 0])[None,...] + alpha[...,None]) * 255

    color = np.ascontiguousarray(color, dtype=np.uint8)
    texco = np.ascontiguousarray(texco, dtype=np.float32)

    progress(0.5, 0.99)
    result = mh.renderSkin(dstImg, mesh.vertsPerPrimitive, coords, index = mesh.index,
                           texture = srcImg, UVs = texco, textureMatrix = texmat,
                           color = color, clearColor = None)

    progress(1)
    return result

def mapImage(imgMesh, mesh, leftTop, rightBottom):
    if mh.hasRenderSkin():
        try:
            return mapImageGL(imgMesh.mesh.object3d.textureTex, mesh, leftTop, rightBottom)
        except Exception as e:
            log.debug(e)
            log.debug("Hardware skin rendering failed, falling back to software render.")
            return mapImageSoft(mh.Image(imgMesh.getTexture()), mesh, leftTop, rightBottom)
    else:
        return mapImageSoft(mh.Image(imgMesh.getTexture()), mesh, leftTop, rightBottom)


'''fixSeams

A function for removing UV seams from projected images.

When creating images through projection, interference with the
limits of the UV map tend to produce dark seams on the mesh
along the lines that define the limits. Using this procedure,
one can repair these seams by UV mask aided post-processing.

Pass the image to be fixed in the 'img' parameter, and the mesh
from which the UV mask will be projected in the 'mesh' parameter.
If you already have a mask projected, you can pass it in place
of the 'mesh' parameter. The function will additionally return
the new expanded mask.

The seam fix can be expanded furtherly in case the projected
image is intented for blurring or other sampling operation
affected by pixels outside the UV border, by adjusting the
'border' parameter.

'''

def fixSeams(img, mesh, border = 1):
    from image_operations import Image, expand
    mask = mesh if isinstance(mesh, Image) else mapMask(img.size, mesh)
    img, mask = expand(img, mask, border)
    return (img, mask) if isinstance(mesh, Image) else img

def mapLightingSoft(lightpos = (-10.99, 20.0, 20.0), mesh = None, res = (1024, 1024), border = 1):
    """
    Create a lightmap for the selected human (software renderer).
    """
    progress = Progress() (0)

    if mesh is None:
        mesh = G.app.selectedHuman.mesh

    W = res[0]
    H = res[1]

    dstImg = mh.Image(width=W, height=H, components=4)
    dstImg.data[...] = 0

    delta = lightpos - mesh.coord
    ld = vnormalize(delta)
    del delta
    s = np.sum(ld * mesh.vnorm, axis=-1)
    del ld
    s = np.maximum(0, np.minimum(255, (s * 256))).astype(np.uint8)
    mesh.color[...,:3] = s[...,None]
    mesh.color[...,3] = 255
    del s

    faces = getFaces(mesh)

    coords = np.asarray([0,H])[None,None,:] + mesh.texco[mesh.fuvs[faces]] * np.asarray([W,-H])[None,None,:]
    colors = mesh.color[mesh.fvert[faces]]
    # log.debug("mapLighting: %s %s %s", faces.shape, coords.shape, colors.shape)

    log.debug("mapLighting: begin render")

    progress(0.1, 0.5)
    RasterizeTriangles(dstImg, coords[:,[0,1,2],:], ColorShader(colors[:,[0,1,2],:]))
    progress(0.5, 0.9)
    RasterizeTriangles(dstImg, coords[:,[2,3,0],:], ColorShader(colors[:,[2,3,0],:]))

    progress(0.9, 0.99)
    dstImg = fixSeams(dstImg, mesh, border)

    log.debug("mapLighting: end render")

    mesh.setColor([255, 255, 255, 255])

    progress.finish()
    return dstImg

def mapLightingGL(lightpos = (-10.99, 20.0, 20.0), mesh = None, res = (1024, 1024), border = 1):
    """
    Create a lightmap for the selected human (hardware accelerated).
    """
    progress = Progress() (0)

    if mesh is None:
        mesh = G.app.selectedHuman.mesh

    W = res[0]
    H = res[1]

    delta = lightpos - mesh.coord
    ld = vnormalize(delta)
    del delta
    s = np.sum(ld * mesh.vnorm, axis=-1)
    del ld
    s = np.maximum(0, np.minimum(255, (s * 256))).astype(np.uint8)
    mesh.color[...,:3] = s[...,None]
    mesh.color[...,3] = 255
    del s

    progress(0.1)
    mesh.markCoords(colr = True)
    mesh.update()

    coords = mesh.r_texco
    colors = mesh.r_color

    progress(0.2, 0.5)
    dstImg = mh.renderSkin((W, H), mesh.vertsPerPrimitive, coords, index = mesh.index,
                           color = colors, clearColor = (0, 0, 0, 0))

    progress(0.5, 0.99)
    dstImg = fixSeams(dstImg, mesh, border)

    mesh.setColor([255, 255, 255, 255])

    log.debug('mapLightingGL: %s', dstImg.data.shape)

    progress(1)
    return dstImg

def mapLighting(lightpos = (-10.99, 20.0, 20.0), mesh = None, res = (1024, 1024), border = 1):
    """
    Bake lightmap for human from one light.
    Uses OpenGL hardware acceleration if the necessary OGL features are
    available, otherwise uses a slower software rasterizer.
    """
    if mh.hasRenderSkin():
        try:
            return mapLightingGL(lightpos, mesh, res, border)
        except Exception as e:
            log.debug(e)
            log.debug("Hardware skin rendering failed, falling back to software render.")
            return mapLightingSoft(lightpos, mesh, res, border)
    else:
        return mapLightingSoft(lightpos, mesh, res, border)

def mapSceneLighting(scn, object = None, res = (1024, 1024), border = 1):
    """
    Create a lightmap for a scene with one or multiple lights.
    This happens by adding the lightmaps produced by each light,
    plus the scene ambience.
    """

    if object is None:
        object = G.app.selectedHuman

    objrot = G.app.modelCamera.getRotation()
    def calcLightPos(light):
        return tuple(
            matrix.transform3(
                matrix.rotx(-objrot[0]) *
                matrix.roty(-objrot[1]) *
                matrix.rotz(-objrot[2]),
                light.position))

    def getLightmap(light):
        lmap = mapLighting(calcLightPos(light), object.mesh, res, border)
        return image_operations.Image(data = lmap.data * light.color.values)

    progress = Progress(1 + len(scn.lights))

    # Ambience
    lmap = image_operations.colorAsImage(
        (scn.environment.ambience * object.material.diffuseColor).values, None, *res)
    if (object.material.shaderConfig['ambientOcclusion']
        and not object.material.aoMapTexture is None):
        aomap = image_operations.Image(data = object.material.aoMapTexture)
        aomap = image_operations.resized(aomap, *lmap.size)
        lmap = image_operations.multiply(lmap, image_operations.mix(
            aomap, image_operations.getWhite(lmap), object.material.aoMapIntensity))
    progress.step()

    # Lights
    if scn.lights:
        amb = lmap
        lmap = getLightmap(scn.lights[0]).data
        progress.step()
        for light in scn.lights[1:]:
            lmap = image_operations.mixData(lmap, getLightmap(light).data, 1, 1)
            progress.step()
        lmap = image_operations.Image(data = lmap)
        if len(scn.lights) > 1:
            lmap = image_operations.normalize(lmap)

        # Ambience calculation function: lmap = lmap + (1 - lmap) * amb
        lmap = image_operations.mix(
            lmap, image_operations.multiply(
                image_operations.invert(lmap), amb), 1, 1)

    return lmap

def mapMask(dimensions = (1024, 1024), mesh = None):
    """
    Create a texture map mask, for finding the texture map borders.
    """
    if mh.hasRenderSkin():
        try:
            if mesh is None:
                mesh = G.app.selectedHuman.mesh
            return mh.renderSkin(dimensions, mesh.vertsPerPrimitive,
                                 mesh.r_texco, index = mesh.index,
                                 clearColor = (0, 0, 0, 0))
        except Exception as e:
            log.debug(e)
            log.debug("Hardware skin rendering failed, falling back to software render.")
            return mapMaskSoft(dimensions, mesh)
    else:
        return mapMaskSoft(dimensions, mesh)

def mapMaskSoft(dimensions = (1024, 1024), mesh = None):
    """
    Create a texture mask for the selected human (software renderer).
    """
    progress = Progress() (0)
    if mesh is None:
        mesh = G.app.selectedHuman.mesh

    W = dimensions[0]
    H = dimensions[1]

    components = 4
    dstImg = mh.Image(width=W, height=H, components=components)
    dstImg.data[...] = np.tile([0,0,0,255], (H,W)).reshape((H,W,components))

    faces = getFaces(mesh)

    coords = np.asarray([0,H])[None,None,:] + mesh.texco[mesh.fuvs[faces]] * np.asarray([W,-H])[None,None,:]
    shape = mesh.fvert[faces].shape
    shape = tuple(list(shape) + [components])
    colors = np.repeat(1, np.prod(shape)).reshape(shape)

    log.debug("mapMask: begin render")

    progress(0.1, 0.55)
    RasterizeTriangles(dstImg, coords[:,[0,1,2],:], MaskShader())
    progress(0.55, 0.99)
    RasterizeTriangles(dstImg, coords[:,[2,3,0],:], MaskShader())

    log.debug("mapMask: end render")

    progress.finish()
    return dstImg

def rasterizeHLines(dstImg, edges, delta):
    flip = delta[:,0] < 0
    p = np.where(flip[:,None,None], edges[:,::-1,:], edges[:,:,:])
    del edges
    d = np.where(flip[:,None], -delta, delta)
    del delta, flip
    x0 = p[:,0,0]
    x1 = p[:,1,0]
    y0 = p[:,0,1]
    del p
    dx = d[:,0]
    dy = d[:,1]
    m = dy / dx
    del dx, dy, d
    c = y0 - m * x0
    x0 = np.floor(x0).astype(int)
    x1 = np.ceil(x1).astype(int)
    del y0

    data = dstImg.data[::-1]

    progress = Progress(len(x0), None)
    for i in range(len(x0)):
        x = np.arange(x0[i], x1[i])
        y = m[i] * (x + 0.5) + c[i]
        data[np.floor(y).astype(int),x,:] = 255
        progress.step()

def rasterizeVLines(dstImg, edges, delta):
    flip = delta[:,1] < 0
    p = np.where(flip[:,None,None], edges[:,::-1,:], edges[:,:,:])
    del edges
    d = np.where(flip[:,None], -delta, delta)
    del delta, flip
    x0 = p[:,0,0]
    y0 = p[:,0,1]
    y1 = p[:,1,1]
    del p
    dx = d[:,0]
    dy = d[:,1]
    m = dx / dy
    del dx, dy, d
    c = x0 - m * y0
    y0 = np.floor(y0).astype(int)
    y1 = np.ceil(y1).astype(int)
    del x0

    data = dstImg.data[::-1]

    progress = Progress(len(y0), None)
    for i in range(len(y0)):
        y = np.arange(y0[i], y1[i]) + 0.5
        x = m[i] * y + c[i]
        data[y.astype(int),np.floor(x).astype(int),:] = 255
        progress.step()

def mapUVSoft(mesh):
    """
    Project the UV map topology of the selected human mesh onto a texture
    (software rasterizer).
    """
    progress = Progress() (0)

    W = 2048
    H = 2048

    dstImg = mh.Image(width=W, height=H, components=3)
    dstImg.data[...] = 0

    faces = getFaces(mesh)

    log.debug("mapUV: begin setup")

    fuvs = mesh.fuvs[faces]
    del faces
    edges = np.array([fuvs, np.roll(fuvs, 1, axis=-1)]).transpose([1,2,0]).reshape((-1,2))
    del fuvs
    edges = np.where((edges[:,0] < edges[:,1])[:,None], edges, edges[:,::-1])
    ec = edges[:,0] + (edges[:,1] << 16)
    del edges
    ec = np.unique(ec)
    edges = np.array([ec & 0xFFFF, ec >> 16]).transpose()
    del ec
    edges = mesh.texco[edges] * (W, H)
    progress(0.05)

    delta = edges[:,1,:] - edges[:,0,:]
    vertical = np.abs(delta[:,1]) > np.abs(delta[:,0])
    horizontal = ~vertical

    hdelta = delta[horizontal]
    vdelta = delta[vertical]
    del delta
    hedges = edges[horizontal]
    vedges = edges[vertical]
    del edges, horizontal, vertical

    log.debug("mapUV: begin render")

    progress(0.1, 0.55, "Projecting UV map")
    rasterizeHLines(dstImg, hedges, hdelta)
    progress(0.55, 0.99, "Projecting UV map")
    rasterizeVLines(dstImg, vedges, vdelta)
    progress(1)

    log.debug("mapUV: end render")

    return dstImg.convert(3)

def mapUVGL(mesh):
    """
    Project the UV map topology of the selected human mesh onto a texture
    (hardware accelerated).
    """
    progress = Progress() (0)

    W = 2048
    H = 2048

    dstImg = mh.Texture(size=(W,H), components=3)

    log.debug("mapUVGL: begin setup")

    fuvs = mesh.index
    edges = np.array([fuvs, np.roll(fuvs, 1, axis=-1)]).transpose([1,2,0]).reshape((-1,2))
    del fuvs
    edges = np.where((edges[:,0] < edges[:,1])[:,None], edges, edges[:,::-1])
    ec = edges[:,0] + (edges[:,1] << 16)
    del edges
    ec = np.unique(ec)
    edges = np.array([ec & 0xFFFF, ec >> 16]).transpose()
    del ec

    log.debug("mapUVGL: begin render")

    coords = mesh.r_texco
    edges = np.ascontiguousarray(edges, dtype=np.uint32)

    progress(0.6, 0.99)
    dstImg = mh.renderSkin(dstImg, 2, coords, index = edges, clearColor = (0, 0, 0, 255))

    log.debug("mapUV: end render")
    progress(1)

    return dstImg.convert(3)

def mapUV(mesh):
    """
    Project the UV map topology of the selected human mesh onto a texture.
    Uses OpenGL hardware acceleration if the necessary OGL features are
    available, otherwise uses a slower software rasterizer.
    """
    if mh.hasRenderSkin():
        try:
            return mapUVGL(mesh)
        except Exception as e:
            log.debug(e)
            log.debug("Hardware skin rendering failed, falling back to software render.")
            return mapUVSoft(mesh)
    else:
        return mapUVSoft(mesh)

