# ===============================================================================
# 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/>.
# ===============================================================================

import sys

from sqlalchemy.sql import and_
from sqlalchemy import desc, select
from sqlalchemy import func

from eos.db import saveddata_session, sd_lock
from eos.db.saveddata.fit import fits_table, projectedFits_table
from eos.db.util import processEager, processWhere
from eos.saveddata.price import Price
from eos.saveddata.user import User
from eos.saveddata.ssocharacter import SsoCharacter
from eos.saveddata.damagePattern import DamagePattern
from eos.saveddata.targetProfile import TargetProfile
from eos.saveddata.character import Character
from eos.saveddata.implantSet import ImplantSet
from eos.saveddata.fit import Fit, FitLite
from eos.saveddata.module import Module
from eos.saveddata.miscData import MiscData
from eos.saveddata.override import Override

import eos.config

configVal = getattr(eos.config, "saveddataCache", None)
if configVal is True:
    import weakref

    itemCache = {}
    queryCache = {}

    def cachedQuery(type, amount, *keywords):
        itemCache[type] = localItemCache = weakref.WeakValueDictionary()
        queryCache[type] = typeQueryCache = {}

        def deco(function):
            localQueryCache = typeQueryCache[function] = {}

            def setCache(cacheKey, args, kwargs):
                items = function(*args, **kwargs)
                IDs = set()
                localQueryCache[cacheKey] = (isinstance(items, list), IDs)
                stuff = items if isinstance(items, list) else (items,)
                for item in stuff:
                    ID = getattr(item, "ID", None)
                    if ID is None:
                        # Some uncachable data, don't cache this query
                        del localQueryCache[cacheKey]
                        break
                    localItemCache[ID] = item
                    IDs.add(ID)

                return items

            def checkAndReturn(*args, **kwargs):
                useCache = kwargs.pop("useCache", True)
                cacheKey = []
                items = None
                cacheKey.extend(args)
                for keyword in keywords:
                    cacheKey.append(kwargs.get(keyword))

                cacheKey = tuple(cacheKey)
                info = localQueryCache.get(cacheKey)
                if info is None or not useCache:
                    items = setCache(cacheKey, args, kwargs)
                else:
                    l, IDs = info
                    if l:
                        items = []
                        for ID in IDs:
                            data = localItemCache.get(ID)
                            if data is None:
                                # Fuck, some of our stuff isn't cached it seems.
                                items = setCache(cacheKey, args, kwargs)
                                break
                            items.append(data)
                    else:
                        for ID in IDs:
                            items = localItemCache.get(ID)
                            if items is None:
                                items = setCache(cacheKey, args, kwargs)
                            break

                return items

            return checkAndReturn

        return deco

    def removeCachedEntry(type, ID):
        if type not in queryCache:
            return
        functionCache = queryCache[type]
        for _, localCache in functionCache.items():
            toDelete = set()
            for cacheKey, info in localCache.items():
                IDs = info[1]
                if ID in IDs:
                    toDelete.add(cacheKey)

            for cacheKey in toDelete:
                del localCache[cacheKey]

            if ID in itemCache[type]:
                del itemCache[type][ID]

elif callable(configVal):
    cachedQuery, removeCachedEntry = eos.config.gamedataCache
else:
    def cachedQuery(amount, *keywords):
        def deco(function):
            def checkAndReturn(*args, **kwargs):
                return function(*args, **kwargs)

            return checkAndReturn

        return deco

    def removeCachedEntry(*args, **kwargs):
        return


def sqlizeString(line):
    # Escape backslashes first, as they will be as escape symbol in queries
    # Then escape percent and underscore signs
    # Finally, replace generic wildcards with sql-style wildcards
    line = line.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_").replace("*", "%")
    return line


