#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Comprehensive Minecraft-Inspired Survival Game Prototype

Features:
  • Huge world (500×100) with three dimensions:
       - Overworld: natural terrain (grass, dirt, stone, ores, trees)
       - Nether: a red, hostile environment with lava and obsidian
       - End: a void with End Stone and a boss (Ender Dragon)
  • Only an 80×20 viewport (1,600 tiles) is visible; the camera scrolls.
  • Core items/blocks: grass, dirt, stone, coal_ore, iron_ore, diamond_ore, wood, leaf, obsidian,
    furnace, crafting_table, torch, wood_plank, apple.
  • Crafting system (key C) with sample recipes.
  • Inventory system (key I) with scrollable UI and stacking (max 64 per stack).
  • Health & hunger mechanics.
  • Day-night cycle (brightness adjustment).
  • Basic enemy: a zombie in the Overworld; bosses (Ender Dragon, Wither) in End/Nether.
  • Dimension travel (key T) cycles between Overworld, Nether, and End.
  • Full mouse support:
        - Left-click: break block or place active block.
        - Mouse wheel: cycle active inventory items.
        - Mouse movement: update player orientation.
        
Note: This prototype is very simplified relative to real Minecraft.
"""

import tkinter as tk
from tkinter import ttk
import math, random, time

# --------------------------
# GLOBAL SETTINGS
# --------------------------
# World dimensions (for each dimension)
OVERWORLD_WIDTH = 500
OVERWORLD_HEIGHT = 100

# For simplicity, Nether and End will be smaller.
NETHER_WIDTH = 120
NETHER_HEIGHT = 60
END_WIDTH = 150
END_HEIGHT = 80

# The viewport (visible tiles) is fixed:
VIEWPORT_WIDTH = 80   # columns
VIEWPORT_HEIGHT = 20  # rows
CELL_SIZE = 10        # pixel size (adjust for visibility)

DAY_LENGTH = 120      # seconds for full day cycle
START_TIME = time.time()

STACK_LIMIT = 64

# --------------------------
# BLOCKS & ITEMS (colors as hex strings)
# --------------------------
BLOCKS = {
    "AIR":            "#FFFFFF",
    "GRASS":          "#00AA00",
    "DIRT":           "#8B4513",
    "STONE":          "#808080",
    "COAL_ORE":       "#333333",
    "IRON_ORE":       "#CC6600",
    "DIAMOND_ORE":    "#55FFFF",
    "WOOD":           "#A0522D",
    "LEAF":           "#66CC66",
    "OBSIDIAN":       "#440044",
    "FURNACE":        "#CC6600",
    "CRAFTING_TABLE": "#D2691E",
    "TORCH":          "#FFFF00",
    # Crafted items:
    "wood_plank":     "#DEB887",
    "wooden_pickaxe": "#CD853F",
    "apple":          "#FF0000",
    # Portal block for dimension travel (not craftable in this prototype)
    "PORTAL":         "#0000FF",  # Blue portal block
    # Nether-specific block:
    "NETHERRACK":     "#A52A2A",
    # End Stone:
    "END_STONE":      "#D2B48C",
}
# Only these blocks can be placed:
PLACEABLE = set(["GRASS", "DIRT", "STONE", "COAL_ORE", "IRON_ORE", "DIAMOND_ORE",
                  "WOOD", "LEAF", "OBSIDIAN", "FURNACE", "CRAFTING_TABLE", "TORCH",
                  "wood_plank"])

# --------------------------
# CRAFTING RECIPES (Sample, very simplified)
# --------------------------
crafting_recipes = {
    "wood_plank":      {"WOOD": 1},
    "wooden_pickaxe":  {"wood_plank": 3},
    "furnace":         {"STONE": 8},
    "crafting_table":  {"wood_plank": 4},
    "torch":           {"COAL_ORE": 1, "wood_plank": 1},
    "apple":           {"wood_plank": 1, "TORCH": 1},
}

# --------------------------
# DIMENSION ENUMERATION
# --------------------------
DIMENSIONS = ["overworld", "nether", "end"]

# --------------------------
# CLASSES
# --------------------------
class Player:
    def __init__(self, x, y, dimension="overworld"):
        self.x = x
        self.y = y
        self.health = 100
        self.hunger = 100  # max 100
        self.dimension = dimension  # "overworld", "nether", or "end"
        self.inventory = {key: 0 for key in BLOCKS if key != "AIR"}
        for item in ["wood_plank", "wooden_pickaxe", "crafting_table", "apple", "TORCH"]:
            self.inventory.setdefault(item, 0)
        self.active_item = None   # currently selected item for placement
        self.orientation = 0      # angle in degrees (for possible future rendering)

class Enemy:
    def __init__(self, x, y, kind="zombie"):
        self.x = x
        self.y = y
        self.kind = kind  # "zombie" in Overworld; could be others in Nether/End
        self.health = 20 if kind=="zombie" else 50
        self.symbol = "Z" if kind=="zombie" else "B"  # B for boss (placeholder)

class Game:
    def __init__(self, root):
        self.root = root
        self.root.title("Minecraft-Inspired Survival")
        # Create main canvas with size determined by viewport.
        self.canvas = tk.Canvas(root, width=VIEWPORT_WIDTH*CELL_SIZE,
                                     height=VIEWPORT_HEIGHT*CELL_SIZE, bg="black")
        self.canvas.pack(side=tk.LEFT)
        self.canvas.focus_set()
        # Status label for health, hunger, active item, dimension, day/night.
        self.status_var = tk.StringVar()
        self.status_label = ttk.Label(root, textvariable=self.status_var, font=('Arial', 12))
        self.status_label.pack(side=tk.TOP, fill=tk.X)
        
        # Bind events.
        self.canvas.bind("<KeyPress>", self.on_key)
        self.canvas.bind("<Button-1>", self.on_left_click)
        self.canvas.bind("<Motion>", self.on_mouse_move)
        self.canvas.bind("<MouseWheel>", self.on_mouse_wheel)
        self.canvas.bind("<Button-4>", self.on_mouse_wheel)
        self.canvas.bind("<Button-5>", self.on_mouse_wheel)
        
        # Set up world for the current dimension.
        self.current_dimension = "overworld"
        self.world = self.generate_world(self.current_dimension)
        # The world is stored as a 2D list of blocks.
        self.world_width, self.world_height = self.get_world_dimensions(self.current_dimension)
        
        self.player = Player(self.world_width//2, self.world_height//2, dimension=self.current_dimension)
        self.enemies = []  # list of Enemy instances
        
        self.last_update = time.time()
        self.portal_cooldown = 0  # prevent spam switching dimensions
        
        self.update_game()
        
    # --------------------------
    # WORLD GENERATION
    # --------------------------
    def get_world_dimensions(self, dimension):
        if dimension == "overworld":
            return OVERWORLD_WIDTH, OVERWORLD_HEIGHT
        elif dimension == "nether":
            return NETHER_WIDTH, NETHER_HEIGHT
        elif dimension == "end":
            return END_WIDTH, END_HEIGHT
        else:
            return 80, 20  # fallback

    def generate_world(self, dimension):
        # Generate different worlds based on the dimension.
        width, height = self.get_world_dimensions(dimension)
        world = []
        if dimension == "overworld":
            for y in range(height):
                row = []
                for x in range(width):
                    if y < 40:
                        row.append("AIR")
                    elif y == 40:
                        row.append("GRASS")
                    elif y < 45:
                        row.append("DIRT")
                    else:
                        r = random.random()
                        if r < 0.05:
                            row.append("COAL_ORE")
                        elif r < 0.08:
                            row.append("IRON_ORE")
                        elif r < 0.09:
                            row.append("DIAMOND_ORE")
                        else:
                            row.append("STONE")
                world.append(row)
            # Add trees in several locations.
            for _ in range(50):
                tx = random.randint(0, width-1)
                ty = 40
                if world[ty][tx] == "GRASS":
                    height_tree = random.randint(3, 5)
                    for h in range(1, height_tree+1):
                        if ty - h >= 0:
                            world[ty-h][tx] = "WOOD"
                    for dx in (-1, 0, 1):
                        for dy in (-2, -1):
                            nx, ny = tx+dx, ty-height_tree+dy
                            if 0 <= nx < width and 0 <= ny < height:
                                world[ny][nx] = "LEAF"
        elif dimension == "nether":
            # Nether: mostly NETHERRACK, with scattered OBISIDIAN and lava (simulated as "FURNACE" block here as a placeholder).
            for y in range(height):
                row = []
                for x in range(width):
                    # Use a red-brown tone to represent netherrack.
                    if random.random() < 0.03:
                        row.append("OBSIDIAN")
                    else:
                        row.append("NETHERRACK")
                world.append(row)
            # Place some PORTAL blocks to simulate Nether portal remnants.
            for _ in range(10):
                x = random.randint(0, width-1)
                y = random.randint(0, height-1)
                world[y][x] = "PORTAL"
        elif dimension == "end":
            # End: mostly void with a platform of END_STONE near the bottom.
            for y in range(height):
                row = []
                for x in range(width):
                    if y > height - 8:
                        row.append("END_STONE")
                    else:
                        row.append("AIR")
                world.append(row)
            # Place a portal/boss spawn (Ender Dragon) at the center.
            midx = width // 2
            midy = height - 9
            world[midy][midx] = "PORTAL"
        return world

    # --------------------------
    # VIEWPORT / SCROLLING
    # --------------------------
    def get_camera_offset(self):
        # The viewport shows VIEWPORT_WIDTH x VIEWPORT_HEIGHT tiles from the current world.
        cam_x = self.player.x - VIEWPORT_WIDTH // 2
        cam_y = self.player.y - VIEWPORT_HEIGHT // 2
        cam_x = max(0, min(cam_x, self.world_width - VIEWPORT_WIDTH))
        cam_y = max(0, min(cam_y, self.world_height - VIEWPORT_HEIGHT))
        return cam_x, cam_y

    # --------------------------
    # DRAWING THE WORLD
    # --------------------------
    def draw_world(self):
        self.canvas.delete("all")
        cam_x, cam_y = self.get_camera_offset()
        # Adjust brightness for day-night.
        elapsed = (time.time() - START_TIME) % DAY_LENGTH
        brightness = 1.0 if elapsed <= DAY_LENGTH/2 else 0.5
        for y in range(VIEWPORT_HEIGHT):
            for x in range(VIEWPORT_WIDTH):
                wx = cam_x + x
                wy = cam_y + y
                block = self.world[wy][wx]
                color = BLOCKS.get(block, "#000000")
                if brightness < 1.0:
                    color = self.adjust_color(color, brightness)
                self.canvas.create_rectangle(
                    x*CELL_SIZE, y*CELL_SIZE,
                    (x+1)*CELL_SIZE, (y+1)*CELL_SIZE,
                    fill=color, outline="gray")
        # Draw player as a red circle.
        view_px = self.player.x - cam_x
        view_py = self.player.y - cam_y
        pad = 2
        self.canvas.create_oval(
            view_px*CELL_SIZE+pad, view_py*CELL_SIZE+pad,
            (view_px+1)*CELL_SIZE-pad, (view_py+1)*CELL_SIZE-pad,
            fill="red", outline="white")
        # Draw enemies as black circles.
        for enemy in self.enemies:
            ex, ey = enemy.x, enemy.y
            if cam_x <= ex < cam_x + VIEWPORT_WIDTH and cam_y <= ey < cam_y + VIEWPORT_HEIGHT:
                vx = ex - cam_x
                vy = ey - cam_y
                self.canvas.create_oval(
                    vx*CELL_SIZE+pad, vy*CELL_SIZE+pad,
                    (vx+1)*CELL_SIZE-pad, (vy+1)*CELL_SIZE-pad,
                    fill="black", outline="white")
        self.canvas.update()

    def adjust_color(self, hex_color, factor):
        hex_color = hex_color.lstrip("#")
        rgb = [int(hex_color[i:i+2], 16) for i in (0, 2, 4)]
        rgb = [max(0, int(c*factor)) for c in rgb]
        return "#%02x%02x%02x" % tuple(rgb)

    def update_status(self):
        elapsed = (time.time() - START_TIME) % DAY_LENGTH
        phase = "Day" if elapsed <= DAY_LENGTH/2 else "Night"
        active = self.player.active_item if self.player.active_item else "None"
        self.status_var.set(f"Health: {int(self.player.health)}  Hunger: {int(self.player.hunger)}  Active: {active}  Dim: {self.player.dimension}  {phase}")

    # --------------------------
    # EVENT HANDLERS
    # --------------------------
    def on_key(self, event):
        key = event.keysym.lower()
        if key in ["w", "a", "s", "d"]:
            self.move_player(key)
        elif key == "i":
            self.open_inventory()
        elif key == "c":
            self.open_crafting()
        elif key == "t":
            self.travel_dimension()
        self.draw_world()
        self.update_status()

    def on_left_click(self, event):
        cam_x, cam_y = self.get_camera_offset()
        grid_x = event.x // CELL_SIZE
        grid_y = event.y // CELL_SIZE
        world_x = cam_x + grid_x
        world_y = cam_y + grid_y
        if world_x < 0 or world_x >= self.world_width or world_y < 0 or world_y >= self.world_height:
            return
        # If there is a block, break it.
        if self.world[world_y][world_x] != "AIR":
            blk = self.world[world_y][world_x]
            # For simplicity, if the block is a PORTAL, do nothing.
            if blk == "PORTAL":
                return
            self.world[world_y][world_x] = "AIR"
            cnt = self.player.inventory.get(blk, 0)
            if cnt < STACK_LIMIT:
                self.player.inventory[blk] = cnt + 1
            else:
                print(f"Stack for {blk} is full!")
        else:
            # Place active block if selected.
            if self.player.active_item:
                self.world[world_y][world_x] = self.player.active_item
                cnt = self.player.inventory.get(self.player.active_item, 0)
                if cnt > 0:
                    self.player.inventory[self.player.active_item] = cnt - 1
                    if self.player.inventory[self.player.active_item] == 0:
                        self.player.active_item = None
        self.draw_world()
        self.update_status()

    def on_mouse_move(self, event):
        cam_x, cam_y = self.get_camera_offset()
        grid_x = event.x // CELL_SIZE
        grid_y = event.y // CELL_SIZE
        world_x = cam_x + grid_x
        world_y = cam_y + grid_y
        dx = world_x - self.player.x
        dy = self.player.y - world_y
        if dx == 0 and dy == 0:
            angle = self.player.orientation
        else:
            angle = math.degrees(math.atan2(dy, dx))
        self.player.orientation = angle
        self.update_status()

    def on_mouse_wheel(self, event):
        valid_items = [item for item in PLACEABLE if self.player.inventory.get(item, 0) > 0]
        if not valid_items:
            return
        try:
            idx = valid_items.index(self.player.active_item)
        except ValueError:
            idx = -1
        if event.num == 4 or (hasattr(event, "delta") and event.delta > 0):
            idx = (idx + 1) % len(valid_items)
        else:
            idx = (idx - 1) % len(valid_items)
        self.player.active_item = valid_items[idx]
        self.update_status()

    def move_player(self, key):
        dx, dy = 0, 0
        if key == "w":
            dy = -1
        elif key == "s":
            dy = 1
        elif key == "a":
            dx = -1
        elif key == "d":
            dx = 1
        new_x = self.player.x + dx
        new_y = self.player.y + dy
        if 0 <= new_x < self.world_width and 0 <= new_y < self.world_height:
            if self.world[new_y][new_x] == "AIR":
                self.player.x = new_x
                self.player.y = new_y

    # --------------------------
    # INVENTORY WINDOW
    # --------------------------
    def open_inventory(self):
        inv_win = tk.Toplevel(self.root)
        inv_win.title("Inventory")
        inv_win.geometry("300x400")
        lbl = ttk.Label(inv_win, text="Click an item to select it for placement", font=('Arial', 12))
        lbl.pack(pady=10)
        frame = ttk.Frame(inv_win)
        frame.pack(fill=tk.BOTH, expand=True)
        canvas = tk.Canvas(frame)
        scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
        scroll_frame = ttk.Frame(canvas)
        scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        for item, count in self.player.inventory.items():
            btn = ttk.Button(scroll_frame, text=f"{item}: {count}",
                             command=lambda i=item: self.select_inventory(i, inv_win))
            btn.pack(fill=tk.X, padx=5, pady=2)

    def select_inventory(self, item, window):
        if item in PLACEABLE and self.player.inventory.get(item, 0) > 0:
            self.player.active_item = item
        else:
            self.player.active_item = None
        window.destroy()
        self.update_status()

    # --------------------------
    # CRAFTING WINDOW
    # --------------------------
    def open_crafting(self):
        craft_win = tk.Toplevel(self.root)
        craft_win.title("Crafting")
        craft_win.geometry("300x400")
        lbl = ttk.Label(craft_win, text="Select a recipe to craft", font=('Arial', 12))
        lbl.pack(pady=10)
        frame = ttk.Frame(craft_win)
        frame.pack(fill=tk.BOTH, expand=True)
        for item, req in crafting_recipes.items():
            if all(self.player.inventory.get(ing, 0) >= amt for ing, amt in req.items()):
                req_str = ", ".join(f"{ing}:{amt}" for ing, amt in req.items())
                btn = ttk.Button(frame, text=f"Craft {item} (requires: {req_str})",
                                 command=lambda i=item: self.craft_item(i, craft_win))
                btn.pack(fill=tk.X, padx=5, pady=2)
        close_btn = ttk.Button(frame, text="Close", command=craft_win.destroy)
        close_btn.pack(pady=10)

    def craft_item(self, item, win):
        recipe = crafting_recipes.get(item)
        if not recipe:
            return
        if all(self.player.inventory.get(ing, 0) >= amt for ing, amt in recipe.items()):
            for ing, amt in recipe.items():
                self.player.inventory[ing] -= amt
            cur = self.player.inventory.get(item, 0)
            self.player.inventory[item] = min(cur + 1, STACK_LIMIT)
        win.destroy()
        self.update_status()

    # --------------------------
    # DIMENSION TRAVEL
    # --------------------------
    def travel_dimension(self):
        # For simplicity: cycle dimensions: overworld->nether->end->overworld
        if self.portal_cooldown > time.time():
            return  # Prevent rapid switching.
        current = self.player.dimension
        next_dim = DIMENSIONS[(DIMENSIONS.index(current)+1) % len(DIMENSIONS)]
        self.player.dimension = next_dim
        # Generate new world for that dimension.
        self.world = self.generate_world(next_dim)
        self.world_width, self.world_height = self.get_world_dimensions(next_dim)
        # Place player at center of new world.
        self.player.x = self.world_width // 2
        self.player.y = self.world_height // 2
        # Clear enemies; spawn new ones as needed.
        self.enemies = []
        self.portal_cooldown = time.time() + 3  # 3-second cooldown
        # For bosses: in End, spawn an Ender Dragon; in Nether, spawn a Wither.
        if next_dim == "end":
            # For simplicity, add one boss enemy at center.
            self.enemies.append(Enemy(self.world_width//2, self.world_height//2, kind="ender_dragon"))
        elif next_dim == "nether":
            self.enemies.append(Enemy(self.world_width//2, self.world_height//2, kind="wither"))
        self.update_status()
        self.draw_world()

    # --------------------------
    # GAME LOOP & SURVIVAL MECHANICS
    # --------------------------
    def update_game(self):
        now = time.time()
        delta = now - self.last_update
        self.last_update = now

        # Update hunger
        self.player.hunger -= 0.05 * delta
        if self.player.hunger < 0: self.player.hunger = 0
        # Regenerate health if hunger is high; otherwise, damage.
        if self.player.hunger > 80 and self.player.health < 100:
            self.player.health += 0.02 * delta
            if self.player.health > 100: self.player.health = 100
        if self.player.hunger < 20:
            self.player.health -= 0.05 * delta

        # Basic enemy spawning in Overworld only.
        if self.player.dimension == "overworld" and random.random() < 0.01 and len(self.enemies) < 5:
            ex = random.randint(0, self.world_width-1)
            ey = random.randint(45, self.world_height-1)
            if abs(ex - self.player.x) > 5 and abs(ey - self.player.y) > 5:
                self.enemies.append(Enemy(ex, ey, kind="zombie"))

        # Update enemy movement.
        for enemy in self.enemies:
            if enemy.x < self.player.x and self.world[enemy.y][min(enemy.x+1, self.world_width-1)] == "AIR":
                enemy.x += 1
            elif enemy.x > self.player.x and self.world[enemy.y][max(enemy.x-1, 0)] == "AIR":
                enemy.x -= 1
            if enemy.y < self.player.y and self.world[min(enemy.y+1, self.world_height-1)][enemy.x] == "AIR":
                enemy.y += 1
            elif enemy.y > self.player.y and self.world[max(enemy.y-1, 0)][enemy.x] == "AIR":
                enemy.y -= 1
            if abs(enemy.x - self.player.x) <= 1 and abs(enemy.y - self.player.y) <= 1:
                self.player.health -= 2 * delta

        if self.player.health <= 0:
            self.game_over()
            return

        self.draw_world()
        self.update_status()
        self.root.after(100, self.update_game)

    def game_over(self):
        self.canvas.delete("all")
        self.canvas.create_text(VIEWPORT_WIDTH*CELL_SIZE//2, VIEWPORT_HEIGHT*CELL_SIZE//2,
                                 text="GAME OVER", fill="red", font=('Arial', 32))
        self.status_var.set("Game Over")

# --------------------------
# MAIN FUNCTION
# --------------------------
def main():
    root = tk.Tk()
    game = Game(root)
    root.mainloop()

if __name__ == "__main__":
    main()