"""
Evennia MUD/MUX/MU* creation system

This is the main top-level API for Evennia. You can explore the evennia library
by accessing evennia.<subpackage> directly. From inside the game you can read
docs of all object by viewing its `__doc__` string, such as through

    py evennia.ObjectDB.__doc__

For full functionality you should explore this module via a django-
aware shell. Go to your game directory and use the command

   evennia shell

to launch such a shell (using python or ipython depending on your install).
See www.evennia.com for full documentation.

"""

import evennia

# docstring header

DOCSTRING = """
Evennia MU* creation system.

Online manual and API docs are found at http://www.evennia.com.

Flat-API shortcut names:
{}
"""

# Delayed loading of properties

# Typeclasses

DefaultAccount = None
DefaultGuest = None
DefaultObject = None
DefaultCharacter = None
DefaultRoom = None
DefaultExit = None
DefaultChannel = None
DefaultScript = None

# Database models
ObjectDB = None
AccountDB = None
ScriptDB = None
ChannelDB = None
Msg = None
ServerConfig = None

# Properties
AttributeProperty = None
TagProperty = None
TagCategoryProperty = None

# commands
Command = None
CmdSet = None
default_cmds = None
syscmdkeys = None
InterruptCommand = None

# search functions
search_object = None
search_script = None
search_account = None
search_channel = None
search_message = None
search_help = None
search_tag = None

# create functions
create_object = None
create_script = None
create_account = None
create_channel = None
create_message = None
create_help_entry = None

# utilities
settings = None
lockfuncs = None
inputhandler = None
logger = None
gametime = None
ansi = None
spawn = None
managers = None
contrib = None
EvMenu = None
EvTable = None
EvForm = None
EvEditor = None
EvMore = None
ANSIString = None
signals = None
FuncParser = None
OnDemandTask = None

# Handlers
SESSION_HANDLER = None
PORTAL_SESSION_HANDLER = None
SERVER_SESSION_HANDLER = None
TASK_HANDLER = None
TICKER_HANDLER = None
MONITOR_HANDLER = None
ON_DEMAND_HANDLER = None

# Containers
GLOBAL_SCRIPTS = None
OPTION_CLASSES = None

PROCESS_ID = None

TWISTED_APPLICATION = None
EVENNIA_PORTAL_SERVICE = None
EVENNIA_SERVER_SERVICE = None


def _create_version():
    """
    Helper function for building the version string
    """
    import os
    from subprocess import STDOUT, CalledProcessError, check_output

    version = "Unknown"
    root = os.path.dirname(os.path.abspath(__file__))
    try:
        with open(os.path.join(root, "VERSION.txt"), "r") as f:
            version = f.read().strip()
    except IOError as err:
        print(err)
    try:
        rev = (
            check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT)
            .strip()
            .decode()
        )
        version = "%s (rev %s)" % (version, rev)
    except (IOError, CalledProcessError, OSError):
        # ignore if we cannot get to git
        pass
    return version


__version__ = _create_version()
del _create_version

_LOADED = False

PORTAL_MODE = False


