import pygame, sys, os
from pygame.locals import *

from collections import deque

def to_box(level, index):
    if level[index] == '-' or level[index] == '@':
        level[index] = '$'
    else:
        level[index] = '*'


def to_man(level, i):
    if level[i] == '-' or level[i] == '$':
        level[i] = '@'
    else:
        level[i] = '+'


def to_floor(level, i):
    if level[i] == '@' or level[i] == '$':
        level[i] = '-'
    else:
        level[i] = '.'


def to_offset(d, width):
    d4 = [-1, -width, 1, width]
    m4 = ['l', 'u', 'r', 'd']
    return d4[m4.index(d.lower())]


def b_manto(level, width, b, m, t):
    maze = list(level)
    maze[b] = '#'
    if m == t:
        return 1
    queue = deque([])
    queue.append(m)
    d4 = [-1, -width, 1, width]
    m4 = ['l', 'u', 'r', 'd']
    while len(queue) > 0:
        pos = queue.popleft()
        for i in range(4):
            newpos = pos + d4[i]
            if maze[newpos] in ['-', '.']:
                if newpos == t:
                    return 1
                maze[newpos] = i
                queue.append(newpos)
    return 0


def b_manto_2(level, width, b, m, t):
    maze = list(level)
    maze[b] = '#'
    maze[m] = '@'
    if m == t:
        return []
    queue = deque([])
    queue.append(m)
    d4 = [-1, -width, 1, width]
    m4 = ['l', 'u', 'r', 'd']
    while len(queue) > 0:
        pos = queue.popleft()
        for i in range(4):
            newpos = pos + d4[i]
            if maze[newpos] in ['-', '.']:
                maze[newpos] = i
                queue.append(newpos)
                if newpos == t:
                    path = []
                    while maze[t] != '@':
                        path.append(m4[maze[t]])
                        t = t - d4[maze[t]]
                    return path

    return []


class PushTheBox:
    def __init__(self):
        self.level = list(
            '----#####--------------#---#--------------#$--#------------###--$##-----------#--$-$-#---------###-#-##-#---#######---#-##-#####--..##-$--$----------..######-###-#@##--..#----#-----#########----#######--------')
        self.w = 19
        self.h = 11
        self.man = 163
        self.hint = list(self.level)
        self.solution = []
        self.push = 0
        self.todo = []
        self.auto = 0
        self.sbox = 0
        self.queue = []

    def draw(self, screen, skin):
        w = skin.get_width() / 4
        offset = (w - 4) / 2
        for i in range(0, self.w):
            for j in range(0, self.h):
                if self.level[j * self.w + i] == '#':
                    screen.blit(skin, (i * w, j * w), (0, 2 * w, w, w))
                elif self.level[j * self.w + i] == '-':
                    screen.blit(skin, (i * w, j * w), (0, 0, w, w))
                elif self.level[j * self.w + i] == '@':
                    screen.blit(skin, (i * w, j * w), (w, 0, w, w))
                elif self.level[j * self.w + i] == '$':
                    screen.blit(skin, (i * w, j * w), (2 * w, 0, w, w))
                elif self.level[j * self.w + i] == '.':
                    screen.blit(skin, (i * w, j * w), (0, w, w, w))
                elif self.level[j * self.w + i] == '+':
                    screen.blit(skin, (i * w, j * w), (w, w, w, w))
                elif self.level[j * self.w + i] == '*':
                    screen.blit(skin, (i * w, j * w), (2 * w, w, w, w))
                if self.sbox != 0 and self.hint[j * self.w + i] == '1':
                    screen.blit(skin, (i * w + offset, j * w + offset), (3 * w, 3 * w, 4, 4))

    def move(self, d):
        self._move(d)
        self.todo = []

    def _move(self, d):
        self.sbox = 0
        h = to_offset(d, self.w)
        h2 = 2 * h
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            # move
            to_man(self.level, self.man + h)
            to_floor(self.level, self.man)
            self.man += h
            self.solution += d
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                # push
                to_box(self.level, self.man + h2)
                to_man(self.level, self.man + h)
                to_floor(self.level, self.man)
                self.man += h
                self.solution += d.upper()
                self.push += 1

    def undo(self):
        if self.solution.__len__() > 0:
            self.todo.append(self.solution[-1])
            self.solution.pop()

            h = to_offset(self.todo[-1], self.w) * -1
            if self.todo[-1].islower():
                # undo a move
                to_man(self.level, self.man + h)
                to_floor(self.level, self.man)
                self.man += h
            else:
                # undo a push
                to_floor(self.level, self.man - h)
                to_box(self.level, self.man)
                to_man(self.level, self.man + h)
                self.man += h
                self.push -= 1

    def redo(self):
        if self.todo.__len__() > 0:
            self._move(self.todo[-1].lower())
            self.todo.pop()

    def manto(self, x, y):
        maze = list(self.level)
        maze[self.man] = '@'
        queue = deque([])
        queue.append(self.man)
        d4 = [-1, -self.w, 1, self.w]
        m4 = ['l', 'u', 'r', 'd']
        while len(queue) > 0:
            pos = queue.popleft()
            for i in range(4):
                newpos = pos + d4[i]
                if maze[newpos] in ['-', '.']:
                    maze[newpos] = i
                    queue.append(newpos)
        # print str(maze)
        t = y * self.w + x
        if maze[t] in range(4):
            self.todo = []
            while maze[t] != '@':
                self.todo.append(m4[maze[t]])
                t = t - d4[maze[t]]
        # print self.todo
        self.auto = 1

    def automove(self):
        if self.auto == 1 and self.todo.__len__() > 0:
            self._move(self.todo[-1].lower())
            self.todo.pop()
        else:
            self.auto = 0

    def boxhint(self, x, y):
        d4 = [-1, -self.w, 1, self.w]
        m4 = ['l', 'u', 'r', 'd']
        b = y * self.w + x
        maze = list(self.level)
        to_floor(maze, b)
        to_floor(maze, self.man)
        mark = maze * 4
        size = self.w * self.h
        self.queue = []
        head = 0
        for i in range(4):
            if b_manto(maze, self.w, b, self.man, b + d4[i]):
                if len(self.queue) == 0:
                    self.queue.append((b, i, -1))
                mark[i * size + b] = '1'
        # print self.queue
        while head < len(self.queue):
            pos = self.queue[head]
            head += 1
            # print pos
            for i in range(4):
                if mark[pos[0] + i * size] == '1' and maze[pos[0] - d4[i]] in ['-', '.']:
                    # print i
                    if mark[pos[0] - d4[i] + i * size] != '1':
                        self.queue.append((pos[0] - d4[i], i, head - 1))
                        for j in range(4):
                            if b_manto(maze, self.w, pos[0] - d4[i], pos[0], pos[0] - d4[i] + d4[j]):
                                mark[j * size + pos[0] - d4[i]] = '1'
        for i in range(size):
            self.hint[i] = '0'
            for j in range(4):
                if mark[j * size + i] == '1':
                    self.hint[i] = '1'
        # print self.hint

    def boxto(self, x, y):
        d4 = [-1, -self.w, 1, self.w]
        m4 = ['l', 'u', 'r', 'd']
        om4 = ['r', 'd', 'l', 'u']
        b = y * self.w + x
        maze = list(self.level)
        to_floor(maze, self.sbox)
        to_floor(maze, self.man)  # make a copy of working maze by removing the selected box and the man
        for i in range(len(self.queue)):
            if self.queue[i][0] == b:
                self.todo = []
                j = i
                while self.queue[j][2] != -1:
                    self.todo.append(om4[self.queue[j][1]].upper())
                    k = self.queue[j][2]
                    if self.queue[k][2] != -1:
                        self.todo += b_manto_2(maze, self.w, self.queue[k][0], self.queue[k][0] + d4[self.queue[k][1]],
                                               self.queue[k][0] + d4[self.queue[j][1]])
                    else:
                        self.todo += b_manto_2(maze, self.w, self.queue[k][0], self.man,
                                               self.queue[k][0] + d4[self.queue[j][1]])
                    j = k
                # print self.todo
                self.auto = 1
                return
        print('not found!')

    def mouse(self, x, y):
        if x >= self.w or y >= self.h:
            return
        m = y * self.w + x
        if self.level[m] in ['-', '.']:
            if self.sbox == 0:
                self.manto(x, y)
            else:
                self.boxto(x, y)
        elif self.level[m] in ['$', '*']:
            if self.sbox == m:
                self.sbox = 0
            else:
                self.sbox = m
                self.boxhint(x, y)
        elif self.level[m] in ['-', '.', '@', '+']:
            self.boxto(x, y)


