# This file is part of ELiDE, frontend to 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, version 3.
#
# 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/>.
import os

from kivy.app import App
from kivy.logger import Logger
from kivy.clock import Clock
from kivy.properties import (
    AliasProperty,
    BooleanProperty,
    ObjectProperty,
    NumericProperty
)
from kivy.resources import resource_find
from .app import ELiDEApp
from kivy.uix.widget import Widget
from kivy.uix.screenmanager import ScreenManager, Screen
import LiSE.proxy
from .util import trigger
from functools import partial


class GameScreen(Screen):
    """A version of :class:`kivy.uix.screenmanager.Screen` that is easier to set up and use with ELiDE

    Should be a child of the :class:`ELiDE.game.Screens` widget, which will never itself be displayed.
    ``GameScreen`` instances in it will be added to the screen manager, so that you can switch
    to them with the ``switch_screen`` method.

    Every ``GameScreen`` needs a ``name``, just like regular ``Screen``.

    """
    switch_screen = ObjectProperty()
    """Method to set the ``screen`` attribute of the main :class:`kivy.uix.screenmanager.ScreenManager`"""
    disabled = BooleanProperty(False)
    """If you bind your widgets' ``disabled`` to this, they will be disabled when a game command is in mid-execution"""

    @property
    def app(self):
        return App.get_running_app()

    @property
    def engine(self):
        return App.get_running_app().engine

    def disable_input(self, cb=None):
        """Set ``self.disabled`` to ``True``, then call ``cb`` if provided

        :param cb: callback function for after disabling
        :return: ``None``

        """
        self.disabled = True
        if cb:
            cb()

    def enable_input(self, cb=None):
        """Call ``cb`` if provided, then set ``self.disabled`` to ``False``

        :param cb: callback function for before enabling
        :return: ``None``

        """
        if cb:
            cb()
        self.disabled = False

    def wait_travel(self, character, thing, dest, cb=None):
        """Schedule a thing to travel someplace, then wait for it to finish.

        :param character: name of the character
        :param thing: name of the thing that will travel
        :param dest: name of the place it will travel to
        :param cb: callback function for when it's done, optional
        :return: ``None``

        """
        self.disable_input()
        self.app.wait_travel(character, thing, dest, cb=partial(self.enable_input, cb))

    def wait_turns(self, turns, cb=None):
        """Call ``self.app.engine.next_turn()`` ``n`` times, waiting ``self.app.turn_length`` in between

        Disables input for the duration.

        :param turns: number of turns to wait
        :param cb: function to call when done waiting, optional
        :return: ``None``

        """
        self.disable_input()
        self.app.wait_turns(turns, cb=partial(self.enable_input, cb))

    def wait_command(self, start_func, turns=1, end_func=None):
        """Call ``start_func``, wait ``turns``, and then call ``end_func`` if provided

        Disables input for the duration.

        :param start_func: function to call just after disabling input
        :param turns: number of turns to wait
        :param end_func: function to call just before re-enabling input
        :return: ``None``

        """
        self.disable_input()
        start_func()
        self.app.wait_turns(turns, cb=partial(self.enable_input, end_func))

    def wait_travel_command(self, character, thing, dest, start_func, turns=1, end_func=lambda: None):
        """Schedule a thing to travel someplace and do something, then wait for it to finish.

        Input will be disabled for the duration.

        :param character: name of the character
        :param thing: name of the thing
        :param dest: name of the destination (a place)
        :param start_func: function to call when the thing gets to dest
        :param turns: number of turns to wait after start_func before re-enabling input
        :param end_func: optional. Function to call after waiting ``turns`` after start_func
        :return: ``None``

        """
        self.disable_input()
        self.app.wait_travel_command(character, thing, dest, start_func, turns, partial(self.enable_input, end_func))


class Screens(Widget):
    """Children of this widget will be added to the ``GameApp``'s :class:`kivy.uix.screenmanager.ScreenManager`"""
    app = ObjectProperty()

    def add_widget(self, wid, index=0, canvas=None):
        wid.switch_screen = self.app.screen_manager.setter('screen')
        super().add_widget(wid, index, canvas)