def _init(portal_mode=False):
    """
    This function is called automatically by the launcher only after
    Evennia has fully initialized all its models. It sets up the API
    in a safe environment where all models are available already.
    """
    global _LOADED
    if _LOADED:
        return
    _LOADED = True
    global DefaultAccount, DefaultObject, DefaultGuest, DefaultCharacter
    global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript
    global ObjectDB, AccountDB, ScriptDB, ChannelDB, Msg
    global Command, CmdSet, default_cmds, syscmdkeys, InterruptCommand
    global search_object, search_script, search_account, search_channel
    global search_help, search_tag, search_message
    global create_object, create_script, create_account, create_channel
    global create_message, create_help_entry, OnDemandTask
    global signals
    global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers
    global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, PROCESS_ID
    global TASK_HANDLER, PORTAL_SESSION_HANDLER, SERVER_SESSION_HANDLER, ON_DEMAND_HANDLER
    global GLOBAL_SCRIPTS, OPTION_CLASSES, EVENNIA_PORTAL_SERVICE, EVENNIA_SERVER_SERVICE, TWISTED_APPLICATION
    global EvMenu, EvTable, EvForm, EvMore, EvEditor
    global ANSIString, FuncParser
    global AttributeProperty, TagProperty, TagCategoryProperty, ServerConfig
    global PORTAL_MODE
    PORTAL_MODE = portal_mode

    # Parent typeclasses
    # utilities
    import os

    from django.conf import settings

    from . import contrib
    from .accounts.accounts import DefaultAccount, DefaultGuest
    from .accounts.models import AccountDB
    from .commands.cmdset import CmdSet
    from .commands.command import Command, InterruptCommand
    from .comms.comms import DefaultChannel
    from .comms.models import ChannelDB, Msg
    from .locks import lockfuncs
    from .objects.models import ObjectDB
    from .objects.objects import (
        DefaultCharacter,
        DefaultExit,
        DefaultObject,
        DefaultRoom,
    )
    from .prototypes.spawner import spawn
    from .scripts.models import ScriptDB
    from .scripts.monitorhandler import MONITOR_HANDLER
    from .scripts.ondemandhandler import ON_DEMAND_HANDLER, OnDemandTask
    from .scripts.scripts import DefaultScript
    from .scripts.taskhandler import TASK_HANDLER
    from .scripts.tickerhandler import TICKER_HANDLER
    from .server import signals
    from .server.models import ServerConfig
    from .typeclasses.attributes import AttributeProperty
    from .typeclasses.tags import TagCategoryProperty, TagProperty
    from .utils import ansi, class_from_module, gametime, logger
    from .utils.ansi import ANSIString

    if not PORTAL_MODE:
        # containers
        from .utils.containers import GLOBAL_SCRIPTS, OPTION_CLASSES

    # create functions
    from .utils.create import (
        create_account,
        create_channel,
        create_help_entry,
        create_message,
        create_object,
        create_script,
    )
    from .utils.eveditor import EvEditor
    from .utils.evform import EvForm
    from .utils.evmenu import EvMenu
    from .utils.evmore import EvMore
    from .utils.evtable import EvTable
    from .utils.funcparser import FuncParser

    # search functions
    from .utils.search import (
        search_account,
        search_channel,
        search_help,
        search_message,
        search_object,
        search_script,
        search_tag,
    )
    from .utils.utils import class_from_module

    PROCESS_ID = os.getpid()

    from twisted.application.service import Application

    TWISTED_APPLICATION = Application("Evennia")

    _evennia_service_class = None

    if portal_mode:
        # Set up the PortalSessionHandler
        from evennia.server.portal import portalsessionhandler

        portal_sess_handler_class = class_from_module(settings.PORTAL_SESSION_HANDLER_CLASS)
        portalsessionhandler.PORTAL_SESSIONS = portal_sess_handler_class()
        SESSION_HANDLER = portalsessionhandler.PORTAL_SESSIONS
        evennia.PORTAL_SESSION_HANDLER = evennia.SESSION_HANDLER
        _evennia_service_class = class_from_module(settings.EVENNIA_PORTAL_SERVICE_CLASS)
        EVENNIA_PORTAL_SERVICE = _evennia_service_class()
        EVENNIA_PORTAL_SERVICE.setServiceParent(TWISTED_APPLICATION)

        from django.db import connection

        # we don't need a connection to the database so close it right away
        try:
            connection.close()
        except Exception:
            pass

    else:
        # Create the ServerSesssionHandler
        from evennia.server import sessionhandler

        sess_handler_class = class_from_module(settings.SERVER_SESSION_HANDLER_CLASS)
        sessionhandler.SESSIONS = sess_handler_class()
        sessionhandler.SESSION_HANDLER = sessionhandler.SESSIONS
        SESSION_HANDLER = sessionhandler.SESSIONS
        SERVER_SESSION_HANDLER = SESSION_HANDLER
        _evennia_service_class = class_from_module(settings.EVENNIA_SERVER_SERVICE_CLASS)
        EVENNIA_SERVER_SERVICE = _evennia_service_class()
        EVENNIA_SERVER_SERVICE.setServiceParent(TWISTED_APPLICATION)

    # API containers

    class _EvContainer(object):
        """
        Parent for other containers

        """

        def _help(self):
            "Returns list of contents"
            names = [name for name in self.__class__.__dict__ if not name.startswith("_")]
            names += [name for name in self.__dict__ if not name.startswith("_")]
            print(self.__doc__ + "-" * 60 + "\n" + ", ".join(names))

        help = property(_help)

    class DBmanagers(_EvContainer):
        """
        Links to instantiated Django database managers. These are used
        to perform more advanced custom database queries than the standard
        search functions allow.

        helpentries - HelpEntry.objects
        accounts - AccountDB.objects
        scripts - ScriptDB.objects
        msgs    - Msg.objects
        channels - Channel.objects
        objects - ObjectDB.objects
        serverconfigs - ServerConfig.objects
        tags - Tags.objects
        attributes - Attributes.objects

        """

        from .accounts.models import AccountDB
        from .comms.models import ChannelDB, Msg
        from .help.models import HelpEntry
        from .objects.models import ObjectDB
        from .scripts.models import ScriptDB
        from .server.models import ServerConfig
        from .typeclasses.attributes import Attribute
        from .typeclasses.tags import Tag

        # create container's properties
        helpentries = HelpEntry.objects
        accounts = AccountDB.objects
        scripts = ScriptDB.objects
        msgs = Msg.objects
        channels = ChannelDB.objects
        objects = ObjectDB.objects
        serverconfigs = ServerConfig.objects
        attributes = Attribute.objects
        tags = Tag.objects
        # remove these so they are not visible as properties
        del HelpEntry, AccountDB, ScriptDB, Msg, ChannelDB
        # del ExternalChannelConnection
        del ObjectDB, ServerConfig, Tag, Attribute

    managers = DBmanagers()
    del DBmanagers

    class DefaultCmds(_EvContainer):
        """
        This container holds direct shortcuts to all default commands in Evennia.

        To access in code, do 'from evennia import default_cmds' then
        access the properties on the imported default_cmds object.

        """

        from .commands.default.cmdset_account import AccountCmdSet
        from .commands.default.cmdset_character import CharacterCmdSet
        from .commands.default.cmdset_session import SessionCmdSet
        from .commands.default.cmdset_unloggedin import UnloggedinCmdSet
        from .commands.default.muxcommand import MuxAccountCommand, MuxCommand

        def __init__(self):
            "populate the object with commands"

            def add_cmds(module):
                "helper method for populating this object with cmds"
                from evennia.utils import utils

                cmdlist = utils.variable_from_module(module, module.__all__)
                self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))

            from .commands.default import (
                account,
                admin,
                batchprocess,
                building,
                comms,
                general,
                help,
                system,
                unloggedin,
            )

            add_cmds(admin)
            add_cmds(building)
            add_cmds(batchprocess)
            add_cmds(building)
            add_cmds(comms)
            add_cmds(general)
            add_cmds(account)
            add_cmds(help)
            add_cmds(system)
            add_cmds(unloggedin)

    default_cmds = DefaultCmds()
    del DefaultCmds

    class SystemCmds(_EvContainer):
        """
        Creating commands with keys set to these constants will make
        them system commands called as a replacement by the parser when
        special situations occur. If not defined, the hard-coded
        responses in the server are used.

        CMD_NOINPUT - no input was given on command line
        CMD_NOMATCH - no valid command key was found
        CMD_MULTIMATCH - multiple command matches were found
        CMD_LOGINSTART - this command will be called as the very
                         first command when an account connects to
                         the server.

        To access in code, do 'from evennia import syscmdkeys' then
        access the properties on the imported syscmdkeys object.

        """

        from .commands import cmdhandler

        CMD_NOINPUT = cmdhandler.CMD_NOINPUT
        CMD_NOMATCH = cmdhandler.CMD_NOMATCH
        CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
        CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
        del cmdhandler

    syscmdkeys = SystemCmds()
    del SystemCmds
    del _EvContainer