@cachedQuery(User, 1, "lookfor")
def getUser(lookfor, eager=None):
    if isinstance(lookfor, int):
        if eager is None:
            with sd_lock:
                user = saveddata_session.query(User).get(lookfor)
        else:
            eager = processEager(eager)
            with sd_lock:
                user = saveddata_session.query(User).options(*eager).filter(User.ID == lookfor).first()
    elif isinstance(lookfor, str):
        eager = processEager(eager)
        with sd_lock:
            user = saveddata_session.query(User).options(*eager).filter(User.username == lookfor).first()
    else:
        raise TypeError("Need integer or string as argument")
    return user


@cachedQuery(Character, 1, "lookfor")
def getCharacter(lookfor, eager=None):
    if isinstance(lookfor, int):
        if eager is None:
            with sd_lock:
                character = saveddata_session.query(Character).get(lookfor)
        else:
            eager = processEager(eager)
            with sd_lock:
                character = saveddata_session.query(Character).options(*eager).filter(Character.ID == lookfor).first()
    elif isinstance(lookfor, str):
        eager = processEager(eager)
        with sd_lock:
            character = saveddata_session.query(Character).options(*eager).filter(
                    Character.savedName == lookfor).first()
    else:
        raise TypeError("Need integer or string as argument")
    return character


def getCharacterList(eager=None):
    eager = processEager(eager)
    with sd_lock:
        characters = saveddata_session.query(Character).options(*eager).all()
    return characters


def getCharactersForUser(lookfor, eager=None):
    if isinstance(lookfor, int):
        eager = processEager(eager)
        with sd_lock:
            characters = saveddata_session.query(Character).options(*eager).filter(Character.ownerID == lookfor).all()
    else:
        raise TypeError("Need integer as argument")
    return characters


@cachedQuery(Fit, 1, "lookfor")
def getFit(lookfor, eager=None):
    if isinstance(lookfor, int):
        if eager is None:
            with sd_lock:
                fit = saveddata_session.query(Fit).get(lookfor)
        else:
            eager = processEager(eager)
            with sd_lock:
                fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == lookfor).first()
    else:
        raise TypeError("Need integer as argument")

    if fit and fit.isInvalid:
        with sd_lock:
            removeInvalid([fit])
        return None

    return fit


def getFitsWithShip(shipID, ownerID=None, where=None, eager=None):
    """
    Get all the fits using a certain ship.
    If no user is passed, do this for all users.
    """
    if isinstance(shipID, int):
        if ownerID is not None and not isinstance(ownerID, int):
            raise TypeError("OwnerID must be integer")
        filter = Fit.shipID == shipID
        if ownerID is not None:
            filter = and_(filter, Fit.ownerID == ownerID)

        filter = processWhere(filter, where)
        eager = processEager(eager)
        with sd_lock:
            fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
    else:
        raise TypeError("ShipID must be integer")

    return fits


def getRecentFits(ownerID=None, where=None, eager=None):
    eager = processEager(eager)
    with sd_lock:
        q = select((
            Fit.ID,
            Fit.shipID,
            Fit.name,
            Fit.modified,
            Fit.created,
            Fit.timestamp,
            Fit.notes
        )).order_by(desc(Fit.modified), desc(Fit.timestamp)).limit(50)
        fits = eos.db.saveddata_session.execute(q).fetchall()

    return fits


def getFitsWithModules(typeIDs, eager=None):
    """
    Get all the fits that have typeIDs fitted to them
    """

    if not hasattr(typeIDs, "__iter__"):
        typeIDs = (typeIDs,)

    eager = processEager(eager)
    with sd_lock:
        fits = removeInvalid(saveddata_session.query(Fit).join(Module).options(*eager).filter(Module.itemID.in_(typeIDs)).all())

    return fits


def countAllFits():
    with sd_lock:
        count = saveddata_session.query(Fit).count()
    return count


def countFitGroupedByShip():
    with sd_lock:
        count = eos.db.saveddata_session.query(Fit.shipID, func.count(Fit.shipID)).group_by(Fit.shipID).all()
    return count


