import os
from functools import partial

from direct.gui.DirectGui import *
from direct.interval.FunctionInterval import Wait, Func
from direct.interval.MetaInterval import Sequence
from direct.showbase import DirectObject
from panda3d.core import TextNode, TransparencyAttrib

import global_stuff
import music_player
from console_entry import ConsoleEntry
from global_stuff import base, colors as tile_colors, kaushan, button_style, door_open_sfx, door_close_sfx, \
    place_wall_sfx
from world import instance as world

main_console = ConsoleEntry(75, 85, 16)
action1_console = ConsoleEntry(500, 505, 4)
action2_console = ConsoleEntry(855, 505, 4)
error_lines = {main_console: [], action1_console: [], action2_console: []}
action_consoles = [action1_console, action2_console]
consoles = list(error_lines.keys())

bg = OnscreenImage(image='img/designer_bg.png', pos=(0, 0, 0), scale=(1, 1, 1), parent=base.render2d)
bg.set_transparency(TransparencyAttrib.MAlpha)

summary_color = (0, 0, 0, 1)

code_counter = TextNode('code counter')
code_counter.set_font(kaushan)
code_counter.set_text_color(summary_color)
cc_np = base.aspect2d.attach_new_node(code_counter)
cc_np.set_scale(0.05)
cc_np.set_pos(-1.5, 0, -0.8)

feng_shui_counter = TextNode('feng shui')
feng_shui_counter.set_font(kaushan)
feng_shui_counter.set_text_color(0, 0, 0, 1)
fs_np = base.aspect2d.attach_new_node(feng_shui_counter)
fs_np.set_scale(0.05)
fs_np.set_pos(-1.25, 0, -0.8)

feng_shui_game_counter = TextNode('feng shui runtime')
feng_shui_game_counter.set_font(kaushan)
feng_shui_game_counter.set_text_color(0, 0, 0, 1)
feng_shui_game_counter.set_align(TextNode.ARight)
fsg = base.pixel2d.attach_new_node(feng_shui_game_counter)
fsg.set_scale(50)
fsg.set_pos(200, 0, -650)

fs_label = TextNode('feng shui label')
fs_label.set_font(kaushan)
fs_label.set_text_color(0, 0, 0, 1)
fs_label.text="Feng Shui\n\n          /-50"
fsl = base.pixel2d.attach_new_node(fs_label)
fsl.set_scale(35)
fsl.set_pos(110, 0, -560)

levelend_bg = OnscreenImage(image='img/levelend_bg.png', pos=(0, 0, 0), scale=(1, 1, 1), parent=base.render2d)
levelend_bg.set_transparency(TransparencyAttrib.MAlpha)
levelend_tn = TextNode('levelend label')
levelend_tn.set_align(TextNode.ACenter)
levelend_tn.set_font(kaushan)
levelend_tn.set_text_color(0, 0, 0, 1)
levelend_label = base.pixel2d.attach_new_node(levelend_tn)
levelend_label.set_pos(620, 0, -250)
levelend_label.set_scale(35)
levelend_bg.hide()
levelend_label.hide()

counters = [cc_np, fs_np]


def update_fengshui_counter(task=None):
    if not cc_np.is_hidden():
        # find code length based on non-commented lines
        code_length = 0
        for con in consoles:
            text = ""
            for line in con.text.splitlines():
                line = line.lstrip()
                if len(line)>0 and line[0] == '#':
                    continue
                text += line
            text = "".join(text.split())
            code_length += len(text
                               )
        world().feng_shui = round(0 - code_length/4, 1)
        code_counter.text = "CODE [" + str(code_length) + "]"
    elif not stop_button.is_hidden():
        if world().feng_shui <= -50:
            levelend(False, 'Failed\nYour feng shui\nis too low.')
        if world().feng_shui > 0 and all((shrine.ghosts_enshrined >= 5 for shrine in world().shrines)):
            levelend(True, "Success\nYou have achieved\nFeng Shui")
    feng_shui_counter.text = "-> FENG SHUI [" + str(world().feng_shui) + "]"
    feng_shui_game_counter.text = str(world().feng_shui)
    if task:
        return task.cont


