'''  
    Copyright (c) 2012, Steve Peck aka gr3yh47 <gr3yh47@gmail.com>
    See COPYING.txt for full license text


    map.py
    This file is part of Bright Flame Tactics.

    Bright Flame Tactics is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Bright Flame Tactics is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Bright Flame Tactics.  If not, see <http://www.gnu.org/licenses/>.
    
    some code derived from qq, Copyright (c) 2008, 2009 Radomir Dopieralski <qq@sheep.art.pl>. see qq.rar for source and license
'''
import ConfigParser
import pygame
import pygame.locals as pg
import random
import copy
import os
import sys
sys.path.append(os.path.join('.', 'libs', 'pytmx'))
import pytmx
from collections import deque, defaultdict


'''GLOBALS'''
#x and y offsets to iterate through for looking at adjacent tiles
#     N   W  S  E
DX = [0, -1, 0, 1]
DY = [-1, 0, 1, 0]

# Dimensions of the map tiles
TILE_WPIX, TILE_HPIX = 32, 32



class Event(object):
    ''' holds text, item, and effects from 'traps' or event triggers
    when a certain map square is landed on'''
    def __init__(self):
        pass
    

class Map(object):
    '''Map stores all tiles on the level and some other level details'''
    def __init__(self, map_file = 'level0.tmx'):
        self.params = {} #tile parameters by symbol
        self.grid = [] #map grid of symbols
        self.tileset = '' #Main tileset file
        # self.tc = None #will hold TileCache()
        # self.thc = {} #tile highlight cache - will be a dict of highlight TileCaches
        self.width = 0
        self.height = 0  
        self.tiles = []        
        self.start_list = {}
        self.start_list['a'] = []
        self.start_list['b'] = []
        
        self.load_file(os.path.join('configs', map_file))
        
        self.high = {}        
        for t_color in ['blue', 'red']:
            self.high[t_color] = pygame.image.load(os.path.join('.', 'graphics', t_color + 'high.png')).convert_alpha()
            
            
    def __str__(self):  
        map_string = 'the Map is ' + str(self.width) + 'x' + str(self.height) + '\n'
        for y in xrange(self.height):
            for x in xrange(self.width):
                map_string += self.tiles[x][y].c + ' '
            map_string += '\n'
        return map_string
    
    def load_file(self, tmx_file):
        '''loads the map file and creates the TileCache() for the map'''
        b_target = pygame.image.load(os.path.join('.', 'graphics', 'btarget.png'))
        a_target = pygame.image.load(os.path.join('.', 'graphics', 'atarget.png'))
        tmx_file = os.path.join(tmx_file)
        import tmxloader
        tiled_map = tmxloader.load_pygame(tmx_file, pixelalpha = True)                    

        self.height, self.width = tiled_map.height, tiled_map.width
        for x in xrange(self.width):
            row = []
            for y in xrange(self.height):
                tile = Tile(x, y)
                tile.image = pygame.Surface((32, 32))
                tile.overlay = None
                row.append(tile)
            self.tiles.append(row)
            
        
        tile_width = 32
        tile_height = 32
        img_num = 0
        for layer in xrange(len(tiled_map.tilelayers)):
            for x in xrange(tiled_map.width):
                for y in xrange(tiled_map.height):
                    tile_img = tiled_map.getTileImage(x, y, layer)
                    
                    if tile_img:
                        tile_img.convert_alpha()
                        self.tiles[x][y].image.blit(tile_img, (0, 0))
                        img_num += 1
                        if layer == 4: #blocking layer
                            self.tiles[x][y].traversable = False
                        elif layer == 6: #overlay layers
                            if not self.tiles[x][y].overlay: #if overlay uninitialized
                                self.tiles[x][y].overlay = pygame.Surface((32, 32)).convert_alpha()
                                self.tiles[x][y].overlay.fill((255, 255, 255, 0))
                            self.tiles[x][y].overlay.blit(tile_img, (0, 0))
                        elif layer == 7: #A team target for ctf
                            self.a_condition = (x, y)
                            if not self.tiles[x][y].overlay: #if overlay uninitialized
                                self.tiles[x][y].overlay = pygame.Surface((32, 32)).convert_alpha()
                                self.tiles[x][y].overlay.fill((255, 255, 255, 0))
                            self.tiles[x][y].overlay.blit(a_target, (0, 0))
                        elif layer == 8: #B team target for ctf
                            self.b_condition = (x, y)
                            if not self.tiles[x][y].overlay: #if overlay uninitialized
                                self.tiles[x][y].overlay = pygame.Surface((32, 32)).convert_alpha()
                                self.tiles[x][y].overlay.fill((255, 255, 255, 0))
                            self.tiles[x][y].overlay.blit(b_target, (0, 0))
                        elif layer == 0: #team A starting pos layer
                            self.start_list['a'].append((x, y))
                        elif layer == 1: #team B starting pos layer
                            self.start_list['b'].append((x, y))
        
        
        
        # print img_num        
        # screen = pygame.display.set_mode((800, 600))
        # for y in xrange(map_height):
            # for x in xrange(map_width):
                # screen.blit(self.tiles[x][y].image, (x * tile_width, y * tile_height))
                # pygame.display.flip()
        
       
       
    def get_valid_moves(self, player):
        '''this function gets the valid moves based on 
        player's starting position and mvt stat'''
        x, y = player.pos
        if player.mounted: #so that this method can be used for both mvt phases for mounted units
            mvt = player.mounted
        else:
            mvt = player.get_stat('mvt')
        affinity = player.team #used to check if occupied squares are traversable
        valid_moves = []
        my_queue = deque([])
        my_queue.append([self.tiles[x][y], 0]) #queue starts with just the source tile, which is level 0
        valid_moves.append(self.tiles[x][y])
        self.tiles[x][y].explored = 1
        
        #Main BFS loop
        while my_queue:
            this = my_queue.popleft() #pop the next object
            if this[1] < mvt: #if we are still in valid movement range...
                #iterate through neighboring traversable tiles, add them to the queue and valid moves list, mark them as explored
                for item in this[0].examine(affinity, self): 
                    my_queue.append([item, this[1] + 1]) #appending neighboring tiles
                    valid_moves.append(item)
                    item.explored = 1
        
        #resetting each tile's explored value, grab valid_coords
        valid_coords = [] #list of coordinates only, used in mvt phase
        for item in valid_moves:
            item.explored = 0
            valid_coords.append((item.x, item.y))
        return valid_moves, valid_coords  
        
    def get_valid_atk(self, player, valid_coords = None):
        '''gets valid attack squares by examining valid_moves'''
        if not valid_coords:
            valid_coords = (player.pos, )
        valid_atk = [] #list of valid attackable tiles that are not mvt tiles
        ranges = set() #only unique ranges matter
        ranges.add(player.p_weapon.d_range)
        if player.p_weapon.i_range:
            for val in player.p_weapon.i_range:
                ranges.add(val)
        if player.s_weapon:
            ranges.add(player.s_weapon.d_range)
            if player.s_weapon.i_range:
                for val in player.s_weapon.i_range:
                    ranges.add(val)
        #get the largest range, used in BFS    
        max_range = max([range for range in ranges])
        my_queue = deque([])
        #run a complete BFS on each valid mvt tile individually
        landable_coords = []
        for (x, y) in valid_coords:
            if self.tiles[x][y].affinity != player.team or (x, y) == ((player.x, player.y) or player.pos):
                landable_coords.append((x, y))
        for (x, y) in landable_coords:
            my_queue.append([self.tiles[x][y], 0])
            self.tiles[x][y].explored = 1
            explored_list = [] #list of explored tiles in this bfs
            #main BFS loop
            while my_queue:
                this = my_queue.popleft()
                explored_list.append(this[0])
                if this[1] in ranges and (this[0].x, this[0].y) not in landable_coords and this[0].affinity != player.team:
                    valid_atk.append(this[0])
                if this[1] < max_range:
                    for item in this[0].examine(None, self):
                        my_queue.append([item, this[1] + 1])
                        item.explored = 1
                            
            #reset explored tiles to 0 for next iteration
            for item in explored_list: 
                item.explored = 0
        return valid_atk
    
    def highlight_valid_squares(self, valid_moves, valid_atk = None):
        '''displays valid move squares on screen'''
        high_squares = []
        for i in xrange(len(valid_moves)):
            valid_moves[i].high = 'blue'
            high_squares.append(valid_moves[i])
            
        if valid_atk:
            for i in xrange(len(valid_atk)):
                valid_atk[i].high = 'red'
                high_squares.append(valid_atk[i])
        return high_squares
      
    def reset_highlights(self, high_squares):
        '''resets tile highlights when necessary'''
        #putting all the tiles back to normal once we are finished
        for i in xrange(len(high_squares)):
            high_squares[i].high = None
  
    def print_valid_moves(self, valid_moves):
        '''DEPRECATED: this prints the valid moves to the command line'''
        original_contents = []
        #for every valid move tile, change the symbol/color to indicate valid tiles
        for i in xrange(len(valid_moves)):
            original_contents.append(valid_moves[i].c)
            valid_moves[i].c = '*'
            valid_moves[i].highlight = 'Blue'
        print 'valid moves are *:'
        print self
        #putting all the tiles back to normal once we are finished
        for i in xrange(len(valid_moves)):
            valid_moves[i].c = original_contents[i]
            valid_moves[i].highlight = None
            #x, y = valid_moves[i].x, valid_moves[i].y
            #self.grid[x][y].c = original_contents[i]
            #self.grid[x][y].highlight = None

    # def print_map(self):
        # '''DEPRECATED: Prints map to command line as ASCII'''
        # print 'the Map is %dx%d' % (self.width, self.height)
        # for x in xrange(5):
            # for y in xrange(5):
                # print self.grid[x][y].c, 
            # print " "
    
    def render(self):
        '''blits each map Tile()'s image to a surface and returns it'''
        #screen = pygame.display.get_surface()
        image = pygame.Surface((self.width*TILE_WPIX, self.height*TILE_HPIX))
        overlays = {}
        #screen = pygame.display.set_mode((800, 600))
        for map_x in xrange(len(self.tiles)):
            for map_y in xrange(len(self.tiles[map_x])):
                tile = self.tiles[map_x][map_y]
                #set the base image to the non-highlighted tile
                tile_image = pygame.Surface((32, 32))
                tile_image.blit(tile.image, (0, 0))
                #if the tile has a highlight, use the correct color highlight instead
                if tile.high:
                    tile_image.blit(self.high[tile.high], (0, 0))
                if tile.overlay:
                    tile_image.blit(tile.overlay, (0, 0))
                image.blit(tile_image, (map_x*TILE_HPIX, map_y*TILE_WPIX))
                #screen.blit(tile_image, (map_x*TILE_HPIX, map_y*TILE_WPIX))
                #pygame.display.flip()
                #print (map_x*TILE_HPIX, map_y*TILE_WPIX)
        return image, overlays
        
    def render_overlays(self):
        overlays = []
        for map_x in xrange(len(self.tiles)):
            for map_y in xrange(len(self.tiles[map_x])):
                tile = self.tiles[map_x][map_y]
                if tile.overlay:
                    overlays.append((tile.overlay, (map_x * 32, map_y * 32)))
        return overlays
                    
        
        