def countFitsWithShip(lookfor, ownerID=None, where=None, eager=None):
    """
    Get all the fits using a certain ship.
    If no user is passed, do this for all users.
    """
    if ownerID is not None and not isinstance(ownerID, int):
        raise TypeError("OwnerID must be integer")

    if isinstance(lookfor, int):
        filter = Fit.shipID == lookfor
    elif isinstance(lookfor, list):
        if len(lookfor) == 0:
            return 0
        filter = Fit.shipID.in_(lookfor)
    else:
        raise TypeError("You must supply either an integer or ShipID must be integer")

    if ownerID is not None:
        filter = and_(filter, Fit.ownerID == ownerID)

    filter = processWhere(filter, where)
    eager = processEager(eager)
    with sd_lock:
        count = saveddata_session.query(Fit).options(*eager).filter(filter).count()

    return count


def getFitList(eager=None):
    eager = processEager(eager)
    with sd_lock:
        fits = removeInvalid(saveddata_session.query(Fit).options(*eager).all())

    return fits


def getFitListLite():
    with sd_lock:
        stmt = select([fits_table.c.ID, fits_table.c.name, fits_table.c.shipID])
        data = eos.db.saveddata_session.execute(stmt).fetchall()
    fits = []
    for fitID, fitName, shipID in data:
        fit = FitLite(id=fitID, name=fitName, shipID=shipID)
        fits.append(fit)
    return fits


@cachedQuery(Price, 1, "typeID")
def getPrice(typeID):
    if isinstance(typeID, int):
        with sd_lock:
            price = saveddata_session.query(Price).get(typeID)
    else:
        raise TypeError("Need integer as argument")
    return price


def clearPrices():
    with sd_lock:
        deleted_rows = saveddata_session.query(Price).delete()
    commit()
    return deleted_rows


def getMiscData(field):
    if isinstance(field, str):
        with sd_lock:
            data = saveddata_session.query(MiscData).get(field)
    else:
        raise TypeError("Need string as argument")
    return data


def getDamagePatternList(eager=None):
    eager = processEager(eager)
    with sd_lock:
        patterns = saveddata_session.query(DamagePattern).options(*eager).all()
    return patterns


def clearDamagePatterns():
    with sd_lock:
        deleted_rows = saveddata_session.query(DamagePattern).filter(DamagePattern.name != 'Uniform').delete()
    commit()
    return deleted_rows


def getTargetProfileList(eager=None):
    eager = processEager(eager)
    with sd_lock:
        patterns = saveddata_session.query(TargetProfile).options(*eager).all()
    return patterns


def clearTargetProfiles():
    with sd_lock:
        deleted_rows = saveddata_session.query(TargetProfile).delete()
    commit()
    return deleted_rows


def getImplantSetList(eager=None):
    eager = processEager(eager)
    with sd_lock:
        sets = saveddata_session.query(ImplantSet).options(*eager).all()
    return sets


@cachedQuery(DamagePattern, 1, "lookfor")
def getDamagePattern(lookfor, eager=None):
    if isinstance(lookfor, int):
        if eager is None:
            with sd_lock:
                pattern = saveddata_session.query(DamagePattern).get(lookfor)
        else:
            eager = processEager(eager)
            with sd_lock:
                pattern = saveddata_session.query(DamagePattern).options(*eager).filter(
                        DamagePattern.ID == lookfor).first()
    elif isinstance(lookfor, str):
        eager = processEager(eager)
        with sd_lock:
            pattern = saveddata_session.query(DamagePattern).options(*eager).filter(
                    DamagePattern.rawName == lookfor).first()
    else:
        raise TypeError("Need integer or string as argument")
    return pattern


@cachedQuery(TargetProfile, 1, "lookfor")
def getTargetProfile(lookfor, eager=None):
    if isinstance(lookfor, int):
        if eager is None:
            with sd_lock:
                pattern = saveddata_session.query(TargetProfile).get(lookfor)
        else:
            eager = processEager(eager)
            with sd_lock:
                pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
                    TargetProfile.ID == lookfor).first()
    elif isinstance(lookfor, str):
        eager = processEager(eager)
        with sd_lock:
            pattern = saveddata_session.query(TargetProfile).options(*eager).filter(
                TargetProfile.rawName == lookfor).first()
    else:
        raise TypeError("Need integer or string as argument")
    return pattern