def levelend(won: bool, message: str):
    levelend_bg.show()
    levelend_label.show()
    levelend_tn.text = message
    for b in levelend_buttons:
        b.show()
    base.task_mgr.removeTasksMatching("*ghost*")


base.task_mgr.add(update_fengshui_counter, "update fs counter")


def run(test=False, console=main_console):
    save_game()  # autosave :D

    if console == main_console:
        world().reset()
    script = console.text
    error_lines[console].clear()
    console.clear_error()
    if test:
        console.unfocus()
        test_button[console].hide()
        clear_button[console].hide()
        reset_button.hide()

    Sequence(
        *(
            Sequence(
                Func(parse_or_error, console, i, line),
                Wait(0.5),
            ) for i, line in enumerate(script.splitlines())
            if len(line.strip()) > 0 and line.lstrip()[0] != '#'
          ),
        Func(test_button[console].show) if test else Wait(0),
        Func(clear_button[console].show) if test else Wait(0),
        Func(reset_button.show) if test else Wait(0),
        Func(console.focus),
     ).start()


def start():
    run(False)
    hide()
    update_fengshui_counter()
    world().start()


def stop():
    show()
    reset()


def hide():
    for b in designer_buttons + counters + consoles + levelend_buttons:
        b.hide()
    for b in runtime_buttons:
        b.show()
    level_select_button.show()
    bg.hide()
    fsl.show()
    fsg.show()
    levelend_bg.hide()
    levelend_label.hide()


def hide_all():
    # for main menu stuff
    for b in designer_buttons+counters+runtime_buttons + consoles + levelend_buttons:
        b.hide()
    level_select_button.hide()
    bg.hide()
    fsl.hide()
    fsg.hide()
    levelend_bg.hide()
    levelend_label.hide()


def show():
    for b in designer_buttons + counters + consoles:
        b.show()
    for b in runtime_buttons + levelend_buttons:
        b.hide()
    level_select_button.show()
    bg.show()
    fsl.hide()
    fsg.hide()
    levelend_bg.hide()
    levelend_label.hide()


def reset():
    main_console.show()
    action1_console.show()
    world().reset()


def clear(console):
    console.clear()
    if console == main_console:
        reset()


def save_game():
    prefix = {main_console: 'M', action1_console: '1', action2_console: '2'}
    level = world().level[:-2]
    with open('saves/' + level + '.s', 'w') as file:
        for console in consoles:
            for e in console.entries:
                line = prefix[console] + e.get() + '\n'
                file.write(line)


def remove_save():
    level_name = world().level
    load_script('levels/' + level_name, True)


def on_level_load():
    for c in consoles:
        c.clear()
    level_name = world().level[:-2]
    savefile = 'saves/' + level_name + '.s'

    if os.path.isfile(savefile):
        load_script(savefile, False)
    else:
        load_script('levels/' + level_name + '.l', True)


def load_script(filename, skip_maps=True):
    for c in consoles:
        clear(c)
    iter = {main_console: 0, action1_console: 0, action2_console: 0}
    with open(filename, 'r') as file:
        if skip_maps:
            for _ in range(19):
                file.readline()
        while True:
            line = file.readline()
            if line == "":
                break
            c = None
            line = line.strip()
            if len(line) == 0:
                continue
            if line[0] == 'M':
                c = main_console
            elif line[0] == '1':
                c = action1_console
            elif line[0] == '2':
                c = action2_console
            if c is None:
                continue
            c.entries[iter[c]].set(line[1:])
            iter[c] += 1


