import tkinter as tk
import math, random, time, threading, socket, sys

# ---------------- Configuration Constants ----------------
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
# Map dimensions in tiles
MAP_WIDTH = 300
MAP_HEIGHT = 300
BASE_TILE_SIZE = 16  # Base tile size at zoom=1
DEFAULT_VIEW_TILES_W = 60
DEFAULT_VIEW_TILES_H = 40
FPS = 20
ZOOM_STEP = 1.1

# Network defaults (for LAN multiplayer)
NET_PORT = 50007
# Use localhost for testing; change as necessary.
NET_HOST = "127.0.0.1"

# ---------------- Map Generation & 2.5D Terrain ----------------
def generate_map(width, height):
    # Tiles: '.' = ground, 'M' = mountain, 'B' = building, 'K' = bunker, 'o' = crater
    grid = []
    for y in range(height):
        row = []
        for x in range(width):
            r = random.random()
            if r < 0.04:
                row.append('M')
            elif r < 0.08:
                row.append('B')
            elif r < 0.10:
                row.append('K')
            elif r < 0.12:
                row.append('o')
            else:
                row.append('.')
        grid.append(row)
    return grid

# ---------------- Tank Models (ASCII Art) ----------------
PLAYER_MODEL = [
    "   ____  ",
    "  /____\\ ",
    " | M4  | ",
    " |_____| "
]
ENEMY_MODEL = [
    "   ____  ",
    "  /____\\ ",
    " |T-34 | ",
    " |_____| "
]
# Infantry will be drawn as a single character "I"

# ---------------- Helper Functions ----------------
def angle_between(x1, y1, x2, y2):
    return math.degrees(math.atan2(y2 - y1, x2 - x1))

def distance(x1, y1, x2, y2):
    return math.hypot(x2 - x1, y2 - y1)

# ---------------- Classes for Tanks, Shells, Explosions ----------------
class Tank:
    def __init__(self, tank_id, country, name, model, max_health, speed, max_ammo, x, y, is_player=False):
        self.id = tank_id
        self.country = country
        self.name = name
        self.model = model  # list of strings
        self.max_health = max_health
        self.health = max_health
        self.speed = speed
        self.max_ammo = max_ammo
        self.ammo = max_ammo
        self.x = x  # tile coordinate (float)
        self.y = y
        self.turret_angle = 0  # in degrees; updated via mouse for player, auto for enemy
        self.is_player = is_player
        self.last_shot_time = 0
        self.fire_delay = 0.5  # seconds between shots
        self.kills = 0

    def alive(self):
        return self.health > 0

    def is_damaged(self):
        return self.health < self.max_health * 0.5

class Shell:
    def __init__(self, x, y, angle, shell_type, owner_id):
        self.x = x
        self.y = y
        self.angle = angle  # in degrees
        self.shell_type = shell_type  # 'AP' or 'HE'
        self.owner_id = owner_id
        self.speed = 3.0
        self.damage = 40 if shell_type == 'AP' else 20

    def update(self):
        rad = math.radians(self.angle)
        self.x += math.cos(rad) * self.speed
        self.y += math.sin(rad) * self.speed

class Explosion:
    FRAMES = [
        "   ░   ",
        "  ▒░▒  ",
        " ▒▓▓▓▒ ",
        "▒▓██▓▒",
        " ▒▓▓▓▒ ",
        "  ▒░▒  ",
        "   ░   "
    ]
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.frame_index = 0
        self.frame_delay = 1
        self.frame_timer = 0

    def update(self):
        self.frame_timer += 1
        if self.frame_timer >= self.frame_delay:
            self.frame_timer = 0
            self.frame_index += 1

    def finished(self):
        return self.frame_index >= len(self.FRAMES)

    def current_frame(self):
        if self.frame_index < len(self.FRAMES):
            return self.FRAMES[self.frame_index]
        return ""

