# -*- coding: utf-8 -*-

"""
Created on Fri Mar  8 15:13:12 2019

Ce module contient de nombreuses implémentations de Noise, permetant en les assemblant de créer des mondes

@author: mysaa
"""

from data import ArrayedWorldChunk,Noise
import random as r
import numpy as np
from math import sqrt,floor,ceil,pi


##### Paramètres #####
G = 4.5
F = 5.2

         
class RandNoise(Noise):
    """
		Ce bruit renvoie une carte de vecteurs complexes (2d) du cercle trigonométrique (de module 1)
	"""
    seed = None
    f = None
    
    def __init__(self,seed,f=lambda r : r.random()):
        self.seed = seed
        self.f = f
        
    def getRandomly(self,xg,yg):
        """
            Cette fonction renvoie un nombre complexe aléatoire du cercle
            trigonométrique, uniformément distribué selon l'argument.
            Cette fonction est déterministe pour un même seed demandé.
        """
        s = ((self.seed & 0xFFFFFFFFFFFFFFFF) << 64) | ((int(xg) & 0xFFFFFFFF) << 32) | (int(yg) & 0xFFFFFFFF)
        r.seed(s)
        return self.f(r)
    
    
    def getChunk(self,x,y,n):
        """
			x,y sont les coordonées du chunk à considérer (boucle au bout de 4294967296=2^32) java:int
			n est un couple ou une liste d'aumoins deux éléments contenant la précision suivant x et y du chunk
		"""
        randomizer = lambda i,j : self.getRandomly(x+i,y+j)
        
        return np.fromfunction(np.vectorize(randomizer),(n[0],n[1]))
        
    
    
class RandLinNoise(RandNoise):
    
    y0 = 0
    y1 = 1
    
    def __init__(self,seed,y0,y1):
        super().__init__(seed,lambda r : (y1-y0)*r.random() + y0)

class RandTrigNoise(RandNoise):
    
    def __init__(self,seed):
        super().__init__(seed,lambda r : np.exp(1j*2*pi*r.random()))
        
class DroiteNoise(Noise):
    
    seed = None
    F,D = 0,0
    
    def __init__(self,seed,F,D):
        self.seed = seed
        self.F,self.D = F,D
        
    def getChunk(self,x,y,n=None):
        
        return self.getLoadedDroites(x,y)
        
#        randomizer = lambda i,j : self.getRandomGradient(x+i,y+j)
#        
#        return np.fromfunction(np.vectorize(randomizer),(n,))
    
    def getLoadedDroites(self,x,y):
        """
            Cette fonction renvoie la liste des droites devant être considérées dans la génération du chunk x,y. Cela permet d'effectuer la génération procédurale.
        """
        def dst(x0,x1,y0,y1):
            """
                Cette fonction renvoie la ditance eucildienne 2D entre les points (x0,y0) et (x1,y1)
            """
            return sqrt( (x1-x0)**2 + (y1-y0)**2 )
        
        F = self.F
        x0 = floor(x-F)
        x1 = floor(x+F+1)
        y0 = floor(y-F)
        y1 = floor(y+F+1)
#        print(x0,x1,y0,y1)
        drts = []
        for i in range(x0,x1+1):
            for j in range(y0,y1+1):
                for d in self.getDroitesOnChunk(i,j):
                    # Tester si la droite sera utile
                    dx = d[0]+i
                    dy = d[1]+j
                    if (x <= dx <= x+1 and y-F <= dy <= y+F+1) or (y <= dy <= y+1 and x-F <= dx <= x+F+1) or  (min(dst(x,dx,y,dy),dst(x+1,dx,y,dy),dst(x+1,dx,y+1,dy),dst(x,dx,y+1,dy)) <= F):
                        drts.append((dx,dy,d[2]))
