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


    items.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 os
from collections import deque, defaultdict
from map import *


class Inventory(object):
    def __init__(self, player):
        self.items = [] #list of items
        self.player = player
        self.bg = pygame.image.load(os.path.join('.', 'graphics', 'inventorybg.png')).convert()
        self.image = pygame.Surface((200, 300))
        self.wpn_image = pygame.Surface((200, 300))
        self.e_icon = pygame.image.load(os.path.join('.', 'graphics', 'equipped_icon.png')).convert_alpha()        
        self.oor_icon = pygame.image.load(os.path.join('.', 'graphics', 'oor_icon.png')).convert_alpha()
        self.checkbox = pygame.image.load(os.path.join('.', 'graphics', 'equippable.png')).convert_alpha()
        self.equipment = {}
        font_file = os.path.join('.', 'fonts', 'MedievalSharp.ttf')
        this_font = pygame.font.Font(font_file, 24)
        self.choose_wpn = pygame.Surface((200, 100))
        self.choose_wpn = self.choose_wpn.convert_alpha()
        self.choose_wpn.fill((255, 255, 255, 0))
        color = (0, 0, 0)
        choose = this_font.render('Choose', 1, color)
        weapon = this_font.render('Weapon', 1, color)
        txt_x = (200 - choose.get_width()) / 2
        self.choose_wpn.blit(choose, (txt_x, 20))
        txt_x = (200 - weapon.get_width()) / 2
        self.choose_wpn.blit(weapon, (txt_x, 60))
        self.image_update()
        
    def add(self, item, count = 1):
        item.count = count
        item.owner = self.player
        self.items.append(item)
    
    def remove(self, item):
        self.items.remove(item)
        
    def show(self, highlight = 0, wpn_only = False, dist = None):
        self.sort()
        img_y = 150
        self.image_update()
        for i in xrange(len(self.items)):
            if self.items[i].is_equippable:
                self.wpn_image.blit(self.checkbox, (10, img_y))
                self.image.blit(self.checkbox, (10, img_y))
                self.wpn_image.blit(self.items[i].inv_image, (25, img_y))
                if self.items[i].is_equipped:
                    self.image.blit(self.e_icon, (10, img_y))
                    self.wpn_image.blit(self.e_icon, (10, img_y))
                if i == highlight:
                    self.wpn_image.blit(self.items[i].inv_high_image, (25, img_y))
                if dist:
                    if dist not in self.items[i].ranges:
                        self.wpn_image.blit(self.oor_icon, (10, img_y))
            if i == highlight:
                self.image.blit(self.items[i].inv_high_image, (25, img_y))
                
            else:
                self.image.blit(self.items[i].inv_image, (25, img_y))
            img_y += 16
        if wpn_only:
            return self.wpn_image
        else:
            return self.image
            
    def image_update(self):
        self.image.blit(self.bg, (0, 0))
        self.image.blit(self.player.resize(), (-272, -250))
        self.player.collection.refresh()
        self.image.blit(self.player.collection.image, (88, 10))
        
        self.wpn_image.blit(self.bg, (0, 0))
        self.wpn_image.blit(self.choose_wpn, (0, 0))
        
    
    def sort(self):
        wpns = [item for item in self.items if item.is_weapon]
        items = [item for item in self.items if not item.is_weapon]
        self.items = wpns + items
    

class Item(object):
    '''base class for all items in the game''' 
    def __init__(self, name = 'Item', is_weapon = False, is_consumable = True, is_equippable = False, Effect = None):
        self.player = None
        self.is_weapon = is_weapon
        self.is_consumable = is_consumable
        self.is_equippable = is_equippable
        self.name = name
        this_font = pygame.font.Font(os.path.join('.', 'fonts', 'UbuntuMono-R.ttf'), 12)
        color = (0, 0, 0)
        self.inv_image = pygame.Surface((150,16)) #will be item name on dark blue 12px high 
        self.inv_image.fill((21, 108, 153))
        self.inv_high_image = pygame.Surface((150,16))#will be item name on lighter blue 12px high 
        self.inv_high_image.fill((0, 207, 223))
        name = this_font.render(self.name, 1, color)
        self.inv_image.blit(name, (0, 2))
        self.inv_high_image.blit(name, (0, 2))
        #self.image = pygame.image.load('.\\items\\' + self.name + 'item.png')
        
        
