#!/usr/bin/env python
import copy
import math
import random

import tcod

import fov

# ############################################
# Constants
# ############################################

# Size of the window.
SCREEN_WIDTH = 80
SCREEN_HEIGHT = 50

MAP_WIDTH = SCREEN_WIDTH
MAP_HEIGHT = 43

PANEL_WIDTH = SCREEN_WIDTH
PANEL_HEIGHT = SCREEN_HEIGHT - MAP_HEIGHT
BAR_WIDTH = 20

# Parameters for dungeon generator.
ROOM_MAX_SIZE = 10
ROOM_MIN_SIZE = 6
MAX_ROOMS = 30
MAX_ROOM_MONSTERS = 3

# Player parameters.
LIGHT_RADIUS = 20
PLAYER_HP = 10
PLAYER_POWER = 5
PLAYER_DEFENSE = 5

# Monster parameters.
TROLL_HP = 3
TROLL_POWER = 6
TROLL_DEFENSE = 1

# Visual paramters.
COLOUR_VISIBLE = tcod.Color(192, 192, 128)
COLOUR_NOT_VISIBLE = tcod.Color(64, 64, 64)
TROLL_COLOUR = tcod.Color(24, 172, 92)
MONSTER_DEATH_COLOUR = tcod.Color(192, 0, 0)
PLAYER_DEATH_COLOUR = tcod.Color(192, 0, 0)


# ############################################
# Classes
# ############################################

class Object:
    """Represents a generic ingame object."""

    def __init__(self, x, y, char, name, colour, passable=False,
                 fighter=None, ai=None):
        self.x = x
        self.y = y
        self.char = char
        self.name = name
        self.colour = colour
        self.passable = passable

        self.fighter = fighter
        self.ai = ai

        if self.fighter:
            self.fighter.parent = self
        if self.ai:
            self.ai.parent = self

    def move(self, dx, dy):
        if not is_blocked(self.y + dy, self.x + dx):
            self.x += dx
            self.y += dy

    def draw(self, console):
        if fov_map.visible[self.y][self.x]:
            console.fg[self.y][self.x] = self.colour
            console.ch[self.y][self.x] = ord(self.char)

    def distance_to(self, other):
        dx = other.x - self.x
        dy = other.y - self.y
        return math.sqrt(dx ** 2 + dy ** 2)

    def move_towards(self, target_y, target_x):
        dy = target_y - self.y
        dx = target_x - self.x
        distance = math.sqrt(dx ** 2 + dy ** 2)

        dx = int(round(dx / distance))
        dy = int(round(dy / distance))
        self.move(dx, dy)

    def send_to_back(self):
        global objects
        objects.remove(self)
        objects.insert(0, self)


class Tile:
    """Represents a tile on the map and it's properties."""

    def __init__(self, char, passable, block_los):
        self.char = char
        self.passable = passable
        self.block_los = block_los


TILE_WALL = Tile('#', False, True)
TILE_FLOOR = Tile('.', True, False)


class Rect:
    """Used to represent a rectangular room on the map."""

    def __init__(self, x, y, w, h):
        self.x1 = x
        self.y1 = y
        self.x2 = x + w
        self.y2 = y + h

    def centre(self):
        centre_x = (self.x1 + self.x2) // 2
        centre_y = (self.y1 + self.y2) // 2
        return centre_x, centre_y

    def intersect(self, other):
        return (self.x1 <= other.x2 and self.x2 >= other.x1 and
                self.y1 <= other.y2 and self.y2 >= other.y1)


# class DrawableComponent:
#     def __init__(self, char, colour, layer):
#         self.char = char
#         self.colour = colour
#         self.layer = layer
#
#     def draw(self, console):
#         y, x = self.parent.y, self.parent.x
#         console.fg[y][x] = self.colour
#         console.ch[y][x] = ord(self.char)