def set_trace(term_size=(140, 80), debugger="auto"):
    """
    Helper function for running a debugger inside the Evennia event loop.

    Args:
        term_size (tuple, optional): Only used for Pudb and defines the size of the terminal
            (width, height) in number of characters.
        debugger (str, optional): One of 'auto', 'pdb' or 'pudb'. Pdb is the standard debugger. Pudb
            is an external package with a different, more 'graphical', ncurses-based UI. With
            'auto', will use pudb if possible, otherwise fall back to pdb. Pudb is available through
            `pip install pudb`.

    Notes:
        To use:

        1) add this to a line to act as a breakpoint for entering the debugger:

            from evennia import set_trace; set_trace()

        2) restart evennia in interactive mode

            evennia istart

        3) debugger will appear in the interactive terminal when breakpoint is reached. Exit
           with 'q', remove the break line and restart server when finished.

    """
    import sys

    dbg = None

    if debugger in ("auto", "pudb"):
        try:
            from pudb import debugger

            dbg = debugger.Debugger(stdout=sys.__stdout__, term_size=term_size)
        except ImportError:
            if debugger == "pudb":
                raise
            pass

    if not dbg:
        import pdb

        dbg = pdb.Pdb(stdout=sys.__stdout__)

    try:
        # Start debugger, forcing it up one stack frame (otherwise `set_trace`
        # will start debugger this point, not the actual code location)
        dbg.set_trace(sys._getframe().f_back)
    except Exception:
        # Stopped at breakpoint. Press 'n' to continue into the code.
        dbg.set_trace()


# initialize the doc string
global __doc__
__doc__ = DOCSTRING.format(
    "\n- "
    + "\n- ".join(
        f"evennia.{key}"
        for key in sorted(globals())
        if not key.startswith("_") and key not in ("DOCSTRING",)
    )
)