class Potion(Item):
    def __init__(self):
        super(Potion, self).__init__('Potion')
        self.image = pygame.image.load(os.path.join('.', 'graphics', 'potion.png'))        
      
    def use(self):
        self.owner.stats['hp'] = min(self.owner.stats['max'], self.owner.stats['hp'] + 10)
        self.owner.inventory.remove(self)
        
    def confirm(self):
        
        return self.image
        
        
class Equipment(Item):
    ''' Any Equipable item'''
    def __init__(self, name = 'Equipment', is_weapon = False):
        super(Equipment, self).__init__(name, is_weapon, False, True)
        self.bonus = defaultdict(lambda:0) #any entry called that doesnt exist will return 0 instead of throwing an error
        #self.owner = player
        self.is_equipped = False
        self.slot = None
        
    def equip(self):
        if self.is_equipped:
            pass
        else:
            if self.slot in self.owner.inventory.equipment:
                if self.owner.inventory.equipment[self.slot]:
                    self.owner.inventory.equipment[self.slot].unequip()
            self.owner.inventory.equipment[self.slot] = self
            self.is_equipped = True
            for stat, value in self.bonus.iteritems():
                self.owner.bonuses[stat] += value
        
    def unequip(self):
        self.is_equipped = False
        for stat, value in self.bonus.iteritems():
            self.owner.bonuses[stat] -= value
        self.owner.inventory.equipment[self.slot] = None
        
    # def trade(self, new_owner):
        # '''not going to be implemented'''
        # pass 
    
 
 
class Weapon(Equipment):
    '''base class for all weapons in the game'''   
    #d_range is direct combat range, i_range is the larger value of the indirect range i.e. 2 for 1-2 or 3 for 2-3)
    #weight is 1, 2 or 3. 1 is light, 2 is standard, 3 is heavy
    #damage will range from 1-5
    #minimum damage for the weakest weapon is 1
    def __init__(self, name = 'Weapon', damage = 1, type = 'None', weight = 1, d_range = 1, i_range = 0):
        super(Weapon, self).__init__(name, True)
        self.name = name
        self.damage = damage
        self.type = type
        self.d_range = d_range
        self.i_range = (i_range, i_range - 1)
        self.ranges = set(self.i_range)
        self.ranges.add(d_range)
        self.weight = weight
        #these will be modified directly by weapons that use them so that they dont have to be passed all the way up the inheritance
        self.piercing = 0
        self.two_handed = False
        #bonus is a dictionary of bonuses to a characters base stats, both positive and negative, max is +-2
        self.can_go_over = False #for bows and Tornado, lighting, spells
        self.aoe_size = 0 #default to 0 so that I can do if weapon.aoe_size
        self.core = None #weapon core stat, like str for swords or int for magic
        self.aux = None #weapon aux stat
        self.action = type.lower() #animation actionsuch as 'slash' 'spell' etc
        self.was_crit = False
        self.slot = 'wpn'
        
    def draw_weapon(self, char_frames):
        filename = os.path.join('tilesets', 'weapons', self.name.lower() + '.png')
        w_frames = TileCache(filename, 320).cache
        pos = 0
        t_frames = []
        for i in xrange(len(char_frames[self.action])):
            t_row = []
            for j in xrange(len(char_frames[self.action][i])):
                img = pygame.Surface((320, 320))
                img = img.convert_alpha()
                img.fill((255, 255, 255, 0))
                w_tile = w_frames[i][j]
                c_tile = char_frames[self.action][i][j]
                if j == 0:
                    img.blit(w_tile, (pos, pos))
                    img.blit(c_tile, (0, 0))
                else:
                    img.blit(c_tile, (0, 0))
                    img.blit(w_tile, (pos, pos))
                t_row.append(img)
            t_frames.append(t_row)
        char_frames[self.name] = t_frames
        
        #test for forcing drawing of frames during init
        # screen = pygame.display.set_mode((320, 320))
        # clock = pygame.time.Clock()
        
        # print len(char_frames[self.action])
        # print len(char_frames[self.action][0])
        # len_i = len(char_frames[self.action])
        # for j in xrange(4):
            # for i in xrange(len_i):
                # pygame.event.pump()
                # print 'frame: ', i, j
                # screen.fill((0, 0, 0))
                # screen.blit(char_frames[self.name][i][j], (0, 0))
                # pygame.display.flip()
                # clock.tick(15)
        
    def draw_projectiles(self):
        pass

    def attack(self, attacker, defender, map): 
        '''takes both characters and calculates damage and xp'''
        #resetting was_crit
        self.was_crit = False
        #print 'defender hp is: ', defender.get_stat('hp')
        # a luck roll which could double, not affect, or zero the result
        crit_skill = attacker.e_weapon.core
        crit_modifier = attacker.get_stat(crit_skill) - defender.get_stat(crit_skill)
        a_luck = (attacker.get_luck(crit_modifier) + 1)
        
        #attack value = attackers weapon core stat + 1/2 times attackers weapon aux stat + weapon damage
        attack = (attacker.get_stat(self.core) + attacker.get_stat(self.aux)/2 + self.damage) * a_luck
        #print 'attack = ', attack
        
        # a luck roll which could double, not affect, or zero the result
        dodge_modifier = defender.get_stat('spd') - attacker.get_stat('spd')
        d_luck = (defender.get_luck(dodge_modifier) + 1)
        
        if a_luck == 2: #critical hit
            self.was_crit = True
            extra_xp = attack / 4
        else:
            extra_xp = attack / 2
        
        if d_luck == 2: #dodge
            self.was_crit = False
            attack = 0
        
        #defense value - defender's defense stat + 1/2 times defender's core stat for attacker's weapon + defender armor rating incl. attacker's weapon's piercing;  
        defense = (defender.get_stat('dfn') + defender.get_stat(attacker.p_weapon.core)/2 + defender.armor_rating(self.piercing)) * d_luck
        #print 'defense = ' , defense
        damage = (max(attack - defense, 0))
        defender.stat_change('hp', -damage)
        #print 'after the hit, defender hp is: ', defender.stats['hp']
        attacker.action = self.name
        if attack: #if the attack was successful value will be > 0
            return 1 + extra_xp, damage  #1 + half damage xp for successful attack
        else:
            return 1, damage #1 xp for unsuccessful attack
  
    def equip(self):
        super(Weapon, self).equip()
        self.owner.e_weapon = self
        

