import pyglet
pyglet.options['vbos'] = False

from pyglet.gl import *
from pyglet.window import key
import math

print("Use WASD to move the player, press E to move the camera, and press the mouse buttons to break or place blocks.")

class World:
    def __init__(self):
        self.blocks = {}
        self.batch_blocks = {}
        self.batch = pyglet.graphics.Batch()

        def load_texture_group(file_name):
            tex = pyglet.image.load(file_name).get_texture()
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
            return pyglet.graphics.TextureGroup(tex)

        self.tex_dirt = load_texture_group('dirt.png')
        self.tex_d = load_texture_group('cobbblestone.png')
        self.tex_grass_top = load_texture_group('grass_top.png')
        self.tex_grass_side = load_texture_group('grass_side.png')

        self.block_textures = {
            1: { 'top': self.tex_grass_top, 'bottom': self.tex_dirt, 'side': self.tex_grass_side},
            
            2: {'top': self.tex_grass_top, 'bottom': self.tex_grass_top, 'side': self.tex_grass_top},
            
            3: {'top': self.tex_d, 'bottom': self.tex_d, 'side': self.tex_d}
        }

        import random
        p = list(range(256))
        random.shuffle(p)
        perm = p * 2

        def fade(t): return t * t * t * (t * (t * 6 - 15) + 10)
        def lerp(t, a, b): return a + t * (b - a)
        def grad(hash_code, x, y):
            h = hash_code & 7
            u = x if h < 4 else y
            v = y if h < 4 else (x if h == 12 or h == 14 else 0)
            return (u if (h & 1) == 0 else -u) + (v if (h & 2) == 0 else -v)

        def perlin_2d(x, y):
            X, Y = int(math.floor(x)) & 255, int(math.floor(y)) & 255
            x -= math.floor(x); y -= math.floor(y)
            u, v = fade(x), fade(y)
            A, B = (perm[X] + Y) & 511, (perm[X + 1] + Y) & 511
            return lerp(v, lerp(u, grad(perm[A], x, y), grad(perm[B], x - 1, y)),
                           lerp(u, grad(perm[A + 1], x, y - 1), grad(perm[B + 1], x - 1, y - 1)))
        
        CHUNK_SIZE = 16

        for chunk_x in range(-1, 2):
            for chunk_z in range(-1, 2):
                for chunk_y in range(-1, 2):
                    start_x = chunk_x * CHUNK_SIZE
                    start_z = chunk_z * CHUNK_SIZE
                    start_y = chunk_y *  CHUNK_SIZE
                
                for x in range(start_x, start_x + CHUNK_SIZE):
                    for z in range(start_z, start_z + CHUNK_SIZE):
                        
                        noise_val = perlin_2d(x / 1.0, z / 1.0)
                        
                        final_height = int(math.floor(-17 + (noise_val * 9)))
                        
                        self.blocks[(x, final_height, z)] = 1
                        
                        for y_under in range(final_height - 3, final_height):
                            self.blocks[(x, y_under, z)] = 2

                            self.blocks[(x, final_height, z)] = 3

        self.rebuild_batch()

    def get_block(self, x, y, z):
        return self.blocks.get((int(x), int(y), int(z)))

    def set_block(self, x, y, z, block_type):
        key = (int(x), int(y), int(z))
        if block_type:
            self.blocks[key] = block_type
        else:
            self.blocks.pop(key, None)
            
        positions_to_update = [
            (key[0], key[1], key[2]),
            (key[0] + 1, key[1], key[2]), (key[0] - 1, key[1], key[2]),
            (key[0], key[1] + 1, key[2]), (key[0], key[1] - 1, key[2]),
            (key[0], key[1], key[2] + 1), (key[0], key[1], key[2] - 1)
        ]
        
        for pos in positions_to_update:
            if pos in self.batch_blocks:
                for item in self.batch_blocks[pos]:
                    item.delete()
                self.batch_blocks.pop(pos, None)
            
            neighbor_type = self.blocks.get(pos)
            if neighbor_type:
                self.add_block_to_batch(pos[0], pos[1], pos[2], neighbor_type)

    def rebuild_batch(self):
        for key, items in self.batch_blocks.items():
            for item in items:
                item.delete()
        self.batch_blocks.clear()
        
        for (x, y, z), block_type in self.blocks.items():
            self.add_block_to_batch(x, y, z, block_type)

    def add_block_to_batch(self, x, y, z, block_type):
        t = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1))
        X, Y, Z = x+1, y+1, z+1

        faces = [
            ((x,Y,Z, X,Y,Z, X,Y,z, x,Y,z), (0,1,0), 'top'),  
            ((x,y,z, X,y,z, X,y,Z, x,y,Z), (0,-1,0), 'bottom'), 
            ((x,y,Z, X,y,Z, X,Y,Z, x,Y,Z), (0,0,1), 'side'),  
            ((X,y,z, x,y,z, x,Y,z, X,Y,z), (0,0,-1), 'side'), 
            ((X,y,Z, X,y,z, X,Y,z, X,Y,Z), (1,0,0), 'side'),  
            ((x,y,z, x,y,Z, x,Y,Z, x,Y,z), (-1,0,0), 'side')  
        ]

        key = (int(x), int(y), int(z))
        if key not in self.batch_blocks:
            self.batch_blocks[key] = []

        textures_for_block = self.block_textures.get(block_type)

        for vertices, normal, face_type in faces:
            nx, ny, nz = normal
            if not self.get_block(x + nx, y + ny, z + nz):
                group = textures_for_block[face_type]
                vertex_list = self.batch.add(4, GL_QUADS, group, ('v3f', vertices), t)
                self.batch_blocks[key].append(vertex_list)

    def draw(self):
        self.batch.draw()

    
    def get_block(self, x, y, z):
        return self.blocks.get((int(x), int(y), int(z)))

    def set_block(self, x, y, z, block_type):
        key = (int(x), int(y), int(z))
        
        if block_type:
            self.blocks[key] = block_type
        else:
            self.blocks.pop(key, None)
            
        positions_to_update = [
            (key[0], key[1], key[2]),     
            (key[0] + 1, key[1], key[2]), 
            (key[0] - 1, key[1], key[2]), 
            (key[0], key[1] + 1, key[2]), 
            (key[0], key[1] - 1, key[2]), 
            (key[0], key[1], key[2] + 1), 
            (key[0], key[1], key[2] - 1)  
        ]
        
        for pos in positions_to_update:

            if pos in self.batch_blocks:
                for item in self.batch_blocks[pos]:
                    item.delete()
                self.batch_blocks.pop(pos, None)
            
            neighbor_type = self.blocks.get(pos)
            if neighbor_type:
                self.add_block_to_batch(pos[0], pos[1], pos[2], neighbor_type)

    def rebuild_batch(self):
        for key, items in self.batch_blocks.items():
            for item in items:
                item.delete()
        self.batch_blocks.clear()
        
        for (x, y, z), block_type in self.blocks.items():
            self.add_block_to_batch(x, y, z, block_type)

    def add_block(self, x, y, z, block_type):
        self.blocks[(x, y, z)] = block_type
        self.rebuild_batch()

    def add_block_to_batch(self, x, y, z, block_type):
        t = ('t2f', (0, 0, 1, 0, 1, 1, 0, 1))
        X, Y, Z = x+1, y+1, z+1

        faces = [
            ((x,Y,Z, X,Y,Z, X,Y,z, x,Y,z), (0,1,0), 'top'),  
            ((x,y,z, X,y,z, X,y,Z, x,y,Z), (0,-1,0), 'bottom'), 
            ((x,y,Z, X,y,Z, X,Y,Z, x,Y,Z), (0,0,1), 'side'),  
            ((X,y,z, x,y,z, x,Y,z, X,Y,z), (0,0,-1), 'side'), 
            ((X,y,Z, X,y,z, X,Y,z, X,Y,Z), (1,0,0), 'side'),  
            ((x,y,z, x,y,Z, x,Y,Z, x,Y,z), (-1,0,0), 'side')  
        ]

        key = (int(x), int(y), int(z))
        if key not in self.batch_blocks:
            self.batch_blocks[key] = []

        textures_for_block = self.block_textures.get(block_type)

        for vertices, normal, face_type in faces:
            nx, ny, nz = normal
            if not self.get_block(x + nx, y + ny, z + nz):
                
                group = textures_for_block[face_type]
                
                vertex_list = self.batch.add(4, GL_QUADS, group, ('v3f', vertices), t)
                self.batch_blocks[key].append(vertex_list)

    def draw(self):
        self.batch.draw()