class EventHandler(DirectObject.DirectObject):
    def __init__(self):
        self.accept('level_select', hide_all)
        self.accept('level_loaded', show)
        self.accept('level_selected', on_level_load)
        self.accept('1', lambda: run(False, action1_console)
                    if not action_button[action1_console].is_hidden() else None)
        self.accept('2', lambda: run(False, action2_console)
                    if not action_button[action2_console].is_hidden() else None)


EventHandler()

reset_button = DirectButton(text="Reset",     command=reset, parent=base.pixel2d, scale=35, pos=(227, 0, -680))
start_button = DirectButton(text="Start", command=start, parent=base.pixel2d, scale=50, pos=(950, 0, -55))
remove_save_button = DirectButton(text="Restore Default Designs", command=remove_save, parent=base.pixel2d, scale=20,
                                  pos=(227, 0, -710))
designer_buttons = [reset_button, start_button, remove_save_button]

test_button = {}
clear_button = {}
for console in consoles:
    test_button[console] = DirectButton(text="Test", scale=35, parent=base.pixel2d,
                                        command=partial(run, test=True, console=console))
    clear_button[console] = DirectButton(text="Clear", scale=35, parent=base.pixel2d,
                                         command=partial(clear, console))
    designer_buttons += [test_button[console], clear_button[console]]

test_button[main_console].set_pos (135, 0, -680)
clear_button[main_console].set_pos(320, 0, -680)
test_button[action1_console].set_pos(590, 0, -680)
clear_button[action1_console].set_pos(740, 0, -680)
test_button[action2_console].set_pos(945, 0, -680)
clear_button[action2_console].set_pos(1095, 0, -680)


stop_button = DirectButton(text="Stop",     parent=base.pixel2d, scale=50, pos=(950, 0, -55), command=stop)
runtime_buttons = [stop_button]
action_button = {}
for i, console in enumerate(action_consoles):
    action_button[console] = DirectButton(text="Action "+str(i+1), parent=base.pixel2d, scale=35,
                                          command=partial(run, test=False, console=console))
    runtime_buttons.append(action_button[console])
action_button[action1_console].set_pos(1170, 0, -570)
action_button[action2_console].set_pos(1060, 0, -660)

level_select_button = DirectButton(text="menu", parent=base.pixel2d, scale=35, pos=(1225, 0, -70),
                                   command=lambda: (save_game(), base.messenger.send('level_select')))
level_select_button.set_hpr(0, 0, 45)

levelend_menu_button = DirectButton(text="menu", parent=base.pixel2d, scale=35, pos=(800, 0, -455),
                                    command=lambda: (save_game(), base.messenger.send('level_select')))
levelend_retry_button = DirectButton(text="again", parent=base.pixel2d, scale=35, pos=(480, 0, -455),
                                     command=stop)
levelend_buttons = [levelend_menu_button, levelend_retry_button]

buttons = designer_buttons + runtime_buttons + [level_select_button] + levelend_buttons

for b in buttons:
    button_style(b)
remove_save_button['image_scale'] = (12, 1, 0.5)

for b in runtime_buttons + levelend_buttons:
    b.hide()


def parse_or_error(console, i, command):
    console.select_line(i)
    try:
        parse(command)
    except SyntaxError:
        error_lines[console].append(i)
        console.set_error(i)


def display_error(console):
    console.set_error(*error_lines[console])


def parse(command):
    command = "".join(command.split())  # remove all whitespace
    try:
        split_point = command.rindex('>')
    except ValueError:
        raise SyntaxError
    left = command[:split_point-1]
    affects = command[split_point-1]
    right = command[split_point+1:]

    assert affects in ('-', '*', '=', '|')

    for tile in world():
        if satisfies(tile, left):
            affected = set()
            affected.add(tile)
            if affects == '-':
                pass
            elif affects == '*':
                affected = affected.union(set(tile.neighbors))
            elif affects == '=':
                affected = affected.union(tile.horizontal_neighbors)
            elif affects == '|':
                affected = affected.union(set(tile.vertical_neighbors))

            for t in affected:
                apply(t, right)
    if "$wall" in command or "$door" in command:
        place_wall_sfx.play()
    if "$open" in command:
        door_open_sfx.play()
    if "$close" in command:
        door_close_sfx.play()


