#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos.  If not, see <http://www.gnu.org/licenses/>.
#===============================================================================


from sqlalchemy.orm import validates, reconstructor
from itertools import chain

from eos.effectHandlerHelpers import HandledItem, HandledImplantBoosterList
import eos.db
import eos
import eos.types
import logging

logger = logging.getLogger(__name__)

class Character(object):
    __itemList = None
    __itemIDMap = None
    __itemNameMap = None

    @classmethod
    def getSkillList(cls):
        if cls.__itemList is None:
            cls.__itemList = eos.db.getItemsByCategory("Skill")

        return cls.__itemList

    @classmethod
    def setSkillList(cls, list):
        cls.__itemList = list

    @classmethod
    def getSkillIDMap(cls):
        if cls.__itemIDMap is None:
            map = {}
            for skill in cls.getSkillList():
                map[skill.ID] = skill

            cls.__itemIDMap = map

        return cls.__itemIDMap

    @classmethod
    def getSkillNameMap(cls):
        if cls.__itemNameMap is None:
            map = {}
            for skill in cls.getSkillList():
                map[skill.name] = skill

            cls.__itemNameMap = map

        return cls.__itemNameMap

    @classmethod
    def getAll5(cls):
        all5 = eos.db.getCharacter("All 5")

        if all5 is None:
            # We do not have to be afraid of committing here and saving
            # edited character data. If this ever runs, it will be during the
            # get character list phase when pyfa first starts
            all5 = Character("All 5", 5)
            eos.db.save(all5)

        return all5

    @classmethod
    def getAll0(cls):
        all0 = eos.db.getCharacter("All 0")

        if all0 is None:
            all0 = Character("All 0")
            eos.db.save(all0)

        return all0

    def __init__(self, name, defaultLevel=None, initSkills=True):
        self.savedName = name
        self.__owner = None
        self.defaultLevel = defaultLevel
        self.__skills = []
        self.__skillIdMap = {}
        self.dirtySkills = set()

        if initSkills:
            for item in self.getSkillList():
                self.addSkill(Skill(item.ID, self.defaultLevel))

        self.__implants = HandledImplantBoosterList()
        self.apiKey = None

    @reconstructor
    def init(self):
        self.__skillIdMap = {}
        for skill in self.__skills:
            self.__skillIdMap[skill.itemID] = skill
        self.dirtySkills = set()

    def apiUpdateCharSheet(self, skills):
        del self.__skills[:]
        self.__skillIdMap.clear()
        for skillRow in skills:
            self.addSkill(Skill(skillRow["typeID"], skillRow["level"]))

    @property
    def ro(self):
        return self == self.getAll0() or self == self.getAll5()

    @property
    def owner(self):
        return self.__owner

    @owner.setter
    def owner(self, owner):
        self.__owner = owner

    @property
    def name(self):
        return self.savedName if not self.isDirty else "{} *".format(self.savedName)

    @name.setter
    def name(self, name):
        self.savedName = name

    @property
    def skills(self):
        return self.__skills

    def addSkill(self, skill):
        if skill.itemID in self.__skillIdMap:
            oldSkill = self.__skillIdMap[skill.itemID]
            if skill.level > oldSkill.level:
                # if new skill is higher, remove old skill (new skill will still append)
                self.__skills.remove(oldSkill)
            else:
                return

        self.__skills.append(skill)
        self.__skillIdMap[skill.itemID] = skill

    def removeSkill(self, skill):
        self.__skills.remove(skill)
        del self.__skillIdMap[skill.itemID]

    def getSkill(self, item):
        if isinstance(item, basestring):
            item = self.getSkillNameMap()[item]
        elif isinstance(item, int):
            item = self.getSkillIDMap()[item]

        skill = self.__skillIdMap.get(item.ID)

        if skill is None:
            skill = Skill(item, self.defaultLevel, False, True)
            self.addSkill(skill)

        return skill

    @property
    def implants(self):
        return self.__implants

    @property
    def isDirty(self):
        return len(self.dirtySkills) > 0

    def saveLevels(self):
        if self == self.getAll5() or self == self.getAll0():
            raise ReadOnlyException("This character is read-only")

        for skill in self.dirtySkills.copy():
            skill.saveLevel()

        self.dirtySkills = set()
        eos.db.commit()

    def revertLevels(self):
        for skill in self.dirtySkills.copy():
            skill.revert()

        self.dirtySkills = set()

    def filteredSkillIncrease(self, filter, *args, **kwargs):
        for element in self.skills:
            if filter(element):
                element.increaseItemAttr(*args, **kwargs)

    def filteredSkillMultiply(self, filter, *args, **kwargs):
        for element in self.skills:
            if filter(element):
                element.multiplyItemAttr(*args, **kwargs)

    def filteredSkillBoost(self, filter, *args, **kwargs):
        for element in self.skills:
            if filter(element):
                element.boostItemAttr(*args, **kwargs)

    def calculateModifiedAttributes(self, fit, runTime, forceProjected = False):
        if forceProjected: return
        for skill in self.skills:
            fit.register(skill)
            skill.calculateModifiedAttributes(fit, runTime)

    def clear(self):
        c = chain(
            self.skills,
            self.implants
        )
        for stuff in c:
            if stuff is not None and stuff != self:
                stuff.clear()

    def __deepcopy__(self, memo):
        copy = Character("%s copy" % self.name, initSkills=False)
        copy.apiKey = self.apiKey
        copy.apiID = self.apiID

        for skill in self.skills:
            copy.addSkill(Skill(skill.itemID, skill.level, False, skill.learned))

        return copy

    @validates("ID", "name", "apiKey", "ownerID")
    def validator(self, key, val):
        map = {"ID": lambda val: isinstance(val, int),
               "name" : lambda val: True,
               "apiKey" : lambda val: val is None or (isinstance(val, basestring) and len(val) > 0),
               "ownerID" : lambda val: isinstance(val, int) or val is None}

        if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
        else: return val

    def __repr__(self):
        return "Character(ID={}, name={}) at {}".format(
            self.ID, self.name, hex(id(self))
        )