class Sword(Weapon):
    '''Base class for Sword weapons. defaults are dagger stats.'''
    #minimum damage for a sword is 1 (dagger)
    #swords never have an indirect range stat so it is excluded in sword.init and passed to weapon.init statically. 
    #Same reasoning as above for Type = sword
    def __init__(self, name = 'Sword', damage = 1, weight = 1, d_range = 1): #these are ordered so that those more likely to change come first
        super(Sword, self).__init__(name, damage, 'Sword', weight, d_range, 0)
        self.core = 'str'
        self.aux = 'spd'
        self.action = 'sword'
        
##Dagger. it's crap but anyone can use it and it's fast       
class Dagger(Sword):
    def __init__(self):
        #the default values for sword happen to be the values for dagger except the name
        super(Dagger, self).__init__('Dagger')
        self.bonus['spd'] = 3 #it's light so it's very fast
        self.bonus['dfn'] = -2 #its short so it sucks at defense
        
     
##Rapier - Piercing, light, less damage than broadsword but partially ignores armor rating        
class Rapier(Sword):
    def __init__(self):
        super(Rapier, self).__init__('Rapier', 2)
        self.piercing = 1 #piercing value of 1 on a scale of 1-3
        self.bonus['spd'] = 2 #the rapier is light and normal sword length so it gives a speed bonus of 2
        self.bonus['dfn'] = -1 #a lighter weapon will have more trouble deflecting another weapon
        self.bonus['skl'] = 1 #why not throw in a skill modifier for an elegant weapon

##this is the ultimate middle of the road sword. exactly average. weight of 2 (standard) dmg of 3 from 1-5        
class Broadsword(Sword):
    def __init__(self):       
        super(Broadsword, self).__init__('Broadsword', 3, 2)
        self.bonus['lck'] = 2
    
##Longsword - 2 handed, better damage than Zweihander, heavy        
class Longsword(Sword):
    def __init__(self):
        super(Longsword, self).__init__('Longsword', 5, 3) #considering a direct combat range of 2
        self.two_handed = True
        self.bonus['dfn'] = 1
        self.bonus['spd'] = -1
              
##Zweihander - 2 handed, better defense bonus than longsword, heavy
class Zweihander(Sword):
    def __init__(self):
        super(Zweihander, self).__init__('Zweihander', 4, 3) #considering a direct combat range of 2
        self.two_handed = True
        self.bonus['dfn'] = 2
        self.bonus['spd'] = -1
        

class Spear(Weapon):
    '''Base class for Spear Weapons. defaults to pike stats'''
    def __init__(self, name = 'Spear', damage = 2, weight = 2, d_range = 1, i_range = 0):
        super(Spear, self).__init__(name, damage, 'Spear', weight, d_range, i_range)
        self.piercing = 2
        self.core = 'str'
        self.aux = 'skl'
        self.action = 'spear'
        