# ---------------- Network Code ----------------
# A minimal LAN multiplayer implementation.
# In this simple prototype, each client sends its player state to the other.
# For brevity, we implement a simple TCP connection that sends a text message every frame:
# Format: "ID;x;y;turret_angle;health;ammo;kills"
class NetworkHandler(threading.Thread):
    def __init__(self, game, is_host, host_ip=NET_HOST):
        super().__init__(daemon=True)
        self.game = game
        self.is_host = is_host
        self.host_ip = host_ip
        self.sock = None
        self.running = True

    def run(self):
        if self.is_host:
            self.run_server()
        else:
            self.run_client()

    def run_server(self):
        # Server listens and accepts one connection
        server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_sock.bind(("", NET_PORT))
        server_sock.listen(1)
        self.sock, addr = server_sock.accept()
        print(f"Client connected from {addr}")
        while self.running:
            try:
                data = self.sock.recv(1024).decode()
                if data:
                    self.process_message(data)
                # Send our player state periodically
                msg = self.game.serialize_state()
                self.sock.sendall(msg.encode())
            except Exception as e:
                print("Server network error:", e)
                break
        server_sock.close()

    def run_client(self):
        # Client connects to server
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.sock.connect((self.host_ip, NET_PORT))
            print("Connected to server.")
        except Exception as e:
            print("Unable to connect to server:", e)
            return
        while self.running:
            try:
                data = self.sock.recv(1024).decode()
                if data:
                    self.process_message(data)
                msg = self.game.serialize_state()
                self.sock.sendall(msg.encode())
            except Exception as e:
                print("Client network error:", e)
                break
        self.sock.close()

    def process_message(self, message):
        # Process a state message, e.g., update enemy tank state.
        # Expected format: "ID;x;y;turret_angle;health;ammo;kills"
        try:
            parts = message.strip().split(";")
            if len(parts) != 7:
                return
            pid, x, y, t_angle, health, ammo, kills = parts
            x = float(x)
            y = float(y)
            t_angle = float(t_angle)
            health = float(health)
            ammo = int(ammo)
            kills = int(kills)
            # If the received state is not for our own player, update enemy state.
            if pid != self.game.player.id:
                # If enemy does not exist, create one
                if pid not in self.game.tanks:
                    self.game.tanks[pid] = Tank(pid, "USSR", "T-34", ENEMY_MODEL, 95, 1.0, 5, x, y, is_player=False)
                enemy = self.game.tanks[pid]
                enemy.x = x
                enemy.y = y
                enemy.turret_angle = t_angle
                enemy.health = health
                enemy.ammo = ammo
                enemy.kills = kills
        except Exception as e:
            print("Error processing message:", e)