class Skill(HandledItem):
    def __init__(self, item, level=0, ro=False, learned=True):
        self.__item = item if not isinstance(item, int) else None
        self.itemID = item.ID if not isinstance(item, int) else item
        self.__level = level if learned else None
        self.commandBonus = 0
        self.build(ro)

    @reconstructor
    def init(self):
        self.build(False)
        self.__item = None

    def build(self, ro):
        self.__ro = ro
        self.__suppressed = False
        self.activeLevel = self.__level

    def saveLevel(self):
        self.__level = self.activeLevel

        if self in self.character.dirtySkills:
            self.character.dirtySkills.remove(self)

    def revert(self):
        self.level = self.__level

    @property
    def isDirty(self):
        return self.__level != self.activeLevel

    @property
    def learned(self):
        return self.activeLevel is not None

    @property
    def level(self):
        return self.activeLevel or 0

    @level.setter
    def level(self, level):
        if (level < 0 or level > 5) and level is not None:
            raise ValueError(str(level) + " is not a valid value for level")

        if hasattr(self, "_Skill__ro") and self.__ro == True:
            raise ReadOnlyException()

        self.activeLevel = level
        self.character.dirtySkills.add(self)

        if self.activeLevel == self.__level and self in self.character.dirtySkills:
            self.character.dirtySkills.remove(self)


    @property
    def item(self):
        if self.__item is None:
            self.__item = item = Character.getSkillIDMap().get(self.itemID)
            if item is None:
                #This skill is no longer in the database and thus invalid it, get rid of it.
                self.character.removeSkill(self)

        return self.__item

    def getModifiedItemAttr(self, key):
        if key in self.item.attributes:
            return self.item.attributes[key].value
        else:
            return None

    def calculateModifiedAttributes(self, fit, runTime):
        if self.__suppressed: # or not self.learned - removed for GH issue 101
            return

        item = self.item
        if item is None:
            return

        for effect in item.effects.itervalues():
            if effect.runTime == runTime and effect.isType("passive") and (not fit.isStructure or effect.isType("structure")):
                try:
                    effect.handler(fit, self, ("skill",))
                except AttributeError:
                    continue

    def clear(self):
        self.__suppressed = False
        self.commandBonus = 0

    def suppress(self):
        self.__suppressed = True

    def isSuppressed(self):
        return self.__suppressed

    @validates("characterID", "skillID", "level")
    def validator(self, key, val):
        if hasattr(self, "_Skill__ro") and self.__ro == True and key != "characterID":
            raise ReadOnlyException()

        map = {"characterID": lambda val: isinstance(val, int),
               "skillID" : lambda val: isinstance(val, int)}

        if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
        else: return val

    def __deepcopy__(self, memo):
        copy = Skill(self.item, self.level, self.__ro)
        return copy

    def __repr__(self):
        return "Skill(ID={}, name={}) at {}".format(
            self.item.ID, self.item.name, hex(id(self))
        )

class ReadOnlyException(Exception):
    pass
