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

Upgrades applied:
  • Map enlarged to 1000×200 (Overworld)
  • Mob spawn cap set to 10
  • Safe-callback wrapper around all Tkinter binds
  • True LAN multiplayer (automatic host/join, unlimited players)
  • Block place/break synchronization over LAN
  • Inventory UI (opens with E), click to select active block
"""

import tkinter as tk
from tkinter import ttk
import math, random, time
import socket, threading, pickle

# --------------------------
# GLOBAL SETTINGS
# --------------------------
OVERWORLD_WIDTH = 1000
OVERWORLD_HEIGHT = 200
NETHER_WIDTH    = 120
NETHER_HEIGHT   = 60
END_WIDTH       = 150
END_HEIGHT      = 80

VIEWPORT_WIDTH  = 80
VIEWPORT_HEIGHT = 20
CELL_SIZE       = 10

DAY_LENGTH   = 120
START_TIME   = time.time()
STACK_LIMIT  = 64
MAX_MOBS     = 10
NETWORK_PORT = 50000

# --------------------------
# BLOCKS & ITEMS (colors)
# --------------------------
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",
    "wood_plank":     "#DEB887",
    "wooden_pickaxe": "#CD853F",
    "apple":          "#FF0000",
    "PORTAL":         "#0000FF",
    "NETHERRACK":     "#A52A2A",
    "END_STONE":      "#D2B48C",
}
PLACEABLE = set(BLOCKS) - {"AIR"}

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},
}

DIMENSIONS = ["overworld","nether","end"]

# ─── Safe-callback decorator ───
def safe_tk(fn):
    def wrapper(event):
        try:
            return fn(event)
        except Exception as e:
            print(f"[Tk error in {fn.__name__}]:", e)
    return wrapper
# ───────────────────────────────

class Player:
    def __init__(self, x, y, dimension="overworld"):
        self.x = x; self.y = y
        self.health = 100; self.hunger = 100
        self.dimension = dimension
        self.inventory = {k:0 for k in BLOCKS if k!="AIR"}
        for it in ["wood_plank","wooden_pickaxe","crafting_table","apple","TORCH"]:
            self.inventory.setdefault(it,0)
        self.active_item = None
        self.orientation = 0
        self.pid = random.randint(1,1<<30)
        c = self.pid & 0xFFFFFF
        self.color = f"#{c:06x}"

class Enemy:
    def __init__(self, x, y, kind="zombie"):
        self.x = x; self.y = y
        self.kind = kind
        self.health = 20 if kind=="zombie" else 50

class Game:
    def __init__(self, root):
        self.root = root
        root.title("Minecraft-Inspired Survival")

        # Canvas setup
        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()

        # Bind events
        self.canvas.bind("<KeyPress>",   safe_tk(self.on_key))
        self.canvas.bind("<Button-1>",   safe_tk(self.on_left_click))
        self.canvas.bind("<Motion>",     safe_tk(self.on_mouse_move))
        self.canvas.bind("<MouseWheel>", safe_tk(self.on_mouse_wheel))
        self.canvas.bind("<Button-4>",   safe_tk(self.on_mouse_wheel))
        self.canvas.bind("<Button-5>",   safe_tk(self.on_mouse_wheel))

        # Status label
        self.status_var = tk.StringVar()
        ttk.Label(root, textvariable=self.status_var).pack(fill=tk.X)

        # World setup
        self.current_dimension = "overworld"
        self.world = self.generate_world(self.current_dimension)
        self.world_width, self.world_height = self.get_world_dimensions(self.current_dimension)

        # Player & enemies
        cx, cy = self.world_width//2, self.world_height//2
        self.player = Player(cx, cy, self.current_dimension)
        self.enemies = []

        # Multiplayer state
        self.players = {self.player.pid: self.player}
        self.sock = None
        self.is_host = False
        self.pending_block_changes = []
        self.start_network()

        # Game loop
        self.last_update = time.time()
        self.update_game()

    # ─── Inventory UI methods ───
    def open_inventory(self):
        inv = tk.Toplevel(self.root)
        inv.title("Inventory")
        inv.geometry("300x400")
        ttk.Label(inv, text="Click an item to select for placement", font=('Arial',12)).pack(pady=10)
        frame = ttk.Frame(inv); frame.pack(fill=tk.BOTH, expand=True)
        c = tk.Canvas(frame); sb = ttk.Scrollbar(frame, orient="vertical", command=c.yview)
        inner = ttk.Frame(c)
        inner.bind("<Configure>", lambda e: c.configure(scrollregion=c.bbox("all")))
        c.create_window((0,0), window=inner, anchor="nw")
        c.configure(yscrollcommand=sb.set)
        c.pack(side=tk.LEFT, fill=tk.BOTH, expand=True); sb.pack(side=tk.RIGHT, fill=tk.Y)
        for item, count in self.player.inventory.items():
            btn = ttk.Button(inner, text=f"{item}: {count}",
                             command=lambda i=item: self.select_inventory(i, inv))
            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()
    # ──────────────────────────────

    # ─── Networking methods (unchanged except block sync) ───
    def start_network(self):
        udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        udp.settimeout(0.5)
        udp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)
        udp.bind(('', NETWORK_PORT))
        try:
            data,addr = udp.recvfrom(1024)
            if data==b"MCHOST":
                self.connect_host(addr[0]); return
        except socket.timeout: pass
        self.is_host = True
        threading.Thread(target=self._broadcast, daemon=True).start()
        threading.Thread(target=self._serve, daemon=True).start()

    def _broadcast(self):
        b=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
        b.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1)
        for _ in range(5):
            b.sendto(b"MCHOST",('<broadcast>',NETWORK_PORT))
            time.sleep(0.2)

    def _serve(self):
        s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        s.bind(('',NETWORK_PORT)); s.listen()
        while True:
            conn,_=s.accept()
            threading.Thread(target=self._handle_client,args=(conn,),daemon=True).start()

    def _handle_client(self,conn):
        while True:
            data=conn.recv(4096)
            if not data: break
            st=pickle.loads(data)
            if 'block_change' in st:
                ch=st['block_change']
                self.world[ch['y']][ch['x']]=ch['block']
                self.pending_block_changes.append(ch)
            pid,pd=st['pid'],st['ply']
            p=self.players.get(pid) or Player(pd['x'],pd['y'],pd['dim'])
            p.x,p.y,p.dimension=pd['x'],pd['y'],pd['dim']
            self.players[pid]=p
            snap={'players':{pid:{'x':pl.x,'y':pl.y,'dim':pl.dimension}
                             for pid,pl in self.players.items()},
                  'enemies':[(e.x,e.y) for e in self.enemies],
                  'block_changes': self.pending_block_changes}
            self.pending_block_changes=[]
            conn.sendall(pickle.dumps(snap))

    def connect_host(self,ip):
        c=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        c.connect((ip,NETWORK_PORT)); self.sock=c
        threading.Thread(target=self._talk_server,daemon=True).start()

    def _talk_server(self):
        while True:
            state={'pid':self.player.pid,
                   'ply':{'x':self.player.x,'y':self.player.y,'dim':self.player.dimension}}
            self.sock.sendall(pickle.dumps(state))
            data=self.sock.recv(65536)
            if not data: break
            snap=pickle.loads(data)
            for ch in snap.get('block_changes',[]):
                self.world[ch['y']][ch['x']] = ch['block']
            for pid,pd in snap['players'].items():
                if pid==self.player.pid: continue
                p=self.players.get(pid) or Player(pd['x'],pd['y'],pd['dim'])
                p.x,p.y,p.dimension=pd['x'],pd['y'],pd['dim']
                self.players[pid]=p
            self.enemies=[Enemy(x,y) for x,y in snap['enemies']]
            time.sleep(0.1)
    # ───────────────────────────────────────────────────────

    # --------------------------
    # WORLD GENERATION
    # --------------------------
    def get_world_dimensions(self,dim):
        if dim=="overworld": return OVERWORLD_WIDTH,OVERWORLD_HEIGHT
        if dim=="nether":    return NETHER_WIDTH,NETHER_HEIGHT
        if dim=="end":       return END_WIDTH,END_HEIGHT
        return 80,20

    def generate_world(self,dim):
        w,h=self.get_world_dimensions(dim)
        grid=[]
        if dim=="overworld":
            for y in range(h):
                row=[]
                for x in range(w):
                    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")
                grid.append(row)
            for _ in range(50):
                tx=random.randint(0,w-1); ty=40
                if grid[ty][tx]=="GRASS":
                    th=random.randint(3,5)
                    for i in range(th): grid[ty-i-1][tx]="WOOD"
                    for dx in(-1,0,1):
                        for dy in(-2,-1):
                            nx,ny=tx+dx,ty-th+dy
                            if 0<=nx<w and 0<=ny<h: grid[ny][nx]="LEAF"
        elif dim=="nether":
            for y in range(h): grid.append(["NETHERRACK"]*w)
            for _ in range(10):
                x=random.randint(0,w-1); y=random.randint(0,h-1)
                grid[y][x]="PORTAL"
        else:
            for y in range(h):
                grid.append(["END_STONE" if y>h-8 else "AIR"]*w)
            mx,my=w//2,h-9; grid[my][mx]="PORTAL"
        return grid

    # --------------------------
    # VIEWPORT & DRAWING
    # --------------------------
    def get_camera_offset(self):
        cx=self.player.x-VIEWPORT_WIDTH//2
        cy=self.player.y-VIEWPORT_HEIGHT//2
        cx=max(0,min(cx,self.world_width-VIEWPORT_WIDTH))
        cy=max(0,min(cy,self.world_height-VIEWPORT_HEIGHT))
        return cx,cy

    def draw_world(self):
        self.canvas.delete("all")
        cx,cy=self.get_camera_offset()
        # draw tiles …
        for y in range(VIEWPORT_HEIGHT):
            for x in range(VIEWPORT_WIDTH):
                bx,by=cx+x,cy+y
                clr=BLOCKS.get(self.world[by][bx],"#000000")
                elapsed=(time.time()-START_TIME)%DAY_LENGTH
                if elapsed>DAY_LENGTH/2: clr=self.adjust_color(clr,0.5)
                self.canvas.create_rectangle(x*CELL_SIZE,y*CELL_SIZE,
                                             (x+1)*CELL_SIZE,(y+1)*CELL_SIZE,
                                             fill=clr,outline="gray")
        pad=2
        # draw enemies …
        for e in self.enemies:
            ex,ey=e.x-cx,e.y-cy
            if 0<=ex<VIEWPORT_WIDTH and 0<=ey<VIEWPORT_HEIGHT:
                self.canvas.create_oval(ex*CELL_SIZE+pad,ey*CELL_SIZE+pad,
                                        (ex+1)*CELL_SIZE-pad,(ey+1)*CELL_SIZE-pad,
                                        fill="black",outline="white")
        # draw all players …
        for p in self.players.values():
            px,py=p.x-cx,p.y-cy
            if 0<=px<VIEWPORT_WIDTH and 0<=py<VIEWPORT_HEIGHT:
                self.canvas.create_oval(px*CELL_SIZE+pad,py*CELL_SIZE+pad,
                                        (px+1)*CELL_SIZE-pad,(py+1)*CELL_SIZE-pad,
                                        fill=p.color,outline="white")
        # draw self on top …
        px,py=self.player.x-cx,self.player.y-cy
        self.canvas.create_oval(px*CELL_SIZE+pad,py*CELL_SIZE+pad,
                                (px+1)*CELL_SIZE-pad,(py+1)*CELL_SIZE-pad,
                                fill="red",outline="white")
        self.canvas.update()

    def adjust_color(self,hex_color,factor):
        rgb=[int(hex_color[i:i+2],16) for i in (1,3,5)]
        return "#%02x%02x%02x"%tuple(int(c*factor) for c in rgb)

    # --------------------------
    # EVENT HANDLERS
    # --------------------------
    def on_key(self,event):
        k=event.keysym.lower()
        if k in("w","a","s","d"):
            dx=(k=="d")-(k=="a"); dy=(k=="s")-(k=="w")
            self._move(self.player,dx,dy)
        elif k=="t":
            self._switch_dim()
        elif k=="e":                                # Inventory key ───
            self.open_inventory()
        self.draw_world()
        self.update_status()

    def on_left_click(self,event):
        cam_x,cam_y=self.get_camera_offset()
        gx,gy=cam_x+event.x//CELL_SIZE,cam_y+event.y//CELL_SIZE
        if 0<=gx<self.world_width and 0<=gy<self.world_height:
            if self.world[gy][gx]!="AIR":
                self.world[gy][gx]="AIR"
                # add to inventory
                blk = self.world[gy][gx]
                cnt = self.player.inventory.get(blk,0)
                if cnt<STACK_LIMIT: self.player.inventory[blk]=cnt+1
            elif self.player.active_item:
                self.world[gy][gx]=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
            change={'x':gx,'y':gy,'block':self.world[gy][gx]}
            if self.is_host:
                self.pending_block_changes.append(change)
            elif self.sock:
                try: self.sock.sendall(pickle.dumps({'block_change':change}))
                except: pass
        self.draw_world()
        self.update_status()

    def on_mouse_move(self,event): pass
    def on_mouse_wheel(self,event): pass

    def _move(self,p,dx,dy):
        w,h=self.get_world_dimensions(p.dimension)
        nx,ny=p.x+dx,p.y+dy
        if 0<=nx<w and 0<=ny<h and self.world[ny][nx]=="AIR":
            p.x,p.y=nx,ny

    def _switch_dim(self):
        idx=DIMENSIONS.index(self.player.dimension)
        nd=DIMENSIONS[(idx+1)%3]
        self.player.dimension=nd
        self.world=self.generate_world(nd)
        self.world_width,self.world_height=self.get_world_dimensions(nd)
        self.player.x,self.player.y=self.world_width//2,self.world_height//2

    # --------------------------
    # GAME LOOP
    # --------------------------
    def update_game(self):
        now=time.time(); dt=now-self.last_update; self.last_update=now
        if (self.player.dimension=="overworld"
            and len(self.enemies)<MAX_MOBS
            and random.random()<0.01):
            x=random.randint(0,self.world_width-1)
            y=random.randint(45,self.world_height-1)
            if abs(x-self.player.x)>5 and abs(y-self.player.y)>5:
                self.enemies.append(Enemy(x,y))
        for e in self.enemies:
            dx=(1 if e.x<self.player.x else -1 if e.x>self.player.x else 0)
            dy=(1 if e.y<self.player.y else -1 if e.y>self.player.y else 0)
            self._move(e,dx,dy)
        self.draw_world()
        self.root.after(100,self.update_game)

    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 or "None"
        self.status_var.set(
            f"Health:{int(self.player.health)} Hunger:{int(self.player.hunger)}"
            f" Active:{active} Dim:{self.player.dimension} {phase}"
        )

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

if __name__=="__main__":
    main()