#        print(len(drts))
        return drts
        
    
    def getDroitesOnChunk(self,xg,yg):
        s = ((self.seed & 0xFFFFFFFFFFFFFFFF) << 64) | ((int(xg) & 0xFFFFFFFF) << 32) | (int(yg) & 0xFFFFFFFF)
        r.seed(s)
        L = []
        for i in range(self.D):
            lx = r.random()
            ly = r.random()
            theta = r.random()*2*pi
            L.append((lx,ly,theta))
        return L
        

class PerlinNoise(Noise):
    
    G = None
    randomizer = None
    interpol = None 
    wrapper = None
    
    
    
    def __init__(self,G,randomizer,interpol=lambda a,b,w : (b-a)*w**2*6*(1/2-w/3)+a,wrapper = lambda x : np.tanh(x*3.8622)): # Par défaut, un banale interpolation linéaire
        self.G = G
        if type(randomizer) == int:
            randomizer = RandTrigNoise(randomizer)
        self.randomizer = randomizer
        self.interpol = interpol
        self.wrapper = wrapper
    
    def getChunkGradients(self,x,y):
        G=self.G # Python de merde !
        x0 = floor(x/G)
        x1 = ceil((x+1)/G)
        y0 = floor(y/G)
        y1 = ceil((y+1)/G)
        nx = x1-x0+1
        ny = y1-y0+1
#        grads = np.fromfunction(np.vectorize(lambda x,y : self.getPerlinGradient(x+x0,y+y0)),(nx,ny))
        grads = self.randomizer.getChunk(x0,y0,(nx,ny))
        return grads,x0,y0
    
    
    def getChunk(self,x,y,n):
        
        G = self.G
        chunk = np.zeros(n) # Initialise la sortie
        
        gradients,x0,y0 = self.getChunkGradients(x,y)
        
        def dotGridGradient(ix, iy, tx, ty):
            dx = tx - ix
            dy = ty - iy
            return (np.conj(gradients[ix-x0][iy-y0])*(dx+1j*dy)).real
        
        
        for i in range(n[0]):
            for j in range(n[1]):
                #C------------D#
                #|            |#
                #|            |#
                #|            |#
                #|    x M     |#
                #|            |#
                #A------------B#
                posx = x + i/n[0]
                posy = y + j/n[1]
                xx = posx / G
                yy = posy / G
                xx0 = floor(xx)
                yy0 = floor(yy)
                xx1 = xx0 + 1
                yy1 = yy0 + 1
                
                
                gA = dotGridGradient(xx0, yy0, xx, yy);
                gB = dotGridGradient(xx1, yy0, xx, yy);
                gC = dotGridGradient(xx0, yy1, xx, yy);
                gD = dotGridGradient(xx1, yy1, xx, yy);
                haut = self.interpol(gA, gB, xx - xx0);
                bas = self.interpol(gC, gD, xx - xx0);
                valeur =  self.interpol(haut, bas, yy - yy0);

                chunk[i,j] = valeur
        
        return self.wrapper(chunk)
                    
class FractalNoise(Noise):
    
    F = None
    D = None
    epsilon = None
    interpol = None
    droiteMaker = None
    
    def interpolizer(n,F):
        """
            Retourne une fonction polynomiale réelle sur [-F,F] et nulle autre part, s'annule en F et -F, vaut 1 en 0 et a comme dérivée 0 en -F,0 et F. n+1 est le degré de la racine 0.
        """
        return lambda x : 0 if abs(x)>F else 1 + (2*n**2+6*n+4)/(F**(2*n+4)) * ((x**2)/(2*n+4)-(F**2)/(2*n+2))*abs(x)**(2*n+2)
    
    
    def __init__(self,F,D,epsilon,droiteMaker,n = 1):
        self.F = F
        self.D = D
        self.epsilon = epsilon
        if type(droiteMaker) == int:
            droiteMaker = DroiteNoise(droiteMaker,F,D)
        self.droiteMaker = droiteMaker
        self.interpol = FractalNoise.interpolizer(n,F)
    
    def getChunk(self,x,y,n):
        drts = self.droiteMaker.getChunk(x,y)
        chunk = np.zeros(n)
        epsilon = self.epsilon
        interpol = self.interpol
           
        def dst(x0,x1,y0,y1):
            return sqrt( (x1-x0)**2 + (y1-y0)**2 )
        
        def kelkote(drt,x,y):
            dx = drt[0]
            dy = drt[1]
            #print(x,y,dx,dy)
            return 1 if (np.exp(1j*(drt[2]+pi/2))   *   ((x-dx)+(dy-y)*1j)).real >= 0 else -1
        
        # FractalNoise(0.7,511,0.01,42).getChunk(3,3,(16,16))
        #33.8 s ± 72.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
        for d in drts:
            drteffect = lambda i,j : interpol(dst(d[0],i/n[0] + x,d[1],j/n[1] + y))*kelkote(d,i/n[0] + x,j/n[1] + y)*epsilon
            chunk += np.fromfunction(np.vectorize(drteffect),n)
            
        # FractalNoise(0.7,511,0.01,42).getChunk(3,3,(16,16))
        #28.9 s ± 344 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