class TileCache(object):
    '''caches tile graphics for easy access'''
    
    def __init__(self, filename, width = 32, height = None, multi = None, path = '', charsprite = False):
        self.width = width
        self.height = height or width
        if multi:
            self.caches = {}
            for action in multi:
                truefile = os.path.join(path, filename + action + '.png')
                self.caches[action] = self._load_tile_table(truefile, self.width, self.height, charsprite)

            if 'bow' in multi:
                table = []
                for j in xrange(3, 0, -1):
                    tiles = []
                    for i in xrange(0, 4):
                        img = pygame.Surface((320, 320), 1)
                        img = img.convert_alpha()
                        img.fill((255, 255, 255, 0))
                        img.blit(self.caches['bow'][j][i], (0, 0))
                        tiles.append(img)
                    table.append(tiles)
                self.caches['bow'].extend(table)
        else:
            self.cache = self._load_tile_table(os.path.join(path, filename), self.width, self.height, charsprite)
                
    '''def __getitem__(self, filename):
        """Return a table of tiles, load it from disk if needed."""

        key = (filename)
        try:
            return self.cache[key]
        except KeyError:
            tile_table = self._load_tile_table(filename, self.width, self.height)                                               
            self.cache[key] = tile_table
            return tile_table'''

    def _load_tile_table(self, filename, width, height, charsprite):
        '''Load an image and split it into tiles.'''
        image = pygame.image.load(os.path.join('.', 'graphics', filename)).convert_alpha()
        image_width, image_height = image.get_size()
        tile_table = []  
        if charsprite:
            for tile_x in xrange(0, image_width/width):
                line = []
                tile_table.append(line)
                for tile_y in xrange(0, image_height/height):
                    rect = (tile_x*width, tile_y*height, width, height)
                    img = image.subsurface(rect)
                    tile = pygame.Surface((320, 320), 1)
                    tile = tile.convert_alpha()
                    tile.fill((255, 255, 255, 0))
                    pos_x = (tile.get_width() - width)/2
                    tile.blit(img, (pos_x, pos_x))
                    line.append(tile)
            return tile_table    
        else:
            for tile_x in xrange(0, image_width/width):
                line = []
                tile_table.append(line)
                for tile_y in xrange(0, image_height/height):
                    rect = (tile_x*width, tile_y*height, width, height)
                    line.append(image.subsurface(rect))
            return tile_table  