##pike - standard spear type. possibly more effective against mounted units but the current bonus system doesnt allow for this and idc about it       
class Pike(Spear):
    def __init__(self):
        super(Pike, self).__init__('Pike')
        self.bonus['dfn'] = 1
        
##lance - heavy, best piercing and damage, possible attack bonus for mounted units using this but the current system doesnt have this. 
class Lance(Spear):
    def __init__(self):
        super(Lance, self).__init__('Lance', 4, 3)
        self.piercing = 3
        self.bonus['dfn'] = 1
        self.bonus['spd'] = -1 #balance it out because of the epic piercing value.
        self.bonus['int'] = 1 #for the nobility of a lance?
    
##javelin: ranged and direct combat
class Trident(Spear):
    def __init__(self):
        super(Trident, self).__init__('Trident', 3, 3)
        self.piercing = 3
        self.bonus['dfn'] = 2
        self.bonus['str'] = 1

##half pike. 
class Spontoon(Spear):
    def __init__(self):
        super(Spontoon, self).__init__('Spontoon')
        self.piercing = 1
        self.bonus['spd'] = 2
        self.bonus['def'] = -1

##it's a long spear.
class Longspear(Spear):
    def __init__(self):
        super(Longspear, self).__init__('Longspear')
        self.bonus['dfn'] = 2
        self.bonus['spd'] = -1
        
  
class Bow(Weapon):    
    '''bows: except for crossbow, can attack units behind other units/obstacles. 
    tall obstacles (trees) different? (probably not)     defaults to shortbow'''
    def __init__(self, name = 'Bow', damage = 2, weight = 1, d_range = 0, i_range = 2):
        super(Bow, self).__init__(name, damage, 'Bow', weight, d_range, i_range)
        self.piercing = 1
        self.can_go_over = True
        self.action = 'bow'
        self.core = 'skl'
        self.aux = 'int'
        self.filename = os.path.join('tilesets', 'weapons', 'short')
        self.extra_filename = os.path.join('tilesets', 'weapons', 'arrow.png')
        self.projectile_file = os.path.join('tilesets', 'weapons', 'onearrow.png')
        self.tile_size = 64
   
    # def draw_weapon(self, frames):
        # weapon_frames = TileCache(self.filename, self.tile_size, None, ['bow']).caches['bow']
        # loc = 160 - self.tile_size/2
        # pos = (loc, loc)
        # frames = frames[self.action]
        # print len(frames)
        # for i in xrange(len(frames)):
            # for j in xrange(len(frames[i])):
                # print i, j
                # frames[i][j].blit(weapon_frames[i][j], pos)
        # if self.extra_filename:
            # extra_frames = TileCache(self.extra_filename, 64).cache
        # for w_frames, e_frames in zip(frames[:10], extra_frames):
            # for frame, extra in zip(w_frames, e_frames):
                # pos_x = (frame.get_width() - extra.get_width())/2
                # frame.blit(extra, (pos_x, pos_x))
        # self.draw_projectiles(frames)
        
    def draw_weapon(self, char_frames):
        char_frames['bow'] = char_frames['bow'][:10]
        super(Bow, self).draw_weapon(char_frames)
        self.draw_projectiles(char_frames[self.name])
        
    def draw_projectiles(self, frames):
        arrow_frames = TileCache(self.projectile_file, 64).cache[0]
        for frame in xrange(-4, 0):
            for dir in xrange(0, 4):
                base_px = 32 * (frame + 4)
                pos = (130 + base_px * DX[dir], 130 + base_px * DY[dir])
                frames[frame][dir].blit(arrow_frames[dir], pos)
      
##Shortbow - light, range 1-2, dmg 1
class Shortbow(Bow):
    def __init__(self):
        super(Shortbow, self).__init__('Shortbow')
        self.bonus['spd'] = 2
 
##longbow - standard, range 2-3, dmg 1-2
class Longbow(Bow):
    def __init__(self):
        super(Longbow, self).__init__('Longbow', 2, 2, 0, 3)
        self.bonus['skl'] = 1
        
##recurve bow - standard, range 1-2, dmg 2
class Recurvebow(Bow):
    def __init__(self):
        super(Recurvebow, self).__init__('Recurve Bow', 4, 2, 0, 2)
        self.piercing = 2
        self.bonus['spd'] = 1
        