class CombatComponent:
    """Combat-related properties and methods. Used for composition with an Object."""

    def __init__(self, hp, defense, power, death_callback=None):
        self.max_hp = hp
        self.hp = hp
        self.defense = defense
        self.power = power
        self.death_callback = death_callback

    def take_damage(self, damage):
        if damage > 0:
            self.hp -= damage
        if self.hp <= 0 and self.death_callback:
            self.death_callback(self.parent)

    def attack(self, target):
        damage = self.power - target.fighter.defense

        if damage > 0:
            print(
                self.parent.name.capitalize() +
                " attacks " +
                target.name +
                " for " +
                str(damage) +
                " hit points.")
            target.fighter.take_damage(damage)
        else:
            print(
                self.parent.name.capitalize() +
                " attacks " +
                target.name +
                " but it has no effect!")


class BasicMonster:
    """Basic follow and attack AI."""

    def take_turn(self):
        monster = self.parent
        if fov_map.visible[monster.y][monster.x]:
            if monster.distance_to(player) >= 2:
                monster.move_towards(player.y, player.x)
            elif player.fighter.hp >= 0:
                monster.fighter.attack(player)


# ############################################
# Utility Functions
# ############################################


def is_blocked(y, x):
    if not map[y][x].passable:
        return True

    for object in objects:
        if not object.passable and object.x == x and object.y == y:
            return True

    return False


# ############################################
# Map Generation Functions
# ############################################

def generate_map():
    map = [[TILE_WALL for _ in range(MAP_WIDTH)]
           for _ in range(MAP_HEIGHT)]

    assert MAX_ROOMS > 0

    def room_rect():
        w = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
        h = random.randint(ROOM_MIN_SIZE, ROOM_MAX_SIZE)
        x = random.randint(1, MAP_WIDTH - w - 1)
        y = random.randint(1, MAP_HEIGHT - h - 1)
        return Rect(x, y, w, h)

    def create_room(rect):
        for row in range(rect.y1, rect.y2):
            for col in range(rect.x1, rect.x2):
                map[row][col] = TILE_FLOOR

    def create_h_tunnel(x1, x2, row):
        for col in range(min(x1, x2), max(x1, x2) + 1):
            map[row][col] = TILE_FLOOR

    def create_v_tunnel(y1, y2, col):
        for row in range(min(y1, y2), max(y1, y2) + 1):
            map[row][col] = TILE_FLOOR

    def create_corridor(new_rect, prev_rect):
        (new_x, new_y) = new_rect.centre()
        (prev_x, prev_y) = prev_rect.centre()
        if random.randint(0, 1) == 0:
            create_h_tunnel(prev_x, new_x, prev_y)
            create_v_tunnel(prev_y, new_y, new_x)
        else:
            create_v_tunnel(prev_y, new_y, prev_x)
            create_h_tunnel(prev_x, new_x, new_y)

    def spawn_monsters_in_rect(rect):
        num_monsters = random.randint(0, MAX_ROOM_MONSTERS)
        for _ in range(num_monsters):
            x = random.randint(rect.x1, rect.x2)
            y = random.randint(rect.y1, rect.y2)
            if map[y][x].passable:
                monster = Object(x, y, 'T', 'troll', TROLL_COLOUR, passable=False,
                                 fighter=copy.deepcopy(FIGHTER_TROLL),
                                 ai=BasicMonster())
                objects.append(monster)

    rect = room_rect()
    create_room(rect)
    rects = [rect]

    for i in range(MAX_ROOMS - 1):
        new_rect = room_rect()
        if not any(new_rect.intersect(rect) for rect in rects):
            create_room(new_rect)
            create_corridor(new_rect, rects[-1])
            rects.append(new_rect)
            spawn_monsters_in_rect(new_rect)
    return map, rects[0].centre()


# ############################################
# Drawing Functions
# ############################################

def render_map():
    for row in range(MAP_HEIGHT):
        for col in range(MAP_WIDTH):
            if explored[row][col]:
                map_console.fg[row][col] = COLOUR_VISIBLE if fov_map.visible[row][col] else COLOUR_NOT_VISIBLE
                map_console.ch[row][col] = ord(map[row][col].char)