class Tile(object):
    '''Tile class represents each square in the map.grid and all relevant data'''
    def __init__(self, x, y): #defaults to empty traversable square   
        self.explored = 0 #used for BFS in Map.get_valid_moves()
        self.is_occupied = False
        self.character = None #reference to character occupying the space. may not be needed
        self.affinity = None #which team is the space occupied by, if any
        self.mvt_cost = 1 #i may not do anything with this.
        self.high = None #highlight for graphical purpose
        #self.c = c #contents of the tile for command line printing. may eventually be a sprite/player reference
        #self.oc = c #original contents for resetting tiles. this does not change
        self.x = x #location of tile on map by coordinates
        self.y = y
        self.c = '.'
        self.oc = '.'
        #self.traversable = int(params[c]['traversable']) #walls and such are not traversable
        #self.img_x = int(params[c]['img_x'])
        #self.img_y = int(params[c]['img_y'])
        self.traversable = True
        
        
    def vacate(self):
        self.character = None
        self.affinity = None 
        self.is_occupied = False
     
    def occupy(self, character):
        self.character = character
        character.x, character.y = self.x, self.y
        self.affinity = character.team
        self.is_occupied = True
    
    def examine(self, affinity, my_map):#, can_go_over = False):  
        '''looks at adjacent tiles. used for mvt and atk highlighting'''
        traversable = []
        #using the global offsets for adjacent tiles
        for x, y in zip(DX, DY):
            xi, yi = self.x + x, self.y + y
            if xi >=0 and yi >=0: #no negative indexes
                #index error handling for values that are off the board
                try:
                    if my_map.tiles[xi][yi].is_traversable(affinity) and not my_map.tiles[xi][yi].explored: 
                        traversable.append(my_map.tiles[self.x + x][self.y + y])
                    # elif can_go_over
                        # traversable.append(my_map.tiles[self.x + x][self.y + y])
                except IndexError:
                    pass
        return traversable   

    def is_traversable(self, affinity):
        '''determines if a single Tile() is traversable for a given char'''
        #if the tile is traversable and the affinity matches or the tile has no affinity...
        #... the third item in the parens is so this can be used for Map.get_valid_attack, which passes affinity = None 
        #print 'checking traversability of ', self.x, self.y, self.traversable, 'and (', self.affinity == affinity, 'or not', self.affinity, 'or not', affinity, ')'
        if self.traversable and (self.affinity == affinity or not self.affinity or not affinity):
            return True
        else:
            return False


