#!/usr/bin/python3.7

import pygame as pg
import easygui
import os.path
import re
import json
from math import inf
import heapq


screen_width = 0
screen_height = 0
info_part = 100
cell_size = 100
font_name = pg.font.match_font('arial')

controlls_text = "Movement: arrows\nToggle safety mode: s\nrestart game: r\nfind shortest path: f\nfind energetically best path: e"

"""
Object with pacman information
"""
class Pacman:
    def __init__(self, energy, start):
        self.energy = energy
        self.x = start[0] - 1
        self.y = start[1] - 1

"""
Object representing one pacman field
"""
class Field:
    def __init__(self, type, x, y):
        self.type = type[0]
        self.value = int(type[1]) if len(type) > 1 else inf
        self.x = x
        self.y = y

    """
    Returns color of current field in RGB
    """
    def get_color(self):
        if self.type == "V":
            return (255, 255, 255)
        if self.type == "J":
            return (0, 255, 0)
        if self.type == "Z":
            return (0, 0, 0)
        if self.type == "B":
            return (255, 0, 0)
    
    """
    Aditional actions after step on field -> final energy
    """
    def make_action(self, energy):
        tmp = self.value
        if self.type == "J":
            energy += self.value
            self.value = 0
        if self.type == "B":
            energy -= self.value
            self.value = 0
        if self.type == "Z":
            energy -= self.value
        return energy, tmp