def render_objects():
    # draw onground objects, draw monsters, draw player in order
    # possibly use a pq with layers here
    for object in objects:
        if object != player:
            object.draw(map_console)
    player.draw(map_console)


def draw_bar(x, y, total_width, name, value, maximum, bar_colour, back_colour):
    panel_console.draw_rect(x, y, total_width, 1, ord(' '), bg=back_colour)

    bar_width = int(float(value) / maximum * total_width)
    if bar_width > 0:
        panel_console.draw_rect(x, y, bar_width, 1, ord(' '), bg=bar_colour)

    msg = name + ': ' + str(value) + '/' + str(maximum)
    panel_console.print(int(x + total_width / 2), y, msg, fg=tcod.white, alignment=tcod.CENTER)


def render_panel():
    draw_bar(1, 1, BAR_WIDTH, 'HP', player.fighter.hp, player.fighter.max_hp,
             tcod.light_red, tcod.darker_red)


# ############################################
# Game Functions
# ############################################

def fov_compute():
    fov_map.compute_visible(player.y, player.x,
                            LIGHT_RADIUS, fov.RECURSIVE_SHADOWCASTING)

    for row in range(MAP_HEIGHT):
        for col in range(MAP_WIDTH):
            if fov_map.visible[row][col]:
                explored[row][col] = True


def player_death(player):
    global game_state
    print("You died!")
    game_state = 'dead'
    player.char = '%'
    player.colour = PLAYER_DEATH_COLOUR


def monster_death(monster):
    print(monster.name.capitalize() + " is dead!")
    monster.char = '%'
    monster.colour = MONSTER_DEATH_COLOUR
    monster.passable = True
    monster.fighter = None
    monster.ai = None
    monster.name = "remains of " + monster.name
    monster.send_to_back()


# ############################################
# Player Functions
# ############################################


def player_attack_move(dx, dy):
    x = player.x + dx
    y = player.y + dy

    target = next((object for object in objects if
                   object.fighter and
                   object.x == x and
                   object.y == y), None)

    if target is not None:
        player.fighter.attack(target)
    else:
        player.move(dx, dy)


MOVE_KEYS = {
    tcod.event.K_LEFT: (-1, 0),
    tcod.event.K_RIGHT: (1, 0),
    tcod.event.K_UP: (0, -1),
    tcod.event.K_DOWN: (0, 1),
    tcod.event.K_HOME: (-1, -1),
    tcod.event.K_END: (-1, 1),
    tcod.event.K_PAGEUP: (1, -1),
    tcod.event.K_PAGEDOWN: (1, 1),

    tcod.event.K_KP_1: (-1, 1),
    tcod.event.K_KP_2: (0, 1),
    tcod.event.K_KP_3: (1, 1),
    tcod.event.K_KP_4: (-1, 0),
    tcod.event.K_KP_6: (1, 0),
    tcod.event.K_KP_7: (-1, -1),
    tcod.event.K_KP_8: (0, -1),
    tcod.event.K_KP_9: (1, -1),
}

WAIT_KEYS = [
    tcod.event.K_CLEAR,
    tcod.event.K_KP_5,
]


class InputState(tcod.event.EventDispatch[None]):

    def ev_quit(self, event: tcod.event.Quit) -> None:
        self.cmd_quit()

    def ev_keydown(self, event: tcod.event.KeyDown) -> bool:
        if game_state == 'running':
            if event.sym in MOVE_KEYS:
                self.cmd_move(*MOVE_KEYS[event.sym])
                return True
            elif event.sym in WAIT_KEYS:
                return True
        if event.sym == tcod.event.K_ESCAPE:
            self.cmd_quit()
        elif event.sym == tcod.event.K_RETURN and \
                (event.mod & (tcod.event.KMOD_LALT | tcod.event.KMOD_RALT)):
            toggle_fullscreen(context)
        return False

    def cmd_move(self, x: int, y: int) -> None:
        player_attack_move(x, y)
        fov_compute()

    def cmd_quit(self) -> None:
        raise SystemExit()


