# ===============================================================================
# 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 logbook import Logger
from sqlalchemy.orm.collections import collection


pyfalog = Logger(__name__)


class HandledList(list):
    def filteredItemPreAssign(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.preAssignItemAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredItemIncrease(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.increaseItemAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredItemMultiply(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.multiplyItemAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredItemBoost(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.boostItemAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredItemForce(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.forceItemAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredChargePreAssign(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.preAssignChargeAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredChargeIncrease(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.increaseChargeAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredChargeMultiply(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.multiplyChargeAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredChargeBoost(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.boostChargeAttr(*args, **kwargs)
            except AttributeError:
                pass

    def filteredChargeForce(self, filter, *args, **kwargs):
        for element in self:
            try:
                if filter(element):
                    element.forceChargeAttr(*args, **kwargs)
            except AttributeError:
                pass

    def remove(self, thing):
        # We must flag it as modified, otherwise it not be removed from the database
        # @todo: flag_modified isn't in os x skel. need to rebuild to include
        # flag_modified(thing, "itemID")
        if thing.isInvalid:  # see GH issue #324
            thing.itemID = 0
        list.remove(self, thing)

    def sort(self, *args, **kwargs):
        # We need it here to prevent external users from accidentally sorting the list as alot of
        # external logic relies on keeping modules at their places
        raise NotImplementedError


class HandledModuleList(HandledList):

    def append(self, mod):
        emptyPosition = float("Inf")
        for i in range(len(self)):
            currMod = self[i]
            if currMod.isEmpty and not mod.isEmpty and currMod.slot == mod.slot:
                currPos = mod.position or i
                if currPos < emptyPosition:
                    emptyPosition = currPos

        if emptyPosition < len(self):
            mod.position = emptyPosition
            self.__toModule(emptyPosition, mod)
            if mod.isInvalid:
                self.__toDummy(mod.position)
        else:
            self.appendIgnoreEmpty(mod)

    @collection.appender
    def appendIgnoreEmpty(self, mod):
        mod.position = len(self)
        super().append(mod)
        if mod.isInvalid:
            self.remove(mod)

    def replace(self, idx, mod):
        try:
            oldMod = self[idx]
        except IndexError:
            return
        self.__toModule(idx, mod)
        if mod.isInvalid:
            self.__toModule(idx, oldMod)

    def replaceRackPosition(self, rackPosition, mod):
        listPositions = []
        for currPos in range(len(self)):
            currMod = self[currPos]
            if currMod.slot == mod.slot:
                listPositions.append(currPos)
        listPositions.sort()
        try:
            modListPosition = listPositions[rackPosition]
        except IndexError:
            self.appendIgnoreEmpty(mod)
        else:
            oldMod = self[modListPosition]
            if mod.isEmpty:
                self.__toDummy(modListPosition)
            else:
                self.__toModule(modListPosition, mod)
                # If new module cannot be appended, restore old state
                if mod.isInvalid:
                    if oldMod.isEmpty:
                        self.__toDummy(modListPosition)
                    else:
                        self.__toModule(modListPosition, oldMod)

    def insert(self, idx, mod):
        mod.position = idx
        i = idx
        while i < len(self):
            self[i].position += 1
            i += 1
        HandledList.insert(self, idx, mod)
        if mod.isInvalid:
            self.remove(mod)

    def remove(self, mod):
        HandledList.remove(self, mod)
        oldPos = mod.position
        mod.position = None
        for i in range(oldPos, len(self)):
            self[i].position -= 1

    def free(self, idx):
        self.__toDummy(idx)

    def __toDummy(self, index):
        mod = self[index]
        if not mod.isEmpty:
            dummy = mod.buildEmpty(mod.slot)
            dummy.position = index
            self[index] = dummy
            mod.position = None

    def __toModule(self, index, mod):
        oldMod = self[index]
        mod.position = index
        self[index] = mod
        oldMod.position = None


class HandledDroneCargoList(HandledList):

    def find(self, item):
        for o in self:
            if o.item == item:
                yield o

    def findFirst(self, item):
        for o in self.find(item):
            return o

    def append(self, thing):
        HandledList.append(self, thing)
        if thing.isInvalid:
            self.remove(thing)

    def insert(self, idx, thing):
        HandledList.insert(self, idx, thing)
        if thing.isInvalid:
            self.remove(thing)


class HandledImplantList(HandledList):

    def append(self, implant):
        if implant.isInvalid:
            HandledList.append(self, implant)
            self.remove(implant)
            return
        if self.__slotCheck(implant):
            HandledList.append(self, implant)
            self.remove(implant)
            return
        HandledList.append(self, implant)

    def insert(self, idx, implant):
        if implant.isInvalid:
            HandledList.insert(self, idx, implant)
            self.remove(implant)
            return
        if self.__slotCheck(implant):
            HandledList.insert(self, idx, implant)
            self.remove(implant)
            return
        HandledList.insert(self, idx, implant)

    def makeRoom(self, implant):
        # if needed, remove booster that was occupying slot
        oldObj = next((i for i in self if i.slot == implant.slot), None)
        if oldObj is not None:
            pyfalog.info("Slot {0} occupied with {1}, replacing with {2}", implant.slot, oldObj.item.name, implant.item.name)
            position = self.index(oldObj)
            from gui.fitCommands.helpers import ImplantInfo
            implantInfo = ImplantInfo.fromImplant(oldObj)
            oldObj.itemID = 0  # hack to remove from DB. See GH issue #324
            self.remove(oldObj)
            return position, implantInfo
        return None, None

    def __slotCheck(self, implant):
        return any(i.slot == implant.slot for i in self)


class HandledBoosterList(HandledList):

    def append(self, booster):
        if booster.isInvalid:
            HandledList.append(self, booster)
            self.remove(booster)
            return
        if self.__slotCheck(booster):
            HandledList.append(self, booster)
            self.remove(booster)
            return
        HandledList.append(self, booster)

    def insert(self, idx, booster):
        if booster.isInvalid:
            HandledList.insert(self, idx, booster)
            self.remove(booster)
            return
        if self.__slotCheck(booster):
            HandledList.insert(self, idx, booster)
            self.remove(booster)
            return
        HandledList.insert(self, idx, booster)

    def makeRoom(self, booster):
        # if needed, remove booster that was occupying slot
        oldObj = next((b for b in self if b.slot == booster.slot), None)
        if oldObj is not None:
            pyfalog.info("Slot {0} occupied with {1}, replacing with {2}", booster.slot, oldObj.item.name, booster.item.name)
            position = self.index(oldObj)
            from gui.fitCommands.helpers import BoosterInfo
            boosterInfo = BoosterInfo.fromBooster(oldObj)
            oldObj.itemID = 0  # hack to remove from DB. See GH issue #324
            self.remove(oldObj)
            return position, boosterInfo
        return None, None

    def __slotCheck(self, booster):
        return any(b.slot == booster.slot for b in self)


class HandledSsoCharacterList(list):

    def append(self, character):
        old = next((x for x in self if x.client == character.client), None)
        if old is not None:
            pyfalog.warning("Removing SSO Character with same hash: {}".format(repr(old)))
            list.remove(self, old)

        list.append(self, character)


class HandledProjectedModList(HandledList):

    def append(self, proj):
        if proj.isInvalid:
            # we must include it before we remove it. doing it this way ensures
            # rows and relationships in database are removed as well
            HandledList.append(self, proj)
            self.remove(proj)
            return
        proj.projected = True
        HandledList.append(self, proj)
        # Remove non-projectable modules
        if not proj.item.isType("projected") and not proj.isExclusiveSystemEffect:
            self.remove(proj)

    def insert(self, idx, proj):
        if proj.isInvalid:
            # we must include it before we remove it. doing it this way ensures
            # rows and relationships in database are removed as well
            HandledList.insert(self, idx, proj)
            self.remove(proj)
            return
        proj.projected = True
        HandledList.insert(self, idx, proj)
        # Remove non-projectable modules
        if not proj.item.isType("projected") and not proj.isExclusiveSystemEffect:
            self.remove(proj)

    @property
    def currentSystemEffect(self):
        return next((m for m in self if m.isExclusiveSystemEffect), None)

    def makeRoom(self, proj):
        if proj.isExclusiveSystemEffect:
            # remove other system effects - only 1 per fit plz
            mod = self.currentSystemEffect

            if mod:
                pyfalog.info("System effect occupied with {0}, removing it to make space for {1}".format(mod.item.name, proj.item.name))
                position = self.index(mod)
                # We need to pack up this info, so whatever...
                from gui.fitCommands.helpers import ModuleInfo
                modInfo = ModuleInfo.fromModule(mod)
                self.remove(mod)
                return position, modInfo
        return None, None


class HandledProjectedDroneList(HandledDroneCargoList):

    def append(self, proj):
        proj.projected = True
        HandledList.append(self, proj)
        # Remove invalid or non-projectable drones
        if proj.isInvalid or not proj.item.isType("projected"):
            self.remove(proj)
            proj.projected = False

    def insert(self, idx, proj):
        proj.projected = True
        HandledList.insert(self, idx, proj)
        # Remove invalid or non-projectable drones
        if proj.isInvalid or not proj.item.isType("projected"):
            self.remove(proj)
            proj.projected = False


class HandledItem:
    def preAssignItemAttr(self, *args, **kwargs):
        self.itemModifiedAttributes.preAssign(*args, **kwargs)

    def increaseItemAttr(self, *args, **kwargs):
        self.itemModifiedAttributes.increase(*args, **kwargs)

    def multiplyItemAttr(self, *args, **kwargs):
        self.itemModifiedAttributes.multiply(*args, **kwargs)

    def boostItemAttr(self, *args, **kwargs):
        self.itemModifiedAttributes.boost(*args, **kwargs)

    def forceItemAttr(self, *args, **kwargs):
        self.itemModifiedAttributes.force(*args, **kwargs)


class HandledCharge:
    def preAssignChargeAttr(self, *args, **kwargs):
        self.chargeModifiedAttributes.preAssign(*args, **kwargs)

    def increaseChargeAttr(self, *args, **kwargs):
        self.chargeModifiedAttributes.increase(*args, **kwargs)

    def multiplyChargeAttr(self, *args, **kwargs):
        self.chargeModifiedAttributes.multiply(*args, **kwargs)

    def boostChargeAttr(self, *args, **kwargs):
        self.chargeModifiedAttributes.boost(*args, **kwargs)

    def forceChargeAttr(self, *args, **kwargs):
        self.chargeModifiedAttributes.force(*args, **kwargs)