# ---------------- Main Game Class Using Tkinter ----------------
class TankGameApp:
    def __init__(self, root, multiplayer=False, host_mode=False, host_ip=NET_HOST):
        self.root = root
        self.multiplayer = multiplayer
        self.host_mode = host_mode  # True if hosting, False if joining
        self.root.title("ASCII Tank Game – LAN Edition")
        self.canvas = tk.Canvas(root, width=WINDOW_WIDTH, height=WINDOW_HEIGHT, bg="black")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.ui_panel = tk.Label(root, text="", font=("Courier", 16), bg="gray20", fg="white")
        self.ui_panel.pack(fill=tk.X)

        self.zoom = 1.0
        self.cam_x = 0
        self.cam_y = 0
        # Generate the map
        self.map = generate_map(MAP_WIDTH, MAP_HEIGHT)
        # Create player tank at center
        self.player = Tank("PLAYER", "USA", "m4 Sherman", PLAYER_MODEL, 100, 1.0, 5, MAP_WIDTH/2, MAP_HEIGHT/2, is_player=True)
        # For multiplayer, enemy state will be updated via network.
        # For offline mode, create a default enemy.
        self.tanks = {self.player.id: self.player}
        if not self.multiplayer:
            enemy = Tank("ENEMY", "USSR", "T-34", ENEMY_MODEL, 95, 1.0, 5, MAP_WIDTH/2 + 30, MAP_HEIGHT/2, is_player=False)
            self.tanks[enemy.id] = enemy
        else:
            # In multiplayer, if hosting, we keep enemy empty; if joining, our enemy state will be received.
            if self.host_mode:
                # As host, create a placeholder enemy that we update
                enemy = Tank("ENEMY", "USSR", "T-34", ENEMY_MODEL, 95, 1.0, 5, MAP_WIDTH/2 + 30, MAP_HEIGHT/2, is_player=False)
                self.tanks[enemy.id] = enemy

        self.shells = []
        self.explosions = []
        self.last_time = time.time()
        self.FPS = FPS

        self.mouse_x = 0
        self.mouse_y = 0

        # Bind mouse and keyboard events
        self.canvas.bind("<Motion>", self.on_mouse_move)
        self.canvas.bind("<Button-1>", self.on_mouse_click)
        self.root.bind("<Key>", self.on_key_press)
        self.root.bind("<MouseWheel>", self.on_mouse_wheel)

        # If in multiplayer, start network handler in a separate thread
        if self.multiplayer:
            self.net_handler = NetworkHandler(self, self.host_mode, host_ip)
            self.net_handler.start()

        self.game_loop()
    
    # --------------- Event Handlers ---------------
    def on_key_press(self, event):
        if event.keysym.lower() == "w":
            self.player.y -= self.player.speed
        elif event.keysym.lower() == "s":
            self.player.y += self.player.speed
        elif event.keysym.lower() == "a":
            self.player.x -= self.player.speed
        elif event.keysym.lower() == "d":
            self.player.x += self.player.speed
        elif event.char == " ":
            self.shoot(self.player)
        elif event.char == "+":
            self.zoom *= ZOOM_STEP
        elif event.char == "-":
            self.zoom /= ZOOM_STEP
        self.clamp_player()
        self.update_camera()
        self.draw()

    def on_mouse_move(self, event):
        self.mouse_x = event.x
        self.mouse_y = event.y
        view_center_x = WINDOW_WIDTH / 2
        view_center_y = WINDOW_HEIGHT / 2
        dx = event.x - view_center_x
        dy = event.y - view_center_y
        self.player.turret_angle = math.degrees(math.atan2(dy, dx))
        self.draw()

    def on_mouse_click(self, event):
        dx = event.x - (WINDOW_WIDTH/2)
        dy = event.y - (WINDOW_HEIGHT/2)
        angle = math.degrees(math.atan2(dy, dx))
        self.player.turret_angle = angle
        self.shoot(self.player)
        self.draw()

    def on_mouse_wheel(self, event):
        if event.delta > 0:
            self.zoom *= ZOOM_STEP
        else:
            self.zoom /= ZOOM_STEP
        self.draw()

    def clamp_player(self):
        self.player.x = max(0, min(MAP_WIDTH-1, self.player.x))
        self.player.y = max(0, min(MAP_HEIGHT-1, self.player.y))

    def update_camera(self):
        self.cam_x = self.player.x - (DEFAULT_VIEW_TILES_W / 2)
        self.cam_y = self.player.y - (DEFAULT_VIEW_TILES_H / 2)
        self.cam_x = max(0, min(MAP_WIDTH - DEFAULT_VIEW_TILES_W, self.cam_x))
        self.cam_y = max(0, min(MAP_HEIGHT - DEFAULT_VIEW_TILES_H, self.cam_y))

    # --------------- Game Logic ---------------
    def shoot(self, tank):
        now = time.time()
        if now - tank.last_shot_time >= tank.fire_delay and tank.ammo > 0:
            shell = Shell(tank.x, tank.y, tank.turret_angle, "AP", tank.id)
            self.shells.append(shell)
            tank.ammo -= 1
            tank.last_shot_time = now

    def update_game(self, dt):
        shells_to_remove = []
        for shell in self.shells:
            shell.update()
            if shell.x < 0 or shell.x >= MAP_WIDTH or shell.y < 0 or shell.y >= MAP_HEIGHT:
                shells_to_remove.append(shell)
                continue
            # Check collision with enemy tank (simplified)
            for tid, tank in self.tanks.items():
                if tid == shell.owner_id or not tank.alive():
                    continue
                if distance(shell.x, shell.y, tank.x, tank.y) < 2:
                    tank.health -= shell.damage
                    shells_to_remove.append(shell)
                    self.explosions.append(Explosion(shell.x, shell.y))
                    break
        for s in shells_to_remove:
            if s in self.shells:
                self.shells.remove(s)
        
        explosions_to_remove = []
        for exp in self.explosions:
            exp.update()
            if exp.finished():
                explosions_to_remove.append(exp)
        for exp in explosions_to_remove:
            if exp in self.explosions:
                self.explosions.remove(exp)
        
        # Update enemy behavior (for offline and host multiplayer, simple auto-targeting)
        if "ENEMY" in self.tanks and self.tanks["ENEMY"].alive():
            enemy = self.tanks["ENEMY"]
            dx = self.player.x - enemy.x
            dy = self.player.y - enemy.y
            enemy.turret_angle = math.degrees(math.atan2(dy, dx))
            # Enemy auto-shooting if in range
            if distance(self.player.x, self.player.y, enemy.x, enemy.y) < 20 and time.time() - enemy.last_shot_time > 2:
                self.shoot(enemy)
                enemy.last_shot_time = time.time()
    
    def serialize_state(self):
        # Serialize player's state as: "ID;x;y;turret_angle;health;ammo;kills\n"
        return f"{self.player.id};{self.player.x};{self.player.y};{self.player.turret_angle};{self.player.health};{self.player.ammo};{self.player.kills}\n"

    # --------------- Drawing Functions ---------------
    def draw(self):
        self.canvas.delete("all")
        tile_size = BASE_TILE_SIZE * self.zoom
        # For a 2.5D effect, we tilt the map: offset each row slightly based on y.
        tilt_offset = 4 * self.zoom
        
        # Draw background (map) with gradient shading (simulate fake shadows/lighting)
        for j in range(DEFAULT_VIEW_TILES_H):
            for i in range(DEFAULT_VIEW_TILES_W):
                map_x = int(self.cam_x) + i
                map_y = int(self.cam_y) + j
                if 0 <= map_x < MAP_WIDTH and 0 <= map_y < MAP_HEIGHT:
                    tile = self.map[map_y][map_x]
                    # Adjust brightness: lower rows appear darker.
                    if tile == '.':
                        color = "#88cc88"
                    elif tile == 'M':
                        color = "#aaaaaa"
                    elif tile == 'B':
                        color = "#ffcc00"
                    elif tile == 'K':
                        color = "#555555"
                    elif tile == 'o':
                        color = "#664422"
                    else:
                        color = "#88cc88"
                    x_px = i * tile_size + (j * tilt_offset * 0.2)
                    y_px = j * tile_size - (j * 0.5)
                    self.canvas.create_text(x_px, y_px, anchor="nw",
                                            text=tile, fill=color,
                                            font=("Courier", int(12 * self.zoom)))
        
        # Draw all tanks with perspective scaling
        for tank in self.tanks.values():
            d = distance(self.player.x, self.player.y, tank.x, tank.y)
            screen_x = (tank.x - self.cam_x) * tile_size
            screen_y = (tank.y - self.cam_y) * tile_size
            # For additional 2.5D feel, offset the tank drawing based on its y (simulate depth)
            screen_x += (tank.y - self.cam_y) * (tilt_offset * 0.2)
            if d > 10:
                # Far: draw as a single, small character
                self.canvas.create_text(screen_x, screen_y, text="T",
                                        fill="cyan" if tank.is_player else "magenta",
                                        font=("Courier", max(8, int(12 * self.zoom * 0.5))))
            else:
                # Near: draw full model
                model = tank.model
                for idx, line in enumerate(model):
                    self.canvas.create_text(screen_x, screen_y + idx * (12 * self.zoom),
                                            anchor="nw",
                                            text=line,
                                            fill="yellow" if tank.is_player else "red",
                                            font=("Courier", int(12 * self.zoom)))
                # Draw turret indicator (simulate rotation animation by adjusting length slightly)
                center_x = screen_x + (len(model[0]) * 6 * self.zoom) / 2
                center_y = screen_y + (len(model) * 12 * self.zoom) / 2
                turret_len = 20 * self.zoom
                rad = math.radians(tank.turret_angle)
                end_x = center_x + math.cos(rad) * turret_len
                end_y = center_y + math.sin(rad) * turret_len
                self.canvas.create_line(center_x, center_y, end_x, end_y, fill="cyan", width=2)
                # Draw a fake shadow (a black offset) for extra depth:
                self.canvas.create_text(screen_x+2, screen_y+2, anchor="nw",
                                        text=model[0], fill="black",
                                        font=("Courier", int(12 * self.zoom)))
        
        # Draw shells
        for shell in self.shells:
            s_x = (shell.x - self.cam_x) * tile_size
            s_y = (shell.y - self.cam_y) * tile_size
            self.canvas.create_text(s_x, s_y, text="*", fill="white",
                                    font=("Courier", int(12 * self.zoom)))
        
        # Draw explosions
        for exp in self.explosions:
            exp_text = exp.current_frame()
            e_x = (exp.x - self.cam_x) * tile_size
            e_y = (exp.y - self.cam_y) * tile_size
            self.canvas.create_text(e_x, e_y, text=exp_text, fill="orange",
                                    font=("Courier", int(12 * self.zoom)))
        
        # Draw crosshair at mouse position
        self.canvas.create_oval(self.mouse_x - 5, self.mouse_y - 5, self.mouse_x + 5, self.mouse_y + 5,
                                outline="lime")
        
        # Update UI panel
        info_text = f"HP: {self.player.health}   Ammo: {self.player.ammo}   Kills: {self.player.kills}   Zoom: {self.zoom:.2f}"
        if self.multiplayer:
            mode = "HOST" if self.host_mode else "CLIENT"
            info_text += f"   Mode: {mode}"
        self.ui_panel.config(text=info_text)
    
    # --------------- Main Game Loop ---------------
    def game_loop(self):
        now = time.time()
        dt = now - self.last_time
        self.last_time = now
        self.update_game(dt)
        self.update_camera()
        self.draw()
        self.root.after(int(1000 / self.FPS), self.game_loop)

    def serialize_state(self):
        return f"{self.player.id};{self.player.x};{self.player.y};{self.player.turret_angle};{self.player.health};{self.player.ammo};{self.player.kills}\n"

# ---------------- Main Program ----------------
def main():
    # Ask user if multiplayer is desired and if hosting or joining:
    mode_choice = input("Enter 'm' for multiplayer LAN or 'o' for offline mode: ").strip().lower()
    multiplayer = (mode_choice == 'm')
    host_mode = True
    host_ip = NET_HOST
    if multiplayer:
        host_choice = input("Enter 'h' to host or 'j' to join: ").strip().lower()
        host_mode = (host_choice == 'h')
        if not host_mode:
            host_ip = input("Enter host IP address: ").strip()
    root = tk.Tk()
    app = TankGameApp(root, multiplayer=multiplayer, host_mode=host_mode, host_ip=host_ip)
    root.mainloop()

if __name__ == "__main__":
    main()