#        for i in range(n[0]):
#            for j in range(n[1]):
#                posx = i/n[0] + x
#                posy = j/n[1] + y
#                value = 0
##                print(i,j)
#                for d in drts:
#                    value += interpol(dst(d[0],posx,d[1],posy))*kelkote(d,posx,posy)*epsilon
#                    chunk[i,j] = value
        return chunk
       
        
class TestNoise(Noise):
    
    nn = PerlinNoise
    
    def getChunk(self,x,y,n):
        
        indexes = np.array([i for i in range(n[0]*n[1]+1)])
        data = np.ones((n[0]*n[1]))
        return WorldChunk(n,indexes,data)
        
        

      
class CavernedNoise(Noise):
    
    perlinSurface = None
    perlinGrotte = None
    perlinFond = None
    
    def __init__(self):
        self.perlinSurface = PerlinNoise(.5,64)
        self.perlinGrotte  = PerlinNoise(7 ,77)
        self.perlinFond    = PerlinNoise(.3 ,23)
    
    
    def getChunk(self,x,y,n):
        chk = self.perlinSurface.getChunk(x,y,n)
        fond =   self.perlinFond.getChunk(x,y,n)
        grotte=self.perlinGrotte.getChunk(x,y,n)
        
        out = []
        
        for i in range(n[0]):
            lig = []
            for j in range(n[1]):
                if(grotte[i,j]>.2): # Pas de grotte
                    lig.append([chk[i,j]])
                elif(grotte[i,j]>0):
                    lig.append([fond[i,j]])
                else:
                    lig.append([fond[i,j],chk[i,j]-0.1*abs(grotte[i,j]),chk[i,j]])
            out.append(lig)
        
        return out
    
    
class CavernedNoise2(Noise):
    
    perlinCielH = None
    perlinGHaut = None
    perlinGH = None
    perlinGHp = None
    perlinGBas = None
    
    #-x^(4)+4x^(3)-6x^(2)+4x
    
    def __init__(self,seed):
        self.perlinCielH = PerlinNoise(7 ,seed)
        self.perlinGHaut = PerlinNoise(5 ,seed)
        self.perlinGH    = PerlinNoise(25 ,seed)
        self.perlinGHp   = PerlinNoise(1 ,seed)
        self.perlinGBas  = PerlinNoise(5 ,seed)
    
    
    def getChunk(self,x,y,n):
        gtTransform = np.vectorize(lambda x : 0 if x<0 else sqrt(2*x-x**2)**1.5)
        transform=lambda M,a,b : M*b+a
        cielH = transform(self.perlinCielH.getChunk(x,y,n),28,28)
        gHaut = transform(self.perlinGHaut.getChunk(x,y,n),60,20)
        ghp   = transform(self.perlinGHp.getChunk(x,y,n)  ,0.005,0.005)
        ghh   = transform(self.perlinGHp.getChunk(x,y,n)  ,0.5,0.5) 
        gBas =  transform(self.perlinGBas.getChunk(x,y,n),10,10)
        gh = gtTransform(ghp+ghh)
        
        toit = 128
        
        out = []
        
        
        
        for i in range(n[0]):
            lig = []
            for j in range(n[1]):
                ch =  cielH[i,j] # la hauteur entre la surface et le ciel
                ght = gHaut[i,j] # haut limite de la grotte
                gbs = gBas[i,j] # bas limite de la grotte
                hauteur=gh[i,j] # pourcentage de hauteur de la grotte
                grh = (ght+gbs +hauteur*(ght-gbs))/2 # vrai plafond de la grotte
                grb = (ght+gbs -hauteur*(ght-gbs))/2 # vrai sol de la grotte
                if(ght<=40):print(ght)
                if hauteur==0:
                    # Pas de grotte
                    lig.append([toit-ch])
                elif(grh+ch>=toit):
                    # La grotte est ouverte sur la surface
                    lig.append([grb])
                else:
                    # Grotte souterraine et surface
                    lig.append([grb,grh,toit-ch])
                
            out.append(lig)
        
        return ArrayedWorldChunk.fromList(out)



