# -------------------------------------------------------------------
# ORTHOGRAPHIC
# Your personal aerial satellite. Always on. At any altitude.*
# Developed by MarStrMind
# License: Open Software License 3.0
# Up to date version always on marstr.online
# -------------------------------------------------------------------
# xp_normalmap.py
# For some geographical features, we will add a normal map for
# additional realism in the orthos, without using additional meshes
# or geometry.
#
# Source converted to Python from this C++ StackOverflow:
# https://stackoverflow.com/a/2368794
# -------------------------------------------------------------------


import numpy as np
import math
import os
from PIL import Image, ImageFilter
from defines import *
from log import *


class mstr_xp_normalmap:

    # Only a few params
    def __init__(self, lat, lng, tag, value, tv, th, latlngfld):
        self._lat = lat
        self._lng = lng
        self._tag = tag
        self._value = value
        self._latlngfld = latlngfld
        self._tv = tv
        self._th = th
        mstr_msg("xp_normalmap", "[X-Plane] Normal Map generator initialized")


    # Load the layer image and resize it to 1/4th its size -
    # then provide it
    def load_layer(self):
        qtr = int(mstr_photores / 4)
        image = Image.open(mstr_datafolder + "_cache/" + str(self._lat) + "-" + str(self._tv) + "_" + str(self._lng) + "-" + str(self._th) + "_" + self._tag + "-" + self._value + "_layer.png")
        image = image.resize((qtr,qtr), Image.Resampling.LANCZOS)
        mstr_msg("xp_normalmap", "[X-Plane] Layer image loaded")
        return image


    # A few mathematical calls we need
    # --------------------------------------------------------
    def intensity(self, pixel):
        avg = (pixel[0] + pixel[1] + pixel[2]) / 3
        if avg > 0:
            pavg = 255.0 / avg
        else:
            pavg = 0
        return pavg


    def clamp(self, px, mpx):
        if px > mpx-1:
            return mpx-1
        else:
            if px < 0:
                return 0
            else:
                return px


    def map_component(self, px):
        return (px + 1.0) * (255.0 / 2.0)


    def normalize_vector(self, v):
        vc = np.array([v[0], v[1], v[2]])
        norm = np.linalg.norm(vc)
        nv = vc / norm
        return nv
    # --------------------------------------------------------


    # The Big Mac. Generate the normal map
    def generate_normal_map_for_layer(self, image):
        mstr_msg("xp_normalmap", "[X-Plane] Beginning normal map generation")
        # No specularity, no reflectivity - but standard color
        # Blue (reflectivity) and alpha (specularity) need to be 1 - but can be adjusted as needed
        nmp = Image.new("RGBA", (image.width, image.height), (128,128,1,1))
        org = image.load()
        nmp_pix = nmp.load()

        # Let's try some shenanigans
        w = image.width
        h = image.height
        for y in range(h):
            for x in range(w):
                p = org[x,y]
                if p[3] > 0: # Only do something if there is something to do in layer
                    # Neighboring pixels
                    px_t  = org[ self.clamp(x, w),   self.clamp(y+1, h) ]
                    px_tr = org[ self.clamp(x+1, w), self.clamp(y+1, h) ]
                    px_r  = org[ self.clamp(x+1, w), self.clamp(y, h)   ]
                    px_br = org[ self.clamp(x+1, w), self.clamp(y+1, h) ]
                    px_b  = org[ self.clamp(x, w),   self.clamp(y-1, h) ]
                    px_bl = org[ self.clamp(x-1, w), self.clamp(y-1, h) ]
                    px_l  = org[ self.clamp(x-1, w), self.clamp(y, h)   ]
                    px_tl = org[ self.clamp(x-1, w), self.clamp(y+1, h) ]

                    # Intensities of pixels
                    it_t  = self.intensity(px_t)
                    it_tr = self.intensity(px_tr)
                    it_r  = self.intensity(px_r)
                    it_br = self.intensity(px_br)
                    it_b  = self.intensity(px_b)
                    it_bl = self.intensity(px_bl)
                    it_l  = self.intensity(px_l)
                    it_tl = self.intensity(px_tl)

                    # Sobel filter
                    dx = (it_tr + 2.0 * it_r + it_br) - (it_tl + 2.0 * it_l + it_bl)
                    dy = (it_bl + 2.0 * it_b + it_br) - (it_tl + 2.0 * it_t + it_tr)
                    dz = 10 # This is usually a good value for strength
                    v = (dx, dy, dz)
                    nrm = self.normalize_vector(v)
                    
                    if nrm[1] > 0:
                        nrm[1] = 0 - (abs(nrm[1]))
                    else:
                        nrm[1] = abs(nrm[1])

                    # Set pixel
                    nmp_pix[x,y] = (int(self.map_component(nrm[0])), int(self.map_component(nrm[1])), int(self.map_component(nrm[2])), 1)

        mstr_msg("xp_normalmap", "[X-Plane] Normal map generated")
        return nmp
    

    # The funnction to call. Blends with the existing map, or creates a new one
    def build_normalmap(self):
        mstr_msg("xp_normalmap", "[X-Plane] Building normal map")
        # The layer image
        lyr = self.load_layer()

        # Make the normal map for the layer
        nrm = self.generate_normal_map_for_layer(lyr)

        # Normal map final file name
        nrmfln = mstr_datafolder + "z_orthographic/normals/" + self._latlngfld + "/" + str(self._tv) + "_" + str(self._th) + ".png"

        # Check for existence of normal map file
        ex = os.path.isfile(nrmfln)

        # Does not exist? Just save
        if ex == False:
            nrm.save(nrmfln)

        # Exists? Open it, composite both, save
        if ex == True:
            nrmmap = Image.open(nrmfln)
            nrmmap.alpha_composite(nrm)

            # Specularity blending correction
            nrmmap_pix = nrmmap.load()
            for y in range(nrmmap.height):
                for x in range(nrmmap.width):
                    c = nrmmap_pix[x,y]
                    nrmmap_pix[x,y] = (c[0], c[1], c[2], 1)
            nrmmap.save(nrmfln)
            
        mstr_msg("xp_normalmap", "[X-Plane] Normal map saved")