def toggle_fullscreen(context: tcod.context.Context) -> None:
    """Toggle a context window between fullscreen and windowed modes."""
    if not context.sdl_window_p:
        return
    fullscreen = tcod.lib.SDL_GetWindowFlags(context.sdl_window_p) & (
            tcod.lib.SDL_WINDOW_FULLSCREEN | tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP
    )
    tcod.lib.SDL_SetWindowFullscreen(
        context.sdl_window_p,
        0 if fullscreen else tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP,
    )


# ############################################
# Initialisation and Main Loop
# ############################################

FIGHTER_TROLL = CombatComponent(
    hp=TROLL_HP,
    defense=TROLL_DEFENSE,
    power=TROLL_POWER,
    death_callback=monster_death)

FIGHTER_PLAYER = CombatComponent(
    hp=PLAYER_HP,
    defense=PLAYER_DEFENSE,
    power=PLAYER_POWER,
    death_callback=player_death)


def create_player():
    return Object(0, 0, '@', 'player', tcod.Color(127, 127, 127), passable=False,
                  fighter=FIGHTER_PLAYER)


tileset = tcod.tileset.load_tilesheet("res/terminal10x16_gs_ro.png",
                                      16, 16, tcod.tileset.CHARMAP_CP437)

main_console = tcod.Console(SCREEN_WIDTH, SCREEN_HEIGHT)
map_console = tcod.Console(MAP_WIDTH, MAP_HEIGHT)
panel_console = tcod.Console(PANEL_WIDTH, PANEL_HEIGHT)
input_state = InputState()

player = create_player()
objects = [player]
# objects_pos = {(object.x, object.y): object for object in objects}
map, (initial_x, initial_y) = generate_map()
player.x = initial_x
player.y = initial_y

explored = [[False for _ in range(MAP_WIDTH)]
            for _ in range(MAP_HEIGHT)]

fov_map = fov.Fov([[map[row][col].block_los for col in range(MAP_WIDTH)]
                   for row in range(MAP_HEIGHT)])
fov_compute()
game_state = 'running'

with tcod.context.new(
        columns=main_console.width, rows=main_console.height,
        renderer=tcod.context.RENDERER_SDL2,
        tileset=tileset,
        title="Hello World",
) as context:
    while True:
        map_console.clear()
        render_map()
        render_objects()
        map_console.blit(main_console)

        panel_console.clear()
        render_panel()
        panel_console.blit(main_console, 0, MAP_HEIGHT)

        context.present(main_console)

        for evt in tcod.event.wait():
            turn_taken = input_state.dispatch(evt)
            if game_state == 'running' and turn_taken:
                for object in objects:
                    if object.ai:
                        object.ai.take_turn()

# ############################################
# Testing Functions
# ############################################

dungeon = ["...........................................................",
           "...........................................................",
           "...........................................................",
           "...........................................................",
           "###########################################################",
           "#...........#.............................................#",
           "#...........#........#....................................#",
           "#.....................#...................................#",
           "#....####..............#..................................#",
           "#.......#.......................#####################.....#",
           "#.......#...........................................#.....#",
           "#.......#...........##..............................#.....#",
           "#####........#......##..........##################..#.....#",
           "#...#...........................#................#..#.....#",
           "#...#............#..............#................#..#.....#",
           "#...............................#..###############..#.....#",
           "#..............######...........#...................#.....#",
           "#..............#####............#...................#.....#",
           "#..............#####............#####################.....#",
           "#.........................................................#",
           "#.........................................................#",
           "###########################################################"]


def generate_map2():
    global map
    map = [[Tile(False) for _ in range(MAP_WIDTH)]
           for _ in range(MAP_HEIGHT)]

    for row, row_actual in enumerate(dungeon):
        for col, col_actual in enumerate(row_actual):
            if dungeon[row][col] == '#':
                map[row][col] = Tile(True)