##heavy bow - like a longbow but can be used in direct combat. heavy, range 1, dmg 2 (direct); range 2-3, dmg 2-3 (ranged)
class Heavybow(Bow):
    def __init__(self):
        super(Heavybow, self).__init__('Heavy Bow', 3, 3, 1, 3)
        self.piercing = 2 #only for ranged combat
        self.bonus['dfn'] = 1
        self.bonus['spd'] = -2
        
##crossbow - standard, range 1-2, dmg 3-4 cannot shoot over units
class Crossbow(Bow):
    def __init__(self):
        super(Crossbow, self).__init__('Crossbow', 4, 3, 0, 2)
        self.piercing = 2
        self.bonus['dfn'] = -2
        self.bonus['spd'] = 1
        self.can_go_over = False


class Spell(Weapon):
    '''spells base class. defaults to fireball stats'''
    def __init__(self, name = 'Spell', damage = 3, d_range = 2, i_range = 2):
        super(Spell, self).__init__(name, damage, 'Spell', 0, d_range, i_range)
        self.aoe_size = 0
        self.aoe_dmg = 0
        self.can_go_over = True
        self.core = 'int'
        self.aux = 'skl'

    def get_aoe(self, caster, target, map): 
        all_in_range = []
        for y in xrange(-self.aoe_size, self.aoe_size):
            for x in xrange(-self.aoe_size, self.aoe_size):
                if abs(x) + abs(y) <= self.aoe_size:
                    tile = map.tiles[target.x + x][target.y + y]
                    if tile.is_occupied and tile.affinity <> caster.team:
                        all_in_range.append(map.tiles[target.x + x][target.y + y].character)
                        
        splash_list = [character for character in all_in_range if character.team != caster.team]
        return splash_list
            
    def attack(self, attacker, defender, map):
        xp, damage = super(Spell, self).attack(attacker, defender, map)
        splash_list = self.get_aoe(attacker, defender, map)
        for character in splash_list:
            #unimplemented, returns a range
            #splash damage = aoe damage times caster luck minus affected character's armor rating only
            splash_dmg = round(max((self.aoe_dmg*(attacker.get_luck() + 1) - character.armor_rating(self.piercing)), 0))
            character.stat_change('hp', splash_dmg) 
        return xp, damage
    
##fireball - light splash damage
class Fire(Spell):
    def __init__(self):
        super(Fire, self).__init__('Fire')
        self.aoe_dmg = (1.0/3.0)
        self.aoe_size = 1
        
##icebeam - possibility of freezing enemy for 1 turn (currently unimplemented)
class Ice(Spell):
    def __init__(self):
        super(Ice, self).__init__('Ice', 3, 1, 3)
        self.piercing = 1
        self.bonus['dfn'] = 2
        
##heal
class Heal(Spell):
    def __init__(self):
        super(Heal, self).__init__('Heal', 0, 1, 0)
        self.friendly = True
    
    def heal(self, target, stat_modifier): #takes a fully calculated stat modifier based on the character's stats. this will be .25, .5, .75 or 1
        '''need to fix this formula. Also, this will be called from Heal.attack() override'''
        health = min(int(target.stats[max]*stat_modifier), (target.stats[max] - target.stats[hp])) #at most will restore hp to max
        target.stat_change(target, hp, health)
        
##lightning - ignores armor bonus
class Lightning(Spell):
    def __init__(self):
        super(Lightning, self).__init__('Lightning', 2, 0, 3)
        self.piercing = 3
        self.bonus['dfn'] = -1 #for balance and because in theory ice and fire could be used to block stuff but not lightning
        self.aoe_dmg = 0 #this will cause it to always do 1 splash damage
        self.aoe_size = 1
        
##Tornado - AOE spell
class Tornado(Spell):
    def __init__(self):
        super(Tornado, self).__init__('Tornado', 2, 0, 3)
        self.aoe_dmg = 2
        self.aoe_size = 2
        self.bonus['dfn'] = -1

'''stats in the bonus array:
    self.stats[hp] = stats[hp]  #hitpoints
    self.stats[str] = stats[str] #strength
    self.stats[dfn] = stats[dfn] #defense
    self.stats[spd] = stats[spd] #speed
    self.stats[int] = stats[int] #intelligence
    self.stats[lck] = stats[lck] #luck
    self.stats[lvl] = stats[lvl] #level
    self.stats[mvt] = stats[mvt] #movement
    self.stats[skl] = stats[skl] #skill (stat) might change to or be used sometimes as dexterity '''      
    
    
    