"""
Object representing basic game strucutre
"""
class Game:
    def __init__(self, energy, start, end, plane):
        self.pacman = Pacman(energy, start)
        self.endX = end[0] - 1
        self.endY = end[1] - 1
        self.width = len(plane[0])
        self.height = len(plane)
        self.playground = []
        for i in range(self.height):
            play_row = []
            for j in range(self.width):
                play_row.append(Field(plane[i][j], j, i))
            self.playground.append(play_row)
    
    """
    Draws current playground
    """
    def draw(self, screen):
        global cell_size
        screen.fill((255,255,255))
        for row in self.playground:
            for cell in row:
                pg.draw.rect(screen, cell.get_color(), pg.Rect(cell.x * cell_size, cell.y * cell_size, cell_size, cell_size))
                if cell.value != inf:
                    draw_text(screen, str(cell.value), 32, cell.x * cell_size + cell_size // 2, cell.y * cell_size + cell_size // 3, (255,255,255))
                if cell.x == self.endX and cell.y == self.endY:
                    draw_text(screen, "F", 32, cell.x * cell_size + cell_size // 2, cell.y * cell_size + cell_size // 3, (0,0,0))
        if best_path is not None:
            draw_line(screen, best_path)
        pg.draw.circle(screen, (0,0,255), (self.pacman.x * cell_size + cell_size // 2, self.pacman.y * cell_size + cell_size // 2), int(cell_size * 0.4))   
        draw_text(screen, str(self.pacman.energy), 32, self.pacman.x * cell_size + cell_size // 2, self.pacman.y * cell_size + cell_size // 3, (255,255,255))
        safety_mode_text = "on" if safety_mode else "off"
        draw_multiline_text(screen, controlls_text + "\n" + "safety mode: " + safety_mode_text, (20, screen_height + 30), 12, (0,0,0))

    """
    Pacmans move
    """
    def move(self, directionX, directionY):
        newX = self.pacman.x + directionX
        newY = self.pacman.y + directionY
        if newX < 0 or newY < 0 or newX >= self.width or newY >= self.height:
            return
        self.pacman.energy -= 1
        if self.pacman.energy < 0:
            if not safety_mode:
                end_game()
            else:
                self.pacman.energy += 1
                return
        if newX == self.endX and newY == self.endY:
            win()
        new_energy, tmp = self.playground[newY][newX].make_action(self.pacman.energy)
        if new_energy < 0:
            if not safety_mode:
                end_game()
            else:
                self.playground[newY][newX].value = tmp
                self.pacman.energy += 1
                return
        self.pacman.x = newX
        self.pacman.y = newY
        self.pacman.energy = new_energy

"""
Object for automatical finding of best paths
"""
class Autosolver:
    def __init__(self, game: Game):
        self.start = (game.pacman.x, game.pacman.y)
        self.energy = game.pacman.energy
        self.end = (game.endX, game.endY)
        self.field = []
        self.nullables = {}
        last_nullable = -1
        for row in game.playground:
            new_row = []
            for cell in row:
                new_row.append(1 if cell.type == "V" else cell.value + 1)
                if (cell.type == "B" or cell.type == "J") and cell.value != 0:
                    last_nullable += 1
                    self.nullables[(cell.x, cell.y)] = last_nullable
                    if(cell.type == "J"):
                        new_row[-1] = 1 - cell.value 
            self.field.append(new_row)
    
    """
    Find shortest path from pacman actual position
    """
    def solve_shortest_path(self):
        definitives = {}
        heap = [(0, 0, self.start, self.start)]
        while True:
            dist, en, act, prev = heapq.heappop(heap)
            if en > self.energy:
                return None
            if act == self.end:
                definitives[(act[0], act[1], en)] = (prev, en - self.field[act[1]][act[0]])
                return (self._reconstruct_path_distance(definitives, en), dist)
            if (act[0], act[1], en) in definitives:
                continue
            definitives[(act[0], act[1], en)] = (prev, en - self.field[act[1]][act[0]])
            for n in self._get_neighbors(act, len(self.field), len(self.field[0])):
                if (n[0], n[1], en + self.field[n[1]][n[0]]) not in definitives:
                    heapq.heappush(heap, (dist + 1, en + self.field[n[1]][n[0]], n, act))

    """
    Find est path from actual pacman position acording to energy
    """
    def solve_energetically_best_path(self):
        definitives = {}
        suffix = tuple([False] * len(self.nullables.keys()))
        heap = [(0, self.start, self.start, suffix, suffix)]
        while len(heap) > 0:
            en, act, prev, sx, prevsx = heapq.heappop(heap)
            if en > self.energy:
                continue
            if act == self.end:
                definitives[(act, sx)] = (prev, prevsx)
                return (self._reconstruct_path(definitives, suffix), en)
            if (act, sx) in definitives:
                continue
            definitives[(act, sx)] = (prev, prevsx)
            for n in self._get_neighbors(act, len(self.field), len(self.field[0])):
                if n not in definitives:
                    new_sx = list(suffix)
                    if n in self.nullables:
                        new_sx[self.nullables[n]] = True
                    heapq.heappush(heap, (en + self.field[n[1]][n[0]] if not self._is_nulled(n, sx) else en + 1, n, act, tuple(new_sx), sx))
        return None

    def _get_neighbors(self, p, h, w):
        res = []
        if p[0] > 0:
            res.append((p[0] - 1, p[1]))
        if p[0] < w - 1:
            res.append((p[0] + 1, p[1]))
        if p[1] > 0:
            res.append((p[0], p[1] - 1))
        if p[1] < h - 1:
            res.append((p[0], p[1] + 1))
        return res

    def _is_nulled(self, pos, sx):
        if pos not in self.nullables:
            return False
        return  sx[self.nullables[pos]]

    def _reconstruct_path(self, definitives, suffix):
        res = []
        res.append(self.end)
        act = self.end
        sx = suffix
        while act != self.start:
            act, sx = definitives[act, sx]
            res.append(act)
        return list(reversed(res))

    def _reconstruct_path_distance(self, definitives, en):
        res = []
        res.append(self.end)
        act = self.end
        e = en
        while act != self.start:
            act,e = definitives[(act[0], act[1], e)]
            res.append(act)
        return list(reversed(res))

"""
Write text on playground
"""
def draw_text(surf, text, size, x, y, color):
    font = pg.font.Font(font_name, size)
    text_surface = font.render(text, True, color)
    text_rect = text_surface.get_rect()
    text_rect.midtop = (x, y)
    surf.blit(text_surface, text_rect)

def draw_multiline_text(surface, text, pos, size, color):
    font = pg.font.SysFont('arial', size)
    words = [word.split(' ') for word in text.splitlines()]  
    space = font.size(' ')[0] 
    max_width, max_height = surface.get_size()
    x, y = pos
    for line in words:
        for word in line:
            word_surface = font.render(word, 0, color)
            word_width, word_height = word_surface.get_size()
            if x + word_width >= max_width:
                x = pos[0] 
                y += word_height  
            surface.blit(word_surface, (x, y))
            x += word_width + space
        x = pos[0]
        y += word_height

def draw_line(screen, path):
    for i in range(1, len(path)):
        pg.draw.line(
            screen, 
            (255, 0, 255), 
            (path[i - 1][0] * cell_size + cell_size // 2, path[i - 1][1] * cell_size + cell_size // 2),
            (path[i][0] * cell_size + cell_size // 2, path[i][1] * cell_size + cell_size // 2)
        )

def load_input_from_path(path):
    global screen_width, screen_height
    data = None
    with open(path) as json_file:  
        data = json.load(json_file)
    width = int(data["SIRKA"])
    height = int(data["VYSKA"])
    start = (int(data["START"]["X"]), int(data["START"]["Y"]))
    end = (int(data["CIL"]["X"]), int(data["CIL"]["Y"]))
    energy = int(data["START"]["E"])
    plane = []
    for r in data["PLAN"]:
        plane.append(r.split(','))
    screen_width = cell_size * width
    screen_height = cell_size * height
    return Game(energy, start, end, plane)

"""
Reads file and load game
"""
def load_input():
    global screen_width, screen_height, file_path
    try:
        file_path = input("Insert path to the file: ")
        return load_input_from_path(file_path)

    except:
        print("File could not be successfully loaded or was in incorrect format. Please try again.")
        return None

"""
End game - lose
"""
def end_game():
    global end, screen, screen_height, screen_width
    end = True
    screen.fill((255,255,255))
    draw_text(screen, "Game over", 64, screen_width // 2, screen_height // 2, (0,0,0))

"""
Win of the game
"""
def win():
    global end, screen, screen_height, screen_width
    end = True
    screen.fill((255,255,255))
    draw_text(screen, "You won", 64, screen_width // 2, screen_height // 2, (0,0,0))

exit = False
end = False
safety_mode = False
file_path = ""

game = load_input()
if game is None:
    exit = True
    
pg.init()
pg.display.set_caption("Pacman")
screen = pg.display.set_mode((screen_width, screen_height + info_part))

best_path = None

while not exit:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            exit = True
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_LEFT:
                game.move(-1, 0)
            if event.key == pg.K_RIGHT:
                game.move(1, 0)
            if event.key == pg.K_UP:
                game.move(0, -1)
            if event.key == pg.K_DOWN:
                game.move(0, 1)
            if event.key == pg.K_s:
                safety_mode = not safety_mode
            if event.key == pg.K_r:
                safety_mode = False
                end = False
                game = load_input_from_path(file_path)
            if event.key == pg.K_e:
                solver = Autosolver(game)
                res = solver.solve_energetically_best_path()
                if res is None:
                    easygui.msgbox("Path does not exist", title="Best path")
                else:
                    best_path, en = res
                    easygui.msgbox("Final energy will be " + str(game.pacman.energy - en), title="Best path")

            if event.key == pg.K_f:
                solver = Autosolver(game)
                res = solver.solve_shortest_path()
                if res is None:
                    solver = Autosolver(game)
                    res = solver.solve_energetically_best_path()
                    if res is None:
                        easygui.msgbox("Path does not exist", title="Best path")
                    else:
                        best_path, dist = res
                        easygui.msgbox("Path length is " + str(len(best_path)), title="Best path")
                else:
                    best_path, dist = res
                    easygui.msgbox("Path length is " + str(dist), title="Best path")



    if not end:
        game.draw(screen)

    pg.display.flip()


# vzorova-data/AI_Level_3_Distance_3_or_Energy_95.json
# vzorova-data/AI_Level_2_Distance_7_or_Energy_1.json 