def start_game():
    # start pygame
    pygame.init()
    screen = pygame.display.set_mode((400, 300))

    # load skin
    skinfilename = os.path.join(r'.\py_games\games\borgar.png')
    try:
        skin = pygame.image.load(skinfilename)
    except pygame.error as msg:
        print('cannot load skin')
        raise SystemExit(msg)
    skin = skin.convert()

    # print skin.get_at((0,0))
    # screen.fill((255,255,255))
    screen.fill(skin.get_at((0, 0)))
    pygame.display.set_caption('PushTheBox.py')

    # create PushTheBox object
    skb = PushTheBox()
    skb.draw(screen, skin)

    #
    clock = pygame.time.Clock()
    pygame.key.set_repeat(200, 50)

    # main game loop
    while True:
        clock.tick(60)

        if skb.auto == 0:
            for event in pygame.event.get():
                if event.type == QUIT:
                    # print skb.solution
                    pygame.quit()
                    sys.exit()
                elif event.type == KEYDOWN:
                    if event.key == K_LEFT:
                        skb.move('l')
                        skb.draw(screen, skin)
                    elif event.key == K_UP:
                        skb.move('u')
                        skb.draw(screen, skin)
                    elif event.key == K_RIGHT:
                        skb.move('r')
                        skb.draw(screen, skin)
                    elif event.key == K_DOWN:
                        skb.move('d')
                        skb.draw(screen, skin)
                    elif event.key == K_BACKSPACE:
                        skb.undo()
                        skb.draw(screen, skin)
                    elif event.key == K_SPACE:
                        skb.redo()
                        skb.draw(screen, skin)
                elif event.type == MOUSEBUTTONUP and event.button == 1:
                    mousex, mousey = event.pos
                    mousex /= (skin.get_width() / 4)
                    mousey /= (skin.get_width() / 4)
                    skb.mouse(mousex, mousey)
                    skb.draw(screen, skin)
        else:
            skb.automove()
            skb.draw(screen, skin)

        pygame.display.update()
        pygame.display.set_caption(skb.solution.__len__().__str__() + '/' + skb.push.__str__() + ' - PushTheBox.py')


if __name__ == '__main__':
    start_game()