# This file is part of LiSE, a framework for life simulation games.
# Copyright (c) Zachary Spector, public@zacharyspector.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""The query engine provides Pythonic methods to access the database.

This module also contains a notably unfinished implementation of a query
language specific to LiSE. Access some stats using entities' method
``historical``, and do comparisons on those, and instead of a boolean
result you'll get a callable object that will return an iterator over
turn numbers in which the comparison evaluated to ``True``.

"""
from operator import gt, lt, eq, ne, le, ge
from functools import partialmethod

import allegedb.query

from .exc import (
    IntegrityError,
    OperationalError
)
from .util import EntityStatAccessor
import LiSE


def windows_union(windows):
    """Given a list of (beginning, ending), return a minimal version that contains the same ranges.

    :rtype: list

    """
    def fix_overlap(left, right):
        if left == right:
            return [left]
        assert left[0] < right[0]
        if left[1] >= right[0]:
            if right[1] > left[1]:
                return [(left[0], right[1])]
            else:
                return [left]
        return [left, right]

    if len(windows) == 1:
        return windows
    none_left = []
    none_right = []
    otherwise = []
    for window in windows:
        if window[0] is None:
            none_left.append(window)
        elif window[1] is None:
            none_right.append(window)
        else:
            otherwise.append(window)

    res = []
    otherwise.sort()
    for window in none_left:
        if not res:
            res.append(window)
            continue
        res.extend(fix_overlap(res.pop(), window))
    while otherwise:
        window = otherwise.pop(0)
        if not res:
            res.append(window)
            continue
        res.extend(fix_overlap(res.pop(), window))
    for window in none_right:
        if not res:
            res.append(window)
            continue
        res.extend(fix_overlap(res.pop(), window))
    return res


def windows_intersection(windows):
    """Given a list of (beginning, ending), return another describing where they overlap.

    :rtype: list
    """

    def intersect2(left, right):
        if left == right:
            return left
        elif left is (None, None):
            return right
        elif right is (None, None):
            return left
        elif left[0] is None:
            if right[0] is None:
                return None, min((left[1], right[1]))
            elif right[1] is None:
                if left[1] <= right[0]:
                    return left[1], right[0]
                else:
                    return None
            elif right[0] <= left[1]:
                return right[0], left[1]
            else:
                return None
        elif left[1] is None:
            if right[0] is None:
                return left[0], right[1]
            else:
                return right  # assumes left[0] <= right[0]
        # None not in left
        elif right[0] is None:
            return left[0], min((left[1], right[1]))
        elif right[1] is None:
            if left[1] >= right[0]:
                return right[0], left[1]
            else:
                return None
        assert None not in left and None not in right and left[0] < right[1]
        if left[1] >= right[0]:
            if right[1] > left[1]:
                return right[0], left[1]
            else:
                return right
        return None

    if len(windows) == 1:
        return windows
    left_none = []
    right_none = []
    otherwise = []
    for window in windows:
        assert window is not None, None
        if window[0] is None:
            left_none.append(window)
        elif window[1] is None:
            right_none.append(window)
        else:
            otherwise.append(window)

    done = []
    todo = left_none + sorted(otherwise)
    for window in todo:
        if not done:
            done.append(window)
            continue
        res = intersect2(done.pop(), window)
        if res:
            done.append(res)
    return done


class Query(object):
    def __new__(cls, engine, leftside, rightside=None, **kwargs):
        if rightside is None:
            if not isinstance(leftside, cls):
                raise TypeError("You can't make a query with only one side")
            me = leftside
        else:
            me = super().__new__(cls)
            me.leftside = leftside
            me.rightside = rightside
        me.engine = engine
        me.windows = kwargs.get('windows', [])
        return me

    def iter_turns(self):
        raise NotImplementedError

    def iter_ticks(self, turn):
        raise NotImplementedError

    def __eq__(self, other):
        return EqQuery(self.engine, self, self.engine.entityfy(other))

    def __gt__(self, other):
        return GtQuery(self.engine, self, self.engine.entityfy(other))

    def __ge__(self, other):
        return GeQuery(self.engine, self, self.engine.entityfy(other))

    def __lt__(self, other):
        return LtQuery(self.engine, self, self.engine.entityfy(other))

    def __le__(self, other):
        return LeQuery(self.engine, self, self.engine.entityfy(other))

    def __ne__(self, other):
        return NeQuery(self.engine, self, self.engine.entityfy(other))

    def and_before(self, end):
        if self.windows:
            new_windows = windows_intersection(
                sorted(self.windows + [(None, end)])
            )
        else:
            new_windows = [(0, end)]
        return type(self)(self.leftside, self.rightside, windows=new_windows)
    before = and_before

    def or_before(self, end):
        if self.windows:
            new_windows = windows_union(self.windows + [(None, end)])
        else:
            new_windows = [(None, end)]
        return type(self)(self.leftside, self.rightside, windows=new_windows)

    def and_after(self, start):
        if self.windows:
            new_windows = windows_intersection(self.windows + [(start, None)])
        else:
            new_windows = [(start, None)]
        return type(self)(self.leftside, self.rightside, windows=new_windows)
    after = and_after

    def or_between(self, start, end):
        if self.windows:
            new_windows = windows_union(self.windows + [(start, end)])
        else:
            new_windows = [(start, end)]
        return type(self)(self.leftside, self.rightside, windows=new_windows)

    def and_between(self, start, end):
        if self.windows:
            new_windows = windows_intersection(self.windows + [(start, end)])
        else:
            new_windows = [(start, end)]
        return type(self)(self.leftside, self.rightside, windows=new_windows)
    between = and_between

    def or_during(self, tick):
        return self.or_between(tick, tick)

    def and_during(self, tick):
        return self.and_between(tick, tick)
    during = and_during


class Union(Query):
    pass


class ComparisonQuery(Query):
    oper = lambda x, y: NotImplemented

    def iter_turns(self):
        return slow_iter_turns_eval_cmp(self, self.oper, engine=self.engine)


class EqQuery(ComparisonQuery):
    oper = eq


class NeQuery(ComparisonQuery):
    oper = ne


class GtQuery(ComparisonQuery):
    oper = gt


class LtQuery(ComparisonQuery):
    oper = lt


class GeQuery(ComparisonQuery):
    oper = ge


class LeQuery(ComparisonQuery):
    oper = le


comparisons = {
    'eq': EqQuery,
    'ne': NeQuery,
    'gt': GtQuery,
    'lt': LtQuery,
    'ge': GeQuery,
    'le': LeQuery
}


class StatusAlias(EntityStatAccessor):
    def __eq__(self, other):
        return EqQuery(self.engine, self, other)

    def __ne__(self, other):
        return NeQuery(self.engine, self, other)

    def __gt__(self, other):
        return GtQuery(self.engine, self, other)

    def __lt__(self, other):
        return LtQuery(self.engine, self, other)

    def __ge__(self, other):
        return GeQuery(self.engine, self, other)

    def __le__(self, other):
        return LeQuery(self.engine, self, other)


def slow_iter_turns_eval_cmp(qry, oper, start_branch=None, engine=None):
    """Iterate over all turns on which a comparison holds.

    This is expensive. It evaluates the query for every turn in history.

    """
    def mungeside(side):
        if isinstance(side, Query):
            return side.iter_turns
        elif isinstance(side, StatusAlias):
            return EntityStatAccessor(
                side.entity, side.stat, side.engine,
                side.branch, side.turn, side.tick, side.current, side.mungers
            )
        elif isinstance(side, EntityStatAccessor):
            return side
        else:
            return lambda: side
    leftside = mungeside(qry.leftside)
    rightside = mungeside(qry.rightside)
    engine = engine or leftside.engine or rightside.engine

    for (branch, _, _) in engine._iter_parent_btt(start_branch or engine.branch):
        if branch is None:
            return
        parent, turn_start, tick_start, turn_end, tick_end = engine._branches[branch]
        for turn in range(turn_start, engine.turn + 1):
            if oper(leftside(branch, turn), rightside(branch, turn)):
                yield branch, turn


class QueryEngine(allegedb.query.QueryEngine):
    path = LiSE.__path__[0]
    IntegrityError = IntegrityError
    OperationalError = OperationalError

    def universals_dump(self):
        for key, branch, turn, tick, value in self.sql('universals_dump'):
            yield self.unpack(key), branch, turn, tick, self.unpack(value)

    def rulebooks_dump(self):
        for rulebook, branch, turn, tick, rules in self.sql('rulebooks_dump'):
            yield self.unpack(rulebook), branch, turn, tick, self.unpack(rules)

    def _rule_dump(self, typ):
        for rule, branch, turn, tick, lst in self.sql('rule_{}_dump'.format(typ)):
            yield rule, branch, turn, tick, self.unpack(lst)

    def rule_triggers_dump(self):
        return self._rule_dump('triggers')

    def rule_prereqs_dump(self):
        return self._rule_dump('prereqs')

    def rule_actions_dump(self):
        return self._rule_dump('actions')

    def characters_dump(self):
        for graph, typ in self.sql('graphs_dump'):
            if typ == 'DiGraph':
                yield self.unpack(graph)
    characters = characters_dump

    def node_rulebook_dump(self):
        for character, node, branch, turn, tick, rulebook in self.sql('node_rulebook_dump'):
            yield self.unpack(character), self.unpack(node), branch, turn, tick, self.unpack(rulebook)

    def portal_rulebook_dump(self):
        for character, orig, dest, branch, turn, tick, rulebook in self.sql('portal_rulebook_dump'):
            yield (
                self.unpack(character), self.unpack(orig), self.unpack(dest),
                branch, turn, tick, self.unpack(rulebook)
            )

    def _charactery_rulebook_dump(self, qry):
        for character, branch, turn, tick, rulebook in self.sql(qry+'_rulebook_dump'):
            yield self.unpack(character), branch, turn, tick, self.unpack(rulebook)

    character_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'character')
    avatar_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'avatar')
    character_thing_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'character_thing')
    character_place_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'character_place')
    character_portal_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'character_portal')

    def character_rules_handled_dump(self):
        for character, rulebook, rule, branch, turn, tick in self.sql('character_rules_handled_dump'):
            yield self.unpack(character), self.unpack(rulebook), rule, branch, turn, tick

    def character_rules_changes_dump(self):
        for (
                character, rulebook, rule, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('character_rules_changes_dump'):
            yield (
                self.unpack(character), self.unpack(rulebook),
                rule, branch, turn, tick, handled_branch, handled_turn
            )

    def avatar_rules_handled_dump(self):
        for character, rulebook, rule, graph, avatar, branch, turn, tick in self.sql('avatar_rules_handled_dump'):
            yield (
                self.unpack(character), self.unpack(rulebook), rule,
                self.unpack(graph), self.unpack(avatar), branch, turn, tick
            )

    def avatar_rules_changes_dump(self):
        jl = self.unpack
        for (
            character, rulebook, rule, graph, avatar, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('avatar_rules_changes_dump'):
            yield (
                jl(character), jl(rulebook), rule, jl(graph), jl(avatar),
                branch, turn, tick, handled_branch, handled_turn
            )

    def character_thing_rules_handled_dump(self):
        for character, rulebook, rule, thing, branch, turn, tick in self.sql('character_thing_rules_handled_dump'):
            yield self.unpack(character), self.unpack(rulebook), rule, self.unpack(thing), branch, turn, tick

    def character_thing_rules_changes_dump(self):
        jl = self.unpack
        for (
            character, rulebook, rule, thing, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('character_thing_rules_changes_dump'):
            yield (
                jl(character), jl(rulebook), rule, jl(thing),
                branch, turn, tick, handled_branch, handled_turn
            )

    def character_place_rules_handled_dump(self):
        for character, rulebook, rule, place, branch, turn, tick in self.sql('character_place_rules_handled_dump'):
            yield self.unpack(character), self.unpack(rulebook), rule, self.unpack(place), branch, turn, tick

    def character_place_rules_changes_dump(self):
        jl = self.unpack
        for (
            character, rulebook, rule, place, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('character_place_rules_changes_dump'):
            yield (
                jl(character), jl(rulebook), rule, jl(place),
                branch, turn, tick, handled_branch, handled_turn
            )

    def character_portal_rules_handled_dump(self):
        for character, rulebook, rule, orig, dest, branch, turn, tick in self.sql('character_portal_rules_handled_dump'):
            yield (
                self.unpack(character), self.unpack(rulebook), rule, self.unpack(orig), self.unpack(dest),
                branch, turn, tick
            )

    def character_portal_rules_changes_dump(self):
        jl = self.unpack
        for (
            character, rulebook, rule, orig, dest, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('character_portal_rules_changes_dump'):
            yield (
                jl(character), jl(rulebook), rule, jl(orig), jl(dest),
                branch, turn, tick, handled_branch, handled_turn
            )

    def node_rules_handled_dump(self):
        for character, node, rulebook, rule, branch, turn, tick in self.sql('node_rules_handled_dump'):
            yield self.unpack(character), self.unpack(node), self.unpack(rulebook), rule, branch, turn, tick

    def node_rules_changes_dump(self):
        jl = self.unpack
        for (
                character, node, rulebook, rule, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('node_rules_changes_dump'):
            yield (
                jl(character), jl(node), jl(rulebook), rule,
                branch, turn, tick, handled_branch, handled_turn
            )

    def portal_rules_handled_dump(self):
        for character, orig, dest, rulebook, rule, branch, turn, tick in self.sql('portal_rules_handled_dump'):
            yield (
                self.unpack(character), self.unpack(orig), self.unpack(dest),
                self.unpack(rulebook), rule, branch, turn, tick
            )

    def portal_rules_changes_dump(self):
        jl = self.unpack
        for (
            character, orig, dest, rulebook, rule, branch, turn, tick, handled_branch, handled_turn
        ) in self.sql('portal_rules_changes_dump'):
            yield (
                jl(character), jl(orig), jl(dest), jl(rulebook), rule,
                branch, turn, tick, handled_branch, handled_turn
            )

    def senses_dump(self):
        for character, sense, branch, turn, tick, function in self.sql('senses_dump'):
            yield self.unpack(character), sense, branch, turn, tick, function

    def things_dump(self):
        for character, thing, branch, turn, tick, location in self.sql('things_dump'):
            yield (
                self.unpack(character), self.unpack(thing), branch, turn, tick,
                self.unpack(location)
            )

    def avatars_dump(self):
        for character_graph, avatar_graph, avatar_node, branch, turn, tick, is_av in self.sql('avatars_dump'):
            yield (
                self.unpack(character_graph), self.unpack(avatar_graph),
                self.unpack(avatar_node), branch, turn, tick, is_av
            )

    def universal_set(self, key, branch, turn, tick, val):
        key, val = map(self.pack, (key, val))
        self.sql('universals_insert', key, branch, turn, tick, val)

    def universal_del(self, key, branch, turn, tick):
        key = self.pack(key)
        self.sql('universals_insert', key, branch, turn, tick, None)

    def comparison(
            self, entity0, stat0, entity1,
            stat1=None, oper='eq', windows=[]
    ):
        stat1 = stat1 or stat0
        return comparisons[oper](
            leftside=entity0.status(stat0),
            rightside=entity1.status(stat1),
            windows=windows
        )

    def count_all_table(self, tbl):
        return self.sql('{}_count'.format(tbl)).fetchone()[0]

    def init_table(self, tbl):
        try:
            return self.sql('create_{}'.format(tbl))
        except OperationalError:
            pass

    def rules_dump(self):
        for (name,) in self.sql('rules_dump'):
            yield name

    def _set_rule_something(self, what, rule, branch, turn, tick, flist):
        flist = self.pack(flist)
        return self.sql('rule_{}_insert'.format(what), rule, branch, turn, tick, flist)

    set_rule_triggers = partialmethod(_set_rule_something, 'triggers')
    set_rule_prereqs = partialmethod(_set_rule_something, 'prereqs')
    set_rule_actions = partialmethod(_set_rule_something, 'actions')

    def set_rule(self, rule, branch, turn, tick, triggers=None, prereqs=None, actions=None):
        self.sql('rules_insert', rule)
        self.set_rule_triggers(rule, branch, turn, tick, triggers or [])
        self.set_rule_prereqs(rule, branch, turn, tick, prereqs or [])
        self.set_rule_actions(rule, branch, turn, tick, actions or [])

    def set_rulebook(self, name, branch, turn, tick, rules=None):
        name, rules = map(self.pack, (name, rules or []))
        self.sql('rulebooks_insert', name, branch, turn, tick, rules)

    def _set_rulebook_on_character(self, rbtyp, char, branch, turn, tick, rb):
        char, rb = map(self.pack, (char, rb))
        self.sql(rbtyp + '_rulebook_insert', char, branch, turn, tick, rb)

    set_character_rulebook = partialmethod(_set_rulebook_on_character, 'character')
    set_avatar_rulebook = partialmethod(_set_rulebook_on_character, 'avatar')
    set_character_thing_rulebook = partialmethod(_set_rulebook_on_character, 'character_thing')
    set_character_place_rulebook = partialmethod(_set_rulebook_on_character, 'character_place')
    set_character_portal_rulebook = partialmethod(_set_rulebook_on_character, 'character_portal')

    def rulebooks(self):
        for book in self.sql('rulebooks'):
            yield self.unpack(book)

    def exist_node(self, character, node, branch, turn, tick, extant):
        super().exist_node(character, node, branch, turn, tick, extant)

    def exist_edge(self, character, orig, dest, idx, branch, turn, tick, extant=None):
        if extant is None:
            branch, turn, tick, extant = idx, branch, turn, tick
            idx = 0
        super().exist_edge(character, orig, dest, idx, branch, turn, tick, extant)

    def set_node_rulebook(self, character, node, branch, turn, tick, rulebook):
        (character, node, rulebook) = map(
            self.pack, (character, node, rulebook)
        )
        return self.sql('node_rulebook_insert', character, node, branch, turn, tick, rulebook)

    def set_portal_rulebook(self, character, orig, dest, branch, turn, tick, rulebook):
        (character, orig, dest, rulebook) = map(
            self.pack, (character, orig, dest, rulebook)
        )
        return self.sql(
            'portal_rulebook_insert',
            character,
            orig,
            dest,
            branch,
            turn,
            tick,
            rulebook
        )

    def handled_character_rule(
            self, character, rulebook, rule, branch, turn, tick
    ):
        (character, rulebook) = map(
            self.pack, (character, rulebook)
        )
        return self.sql(
            'character_rules_handled_insert',
            character,
            rulebook,
            rule,
            branch,
            turn,
            tick,
        )

    def handled_avatar_rule(self, character,  rulebook, rule, graph, av, branch, turn, tick):
        character, graph, av, rulebook = map(
            self.pack, (character, graph, av, rulebook)
        )
        return self.sql(
            'avatar_rules_handled_insert',
            character,
            rulebook,
            rule,
            graph,
            av,
            branch,
            turn,
            tick
        )

    def handled_character_thing_rule(self, character, rulebook, rule, thing, branch, turn, tick):
        character, thing, rulebook = map(
            self.pack, (character, thing, rulebook)
        )
        return self.sql(
            'character_thing_rules_handled_insert',
            character,
            rulebook,
            rule,
            thing,
            branch,
            turn,
            tick
        )

    def handled_character_place_rule(self, character, rulebook, rule, place, branch, turn, tick):
        character, rulebook, place = map(
            self.pack, (character, rulebook, place)
        )
        return self.sql(
            'character_place_rules_handled_insert',
            character,
            rulebook,
            rule,
            place,
            branch,
            turn,
            tick
        )

    def handled_character_portal_rule(self, character, rulebook, rule, orig, dest, branch, turn, tick):
        character, rulebook, orig, dest = map(
            self.pack, (character, rulebook, orig, dest)
        )
        return self.sql(
            'character_portal_rules_handled_insert',
            character,
            rulebook,
            rule,
            orig,
            dest,
            branch,
            turn,
            tick
        )

    def handled_node_rule(
            self, character, node, rulebook, rule, branch, turn, tick
    ):
        (character, node, rulebook) = map(
            self.pack, (character, node, rulebook)
        )
        return self.sql(
            'node_rules_handled_insert',
            character,
            node,
            rulebook,
            rule,
            branch,
            turn,
            tick
        )

    def handled_portal_rule(
            self, character, orig, dest, rulebook, rule, branch, turn, tick
    ):
        (character, orig, dest, rulebook) = map(
            self.pack, (character, orig, dest, rulebook)
        )
        return self.sql(
            'portal_rules_handled_insert',
            character,
            orig,
            dest,
            rulebook,
            rule,
            branch,
            turn,
            tick
        )

    def get_rulebook_char(self, rulemap, character):
        character = self.pack(character)
        for (book,) in self.sql(
                'rulebook_get_{}'.format(rulemap), character
        ):
            return self.unpack(book)
        raise KeyError("No rulebook")

    def set_thing_loc(
            self, character, thing, branch, turn, tick, loc
    ):
        (character, thing) = map(
            self.pack,
            (character, thing)
        )
        loc = self.pack(loc)
        self.sql('del_things_after', character, thing, branch, turn, turn, tick)
        self.sql(
            'things_insert',
            character,
            thing,
            branch,
            turn,
            tick,
            loc
        )

    def avatar_set(self, character, graph, node, branch, turn, tick, isav):
        (character, graph, node) = map(
            self.pack, (character, graph, node)
        )
        self.sql(
            'del_avatars_after',
            character, graph, node, branch, turn, turn, tick
        )
        self.sql(
            'avatars_insert', character, graph, node, branch, turn, tick, isav
        )

    def rulebooks_rules(self):
        for (rulebook, rule) in self.sql('rulebooks_rules'):
            yield map(self.unpack, (rulebook, rule))

    def rulebook_get(self, rulebook, idx):
        return self.unpack(
            self.sql(
                'rulebook_get', self.pack(rulebook), idx
            ).fetchone()[0]
        )

    def branch_descendants(self, branch):
        for child in self.sql('branch_children', branch):
            yield child
            yield from self.branch_descendants(child)

    def turns_completed_dump(self):
        return self.sql('turns_completed_dump')

    def complete_turn(self, branch, turn):
        try:
            self.sql('turns_completed_insert', branch, turn)
        except IntegrityError:
            self.sql('turns_completed_update', turn, branch)

    def initdb(self):
        """Set up the database schema, both for allegedb and the special
        extensions for LiSE

        """
        super().initdb()
        for table in (
            'universals',
            'rules',
            'rulebooks',
            'senses',
            'things',
            'character_rulebook',
            'avatar_rulebook',
            'character_thing_rulebook',
            'character_place_rulebook',
            'character_portal_rulebook',
            'node_rulebook',
            'portal_rulebook',
            'avatars',
            'character_rules_handled',
            'avatar_rules_handled',
            'character_thing_rules_handled',
            'character_place_rules_handled',
            'character_portal_rules_handled',
            'node_rules_handled',
            'portal_rules_handled',
            'rule_triggers',
            'rule_prereqs',
            'rule_actions',
            'turns_completed'
        ):
            self.init_table(table)