class GameApp(ELiDEApp):
    modules = []
    world_file = None
    turn_length = NumericProperty(0.5)

    def wait_turns(self, turns, dt=None, *, cb=None):
        """Call ``self.engine.next_turn()`` ``n`` times, waiting ``self.turn_length`` in between

        If provided, call ``cb`` when done.

        :param turns: number of turns to wait
        :param dt: unused, just satisfies the clock
        :param cb: callback function to call when done, optional
        :return: ``None``

        """
        if turns == 0:
            if cb:
                cb()
            return
        self.engine.next_turn()
        turns -= 1
        Clock.schedule_once(partial(self.wait_turns, turns, cb=cb), self.turn_length)

    def wait_travel(self, character, thing, dest, cb=None):
        """Schedule a thing to travel someplace, then wait for it to finish, and call ``cb`` if provided

        :param character: name of the character
        :param thing: name of the thing
        :param dest: name of the destination (a place)
        :param cb: function to be called when I'm done
        :return: ``None``
        
        """
        self.wait_turns(self.engine.character[character].thing[thing].travel_to(dest), cb=cb)

    def wait_command(self, start_func, turns=1, end_func=None):
        """Call ``start_func``, and wait to call ``end_func`` after simulating ``turns`` (default 1)

        :param start_func: function to call before waiting
        :param turns: number of turns to wait
        :param end_func: function to call after waiting
        :return: ``None``

        """
        start_func()
        self.wait_turns(turns, cb=end_func)

    def wait_travel_command(self, character, thing, dest, start_func, turns=1, end_func=None):
        """Schedule a thing to travel someplace and do something, then wait for it to finish.

        :param character: name of the character
        :param thing: name of the thing
        :param dest: name of the destination (a place)
        :param start_func: function to call when the thing gets to dest
        :param turns: number of turns to wait after start_func before re-enabling input
        :param end_func: optional. Function to call after waiting ``turns`` after start_func
        :return: ``None``
        """
        self.wait_travel(character, thing, dest, cb=partial(
            self.wait_command, start_func, turns, end_func)
        )

    def on_engine(self, *args):
        self.branch, self.turn, self.tick = self.engine._btt()
        self.engine.time.connect(self._pull_time, weak=False)

    def _pull_time(self, *args, branch, turn, tick):
        self.branch, self.turn, self.tick = branch, turn, tick

    def _get_worlddb(self):
        filen = self.world_file or \
                self.name + 'World.db' if self.name \
                else 'LiSEWorld.db'
        return resource_find(filen) or filen
    worlddb = AliasProperty(
        _get_worlddb,
        lambda self, v: None
    )

    def build(self):
        have_world = False
        try:
            os.stat(self.worlddb)
            have_world = True
        except FileNotFoundError:
            pass
        self.procman = LiSE.proxy.EngineProcessManager()
        self.engine = self.procman.start(
            self.worlddb,
            logger=Logger, loglevel=getattr(self, 'loglevel', 'debug'),
            do_game_start=not have_world,
            install_modules=self.modules
        )
        self.screen_manager = ScreenManager()
        self.screens = Screens(app=self)
        self.screens.bind(children=self._pull_screens)
        self._pull_screens()
        if hasattr(self, 'inspector'):
            from kivy.core.window import Window
            from kivy.modules import inspector
            inspector.create_inspector(Window, self.screen_manager)
        return self.screen_manager

    @trigger
    def _pull_screens(self, *args):
        for screen in reversed(self.screens.children):
            self.screens.remove_widget(screen)
            self.screen_manager.add_widget(screen)

    def on_pause(self):
        """Sync the database with the current state of the game."""
        self.engine.commit()
        self.config.write()

    def on_stop(self, *largs):
        """Sync the database, wrap up the game, and halt."""
        self.procman.shutdown()
        self.config.write()