@cachedQuery(ImplantSet, 1, "lookfor")
def getImplantSet(lookfor, eager=None):
    if isinstance(lookfor, int):
        if eager is None:
            with sd_lock:
                pattern = saveddata_session.query(ImplantSet).get(lookfor)
        else:
            eager = processEager(eager)
            with sd_lock:
                pattern = saveddata_session.query(ImplantSet).options(*eager).filter(
                    TargetProfile.ID == lookfor).first()
    elif isinstance(lookfor, str):
        eager = processEager(eager)
        with sd_lock:
            pattern = saveddata_session.query(ImplantSet).options(*eager).filter(TargetProfile.name == lookfor).first()
    else:
        raise TypeError("Improper argument")
    return pattern


def searchFits(nameLike, where=None, eager=None):
    if not isinstance(nameLike, str):
        raise TypeError("Need string as argument")
    # Prepare our string for request
    nameLike = "%{0}%".format(sqlizeString(nameLike))

    # Add any extra components to the search to our where clause
    filter = processWhere(Fit.name.like(nameLike, escape="\\"), where)
    eager = processEager(eager)
    with sd_lock:
        fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).limit(100).all())

    return fits


def getProjectedFits(fitID):
    if isinstance(fitID, int):
        with sd_lock:
            filter = and_(projectedFits_table.c.sourceID == fitID, Fit.ID == projectedFits_table.c.victimID)
            fits = saveddata_session.query(Fit).filter(filter).all()
            return fits
    else:
        raise TypeError("Need integer as argument")


def getSsoCharacters(clientHash, eager=None):
    eager = processEager(eager)
    with sd_lock:
        characters = saveddata_session.query(SsoCharacter).filter(SsoCharacter.client == clientHash).options(*eager).all()
    return characters


@cachedQuery(SsoCharacter, 1, "lookfor", "clientHash")
def getSsoCharacter(lookfor, clientHash, eager=None):
    filter = SsoCharacter.client == clientHash

    if isinstance(lookfor, int):
        filter = and_(filter, SsoCharacter.ID == lookfor)
    elif isinstance(lookfor, str):
        filter = and_(filter, SsoCharacter.characterName == lookfor)
    else:
        raise TypeError("Need integer or string as argument")

    eager = processEager(eager)
    with sd_lock:
        character = saveddata_session.query(SsoCharacter).options(*eager).filter(filter).first()

    return character


def getOverrides(itemID, eager=None):
    if isinstance(itemID, int):
        return saveddata_session.query(Override).filter(Override.itemID == itemID).all()
    else:
        raise TypeError("Need integer as argument")


def clearOverrides():
    with sd_lock:
        deleted_rows = saveddata_session.query(Override).delete()
    commit()
    return deleted_rows


def getAllOverrides(eager=None):
    return saveddata_session.query(Override).all()


def removeInvalid(fits):
    invalids = [f for f in fits if f.isInvalid]

    if invalids:
        list(map(fits.remove, invalids))
        list(map(saveddata_session.delete, invalids))
        saveddata_session.commit()

    return fits


def add(stuff):
    with sd_lock:
        saveddata_session.add(stuff)


def save(stuff):
    add(stuff)
    commit()


def remove(stuff):
    removeCachedEntry(type(stuff), stuff.ID)
    with sd_lock:
        saveddata_session.delete(stuff)
    commit()


def commit():
    with sd_lock:
        try:
            saveddata_session.commit()
        except (KeyboardInterrupt, SystemExit):
            raise
        except Exception:
            saveddata_session.rollback()
            exc_info = sys.exc_info()
            raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])


def flush():
    with sd_lock:
        try:
            saveddata_session.flush()
        except (KeyboardInterrupt, SystemExit):
            raise
        except Exception:
            saveddata_session.rollback()
            exc_info = sys.exc_info()
            raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