def satisfies(tile, condition: str) -> bool:
    # check for constants
    if condition == 't':
        return True
    if condition == 'f':
        return False

    start = -1
    # group characters into tokens
    keys = ('(', ')', '!', '&', '|')
    tokens = []
    for i,c in enumerate(condition):
        if c in keys:
            if start != -1:
                tokens.append(condition[start:i])
            tokens.append(c)
            start = -1
        elif start == -1:
            start = i
    if start != -1:
        tokens.append(condition[start:])

    start = -1
    # evaluate all parentheses first
    for i,c in enumerate(tokens):
        if c == '(' and start==-1:
            start = i
        elif c == ')' and start != -1:
            res = 't' if satisfies(tile, "".join(tokens[start+1:i])) else 'f'
            return satisfies(tile, "".join(tokens[:start] + res + tokens[i+1:]))

    # then evaluate all nots
    for i,c in enumerate(tokens):
        if c == '!':
            res = 'f' if satisfies(tile, tokens[i+1]) else 't'
            return satisfies(tile, "".join(tokens[:i]) + res + "".join(tokens[i+2:]))

    # evaluate all ands
    for i,c in enumerate(tokens):
        if c == '&':
            res = 't' if satisfies(tile, tokens[i-1]) and satisfies(tile, tokens[i+1]) else 'f'
            return satisfies(tile, "".join(tokens[:i-1]) + res + "".join(tokens[i+2:]))

    # then ors
    for i,c in enumerate(tokens):
        if c == '|':
            res = 't' if satisfies(tile, tokens[i-1]) or satisfies(tile, tokens[i+1]) else 'f'
            return satisfies(tile, "".join(tokens[:i-1]) + res + "".join(tokens[i+2:]))

    if len(condition) == 1 and condition in "123456789ABCDEFGHI":
        condition = "=" + condition

    # finally, check actual tile data
    if condition in tile_colors.keys():
        return tile.color == condition
    if len(condition) == 0:
        raise SyntaxError
    if condition[0] in ('=', '>', '<', '%'):
        if condition[1] in "123456789":
            actual, expected = tile.x, int(condition[1])
        elif condition[1] in "ABCDEFGHI":
            actual, expected = tile.y, ord(condition[1]) - ord('A') + 1
        else:
            raise SyntaxError
        if condition[0] == '%':
            if len(condition) == 4:
                if condition[2] != '+':
                    raise SyntaxError
                if condition[3] in "ABCDEFGHI":
                    condition[3] = ord(condition[3]) - ord('A') + 1
                target = int(condition[3])
                return (actual % expected) == target
            return (actual % expected) == 0
        if len(condition) > 2:
            raise SyntaxError
        if condition[0] == '=':
            return actual == expected
        if condition[0] == '>':
            return actual > expected
        if condition[0] == '<':
            return actual < expected
    raise SyntaxError(condition)


def apply(tile, action: str):
    actions = action.split("&")
    if len(actions) > 1:
        for a in actions:
            apply(tile, a)
        return

    if action == "$wall":
        tile.wall = True
        return

    if action == "$door":
        tile.door = True
        return

    if action == "$open":
        if tile.door:
            tile.open_door()
        return

    if action == "$close":
        if tile.door:
            tile.open_door(False)
        return

    if action == "$kill":
        for g in set(world().ghosts):
            if g.tile == tile:
                if g.color != '?':
                    world().add_feng_shui(-0.2, tile.model.get_pos())
                g.die()
        return

    if action in tile_colors.keys():
        tile.color = action
        return

    raise SyntaxError


hide_all()