class Player:
    def __init__(self, pos=(0,0,0), rot=(0,0)):
        self.pos = list(pos)
        self.rot = list(rot)

    def mouse_motion(self, dx, dy):
        dx /= 8; dy /= 8
        self.rot[0] += dy
        self.rot[1] -= dx
        if self.rot[0] > 90: self.rot[0] = 90
        elif self.rot[0] < -90: self.rot[0] = -90

    def update(self, dt, keys):
        s = dt * 8
        rotY = -self.rot[1] / 180 * math.pi
        dx, dz = s * math.sin(rotY), s * math.cos(rotY)

        if keys[key.W]: self.pos[0] += dx; self.pos[2] -= dz
        if keys[key.S]: self.pos[0] -= dx; self.pos[2] += dz
        if keys[key.A]: self.pos[0] -= dz; self.pos[2] -= dx
        if keys[key.D]: self.pos[0] += dz; self.pos[2] += dx
        if keys[key.SPACE]: self.pos[1] += s
        if keys[key.LSHIFT]: self.pos[1] -= s

class Window(pyglet.window.Window):

    def setLock(self,state): self.lock = state; self.set_exclusive_mouse(state)
    lock = False; mouse_lock = property(lambda self:self.lock,setLock)
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.keys = key.KeyStateHandler()
        self.push_handlers(self.keys)
        
        self.mouse_lock = False
        pyglet.clock.schedule(self.update)
        
        self.world = World()
        self.player = Player()

    def on_close(self):
        if hasattr(self, 'world') and hasattr(self.world, 'batch'):
            self.world.batch = None
        glBindTexture(GL_TEXTURE_2D, 0)
        glFlush()
        self.close()
        import os
        os._exit(0)

    def on_mouse_motion(self, x, y, dx, dy):
        if self.mouse_lock:
            self.player.mouse_motion(dx, dy)

    def on_key_press(self, KEY, MOD):
        if KEY == key.ESCAPE:
            self.close()
        elif KEY == key.E:
            self.mouse_lock = not self.mouse_lock

    def on_mouse_press(self, x, y, button, modifiers):
        if not self.mouse_lock: return
        
        px, py, pz = self.player.pos
        rot_x = self.player.rot[0] * math.pi / 180.0
        rot_y = self.player.rot[1] * math.pi / 180.0
        
        dx = math.sin(rot_y) * math.cos(rot_x)
        dy = -math.sin(rot_x)
        dz = -math.cos(rot_y) * math.cos(rot_x)
        
        bx, by, bz = int(math.floor(px)), int(math.floor(py)), int(math.floor(pz))
        
        step_x = 1 if dx >= 0 else -1
        step_y = 1 if dy >= 0 else -1
        step_z = 1 if dz >= 0 else -1
        
        delta_x = 1e30 if dx == 0 else abs(1.0 / dx)
        delta_y = 1e30 if dy == 0 else abs(1.0 / dy)
        delta_z = 1e30 if dz == 0 else abs(1.0 / dz)
        
        max_x = (int(math.floor(px)) + 1 - px) * delta_x if dx >= 0 else (px - int(math.floor(px))) * delta_x
        max_y = (int(math.floor(py)) + 1 - py) * delta_y if dy >= 0 else (py - int(math.floor(py))) * delta_y
        max_z = (int(math.floor(pz)) + 1 - pz) * delta_z if dz >= 0 else (pz - int(math.floor(pz))) * delta_z
        
        nx, ny, nz = 0, 0, 0
        hit = False
        
        max_distance = 30.0
        distance = 0.0
        
        while distance < max_distance:
            if max_x < max_y:
                if max_x < max_z:
                    distance = max_x
                    max_x += delta_x
                    bx -= step_x
                    nx, ny, nz = +step_x, 0, 0
                else:
                    distance = max_z
                    max_z += delta_z
                    bz += step_z
                    nx, ny, nz = 0, 0, -step_z
            else:
                if max_y < max_z:
                    distance = max_y
                    max_y += delta_y
                    by -= step_y
                    nx, ny, nz = 0, +step_y, 0
                else:
                    distance = max_z
                    max_z += delta_z
                    bz += step_z
                    nx, ny, nz = 0, 0, -step_z
                    
            if self.world.get_block(bx, by, bz):
                hit = True
                break

        if hit:
            if button == 1:
                self.world.set_block(bx, by, bz, None)
            elif button == 4:
                abx, aby, abz = bx + nx, by + ny, bz + nz
                px_f, py_f, pz_f = int(math.floor(px)), int(math.floor(py)), int(math.floor(pz))
                if not (abx == px_f and aby == py_f and abz == pz_f):
                    self.world.set_block(abx, aby, abz, 2)

    def update(self, dt):
        self.player.update(dt, self.keys)

    def on_draw(self):
        self.clear()
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_CULL_FACE)
        
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(70, self.width/self.height, 0.05, 1000)
        
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glRotatef(-self.player.rot[0], 1, 0, 0)
        glRotatef(-self.player.rot[1], 0, 1, 0)
        glTranslatef(-self.player.pos[0], -self.player.pos[1], -self.player.pos[2])
        
        self.world.draw()


if __name__ == '__main__':
    window = Window(width=854, height=480, caption='Jade Cove v1', resizable=True)
    glClearColor(0.5, 0.7, 1, 1)
    pyglet.app.run()