#for c in cmaps:
#    pp.figure()
#    print(c)
#    pp.imshow(I, cmap=c)




#sys.exit()
####### Fenêtre graphique #######
#from PyQt5.QtWidgets import QVBoxLayout,QHBoxLayout,QPushButton,QWidget,QApplication,QFormLayout,QLabel,QTextEdit,QDial
#
#app = QApplication([])
#
#class ExplorerWidget(QWidget):
#    
#    def __init__():
#        print('wow')
#    
##### Control Panel ####
#seedSelector = QTextEdit()
#ndroitesSelector = QDial()
#
#cPanel = QFormLayout()
#cPanel.addWidget(QLabel("Seed : "))
#cPanel.addWidget(seedSelector)
#cPanel.addWidget(QLabel("Nombre de droites :"))
#cPanel.addWidget(ndroitesSelector)
#
#globalL = QHBoxLayout()
#globalL.addStretch(1)
#globalL.addLayout(cPanel)
#
#window = QWidget()
#window.setLayout(globalL)
#window.show()
#
#app.exec_()

#
#(x0,x1,y0,y1) = (0,4,0,4)
#n = 100
#
#
#x = np.linspace(x0,x1,n)
#y = np.linspace(y0,y1,n)
#x00 = int(x0)-1
#y00 = int(y0)-1
#x11 = int(x1)+1
#y11 = int(y1)+1
#gradient = np.exp(np.random.rand(x11-x00+1,y11-y00+1)*2*np.pi*1j)
#X,Y = np.meshgrid(x,y)
##print(gradient)
#
#def lerp(a0, a1, w):
#    return a0 + (a1-a0)*(-2*w*w*w+3*w*w)
# 
#def dotGridGradient(ix, iy, x, y):
#    dx = x - ix
#    dy = y - iy
#    return (np.conj(gradient[iy-y00][ix-x00])*(dx+1j*dy)).real
#
#def bruit(x,y):
#    (x0,y0) = (int(x),int(y))
#    (x1,y1) = (x0+1,y0+1)
#    
#    sx = x - x0;
#    sy = y - y0;
#    
#    n0 = dotGridGradient(x0, y0, x, y);
#    n1 = dotGridGradient(x1, y0, x, y);
#    ix0 = lerp(n0, n1, sx);
#    n0 = dotGridGradient(x0, y1, x, y);
#    n1 = dotGridGradient(x1, y1, x, y);
#    ix1 = lerp(n0, n1, sx);
#    return lerp(ix0, ix1, sy);
#
#
#
#Z = np.zeros((n,n))
#for i in range(n):
#    for j in range(n):
#        Z[i,j] = bruit(x[i],y[j])
#
#pp.imshow(Z,cmap='autumn')
#
#fig = pp.figure()
#ax = pp.axes(projection='3d')
#
#ax.view_init(80, 42)
#ax.plot_surface(X,Y,Z, rstride=1, cstride=1,
#                cmap='autumn', edgecolor='none')





