#!/usr/bin/env python

from random import random
from re import match, split, sub, MULTILINE

from django.core.exceptions import ObjectDoesNotExist
from evennia.commands.default.general import CmdGet, NumberedTargetCommand
from evennia.commands.default.muxcommand import MuxCommand
from evennia.contrib.rpg.rpsystem import send_emote
from evennia.utils import iter_to_str, logger
from evennia.utils.evmore import EvMore

from commands.command import Command
from typeclasses.characters import Character
from typeclasses.tutorial import TutorBird, TutorialState
from utils.word_list import routput, paragraph, choices


class CmdUse(MuxCommand):
    """
    Use an item.

    The item, probably something in your inventory, could be something
    in the local area.

    Usage:

      use <item> [= object]

    This standard from the text adventurer tome is a catch-all command
    allowing you to use or combine an object with another object.
    """
    key = "use"
    aliases = ["apply"]
    rhs_split = ("=", " with ", " on ", " to ")

    def func(self):
        """Call the 'do_use' method."""
        if not self.args:
            self.caller.msg("Use what?")
            return

        item = self.caller.search(self.lhs)
        if item:
            if self.rhs:
                obj = self.caller.search(self.rhs)
            else:
                obj = None

            if item.has_method('do_use'):
                item.do_use(self.caller, obj)
            else:
                self.caller.msg(item.db.use_msg or f"You can't use {item.name}.")


class CmdPush(Command):
    """
    Push an item in the area.

    Usage:

      push <item>
    """
    key = "push"

    def func(self):
        """Call the 'do_push' method."""
        pusher = self.caller
        to_push = self.args.strip()

        if to_push == "":
            pusher.msg("What do you want to push?")
            return

        item = pusher.search(self.args.strip())
        if item:
            if item.has_method('do_push'):
                item.do_push(pusher)
            else:
                pusher.msg(item.db.push_msg or f"You can't push {item.name}.")


class CmdPull(Command):
    """
    Pull on something.

    Usage:

      pull <item>
    """
    key = "pull"
    aliases = ["yank"]

    def func(self):
        """Call the 'do_pull' method."""
        puller = self.caller
        to_pull = self.args.strip()

        if to_pull == "":
            puller.msg("What do you want to pull?")
            return

        item = puller.search(to_pull)
        if item:
            if item.has_method('do_pull'):
                item.do_pull(puller)
            else:
                puller.msg(item.db.pull_msg or f"You can't pull {item.name}.")


class CmdOpen(Command):
    """
    Open something.

    Usage:

      open <item>
    """
    key = "open"

    def func(self):
        """
        Call an item's 'do_open' method.

        Note that if the room the caller is in has a 'do_open', we
        call that first to see if the room can open something (like an
        exit).
        """
        opener = self.caller
        room = opener.location
        to_open = self.args.strip()

        if to_open == "":
            opener.msg("What would you like to open?")
            return

        if room.has_method('do_open'):
            if room.do_open(opener, to_open):
                return

        item = opener.search(to_open)
        if item:
            if item.has_method('do_open'):
                item.do_open(opener)
            else:
                opener.msg(item.db.open_msg or f"You can't open {item.name}.")


class CmdClose(Command):
    """
    Close something.

    Usage:

      close <item>
    """
    key = "close"

    def func(self):
        """Call the 'do_close' method.

        Note that if the room the caller is in has a 'do_close', we
        call that first to see if the room can close something (like
        an exit).
        """
        closer = self.caller
        room = closer.location
        to_close = self.args.strip()

        if to_close == "":
            closer.msg("What would you like to close?")
            return

        if room.has_method('do_close'):
            if room.do_close(closer, to_close):
                return

        item = closer.search(to_close)
        if item:
            if item.has_method('do_close'):
                item.do_close(closer)
            else:
                closer.msg(item.db.close_msg or f"You can't close {item.name}.")


class CmdWhisper(MuxCommand):
    """
    Speak privately as your character to another.

    Usage:
      whisper <character> = <message>
      whisper <char1>, <char2> = <message>

    Talk privately to one or more characters in your current location, without
    others in the room being informed.
    """

    key = "whisper"
    priority = 0
    locks = "cmd:all()"
    rhs_split = ("=")

    def func(self):
        """Implement the new 'whisper' command."""
        if not self.args:
            self.caller.msg("What are you whispering?")
            return

        if not self.rhs:
            self.caller.msg("Usage: whisper <character> = <message>")
            return

        targets = split(r" *, *", self.lhs)
        chars = [self.caller.search(target) for target in targets]
        for c in chars:
            if not c:
                return

        self.caller.msg(f"Wishering to {chars}")
        full_speech = f"/Me whispers to you, \"{self.rhs}\""
        send_emote(self.caller, chars, full_speech, msg_type="say",
                   anonymous_add=None, quiet=True)

        to_list = [target.get_display_name(self) for target in chars]
        full_speech = f"You whisper to {iter_to_str(to_list, endsep='and')}, \"{self.rhs}\""
        self.caller.msg(full_speech, from_obj=self.caller)


class CmdThink(Command):
    """Think a thought out loud.

    Usage:
      think <message>

    Similar to the 'say' or 'pose' commands, this communicates an
    inner monologue to other players on the 'public' channel.
    """

    key = "think"
    aliases = ["thinks", "("]
    arg_regex = None

    def func(self):
        """Implement the think out loud command."""
        if not self.args:
            self.caller.msg("What do you want to think out loud?")
        else:
            thought = self.args.strip()
            if (self.caller.db.thinking_count or 0) < 3 or random() < 0.4:
                msg = routput(
                    f"<< thinks ^ wonders >> << out loud ^ aloud >> "
                    f"... o O ( {thought} )"
                )
            else:
                msg = f". o O ( {thought} )"
            self.caller.db.thinking_count = (self.caller.db.thinking_count or 0) + 1

            self.caller.execute_cmd(f"pub :{msg}")


class CmdRead(Command):
    """
    Return the inside contents of a book or other readable object.

    Usage:

       read <target>

    To add something to read on target, use the @set command:

      @set <target>/inside = 'This is the text to read.'
    """

    key = "read"

    def find_readable(self, reader, readable_str):
        """Search the room for a readable item."""
        if readable_str == "chalkboard":
            readable_str = "shrub"
        elif readable_str.startswith("cocktail") or \
             readable_str.startswith("drink") or readable_str == "list":
            readable_str = "sign"

        targets = reader.search(readable_str, quiet=True)
        if not targets:
            # Let's get a better error message:
            if reader.location.details and reader.location.details.get(readable_str):
                reader.msg(f"You can't read {readable_str}.")
            else:
                reader.msg(f"You don't see {readable_str}.")
            return None

        label_targets = [t for t in targets if t.db.inside]

        if len(label_targets) == 1:
            return label_targets[0]

        if len(label_targets) == 0:
            reader.msg(f"You can't find anything readable on {readable_str}.")
        else:
            reader.msg(f"Too many things match, '{readable_str}'. "
                       "Can you narrow it down with a title, "
                       "or preface with a number, like '2-paper'?")

    def func(self):
        """Return the 'inside' attribute."""
        reader = self.caller
        target_str = self.args.strip()
        if target_str == "":
            reader.msg("Usage: |gread <object>|n")
            return

        book = self.find_readable(reader, target_str)
        if book:
            contents = book.db.inside
            prefix = book.db.prefix
            if prefix:
                prefix = prefix + "|/"
            if contents.startswith("file:"):
                self.show_file(reader, contents[5:], prefix,
                               self.client_width(), reader.client_height())
            else:
                reader.msg((prefix or "") + contents)

    def show_file(self, reader, filename, prefix, width, height):
        """
        Display a file to the user.

        The file is _somewhat_ Markdown formatted.
        """
        with open(filename, "r") as myfile:
            buf = myfile.read()
            session = reader.sessions.get()[0]
            width = self.client_width()

            if reader.is_webclient():
                tidied = md_to_html(buf)
            else:
                tidied = md_to_evennia(buf, reader.is_utf(), width)
            if prefix:
                tidied = prefix + "\n\n" + tidied
            EvMore(reader, tidied, session=session,
                   justify=True, justify_kwargs={"width": width})

def md_to_evennia(text, utf, width):
    brk = '─' if utf else '-'
    line_brk = '|W' + (brk * width) + '|n'
    breaks = [line_brk if line.startswith("#") else line
              for line in md_preprocessor(text)]
    return "\n".join(breaks)

def md_to_html(text):
    brk = '───────────────────────────────────────────────────────'
    breaks = [brk if line.startswith("#") else line
              for line in md_preprocessor(text)]
    return "\n".join(breaks)

def md_preprocessor(text):
    lines = text.splitlines()
    return [line for line in lines if not line.startswith(">")]

class CmdTake(CmdGet, NumberedTargetCommand):
    """
    Get something, possibly from another character or NPC.

    Usage:
      get <thing> [ from <character> ]

    Note that only some things can be stolen.
    For instance, the brass ring from the door knocker.
    """
    key = "take"
    aliases = ["steal", "get"]
    rhs_split = ("=", " from ")

    def func(self):
        """
        Implement the take command.

        Since this command is designed to work on the object, we
        operate only on self.obj.
        """
        if not self.args:
            self.caller.msg("What do you want to get?")
        elif not self.rhs:
            # This is soo bad to hard-code this game logic, but enough
            # people complained about not being able to 'get' the
            # ring, I need to:
            if self.lhs == "ring" and self.caller.location.key == "Grotto":
               self.caller.msg("From whom do you want to get this ring?")
            elif self.lhs == "frog" and self.caller.location.key == "Frog Meadow":
               self.caller.msg("The little guys are too quick to catch.")
            else:
                super().func()  # Call the 'get' function instead.
        else:
            location = self.caller.search(self.rhs, quiet=True)
            if len(location) == 0:
                self.caller.msg(f"Can't take '{self.lhs}' from '{self.rhs}'.")
                return

            location = location[0]
            if location.is_typeclass("typeclasses.rooms.Room"):
                self.args = self.lhs
                super().func()  # Call the 'get' function instead.
                return
            self.caller.do_take(self.lhs, location)


class CmdDrink(Command):
    """
    Drink a beverage in your inventory.

    Usage:

      drink [ container ]

    If you are holding a teacup, or cocktail, and it is not empty,
    you may drink.

    This doesn't tell others of this particular activity.
    """

    key = "drink"
    aliases = ["sip", "quaff"]

    def drink_item(self, name):
        """Find item in inventory, name, and call 'do_drink' on it."""
        notfound = f"You don't have {name} in your inventory."
        item = self.caller.search(name, location=self.caller,
                                  nofound_string=notfound)
        if item:
            if item.has_method('do_drink'):
                item.do_drink(self.caller)
            else:
                self.caller.msg(f"The {item.name} is not drinkable.")

    def drink_anything(self):
        """Drink anything in your inventory, but only if you have one thing."""
        def not_empty(item):
            return (item.db.amount or 0) > 0
        containers = self.search_contents_by_func('do_drink', not_empty)
        if len(containers) == 1:
            containers[0].do_drink(self.caller)
        elif len(containers) > 1:
            self.caller.msg("You have too many things you can drink. "
                            "Which one do you want?")
        else:
            self.caller.msg("You have nothing to drink.")

    def func(self):
        """
        If given the name of something to drink, find and drink it.

        Otherwise, drink the first item you find in your inventory.
        """
        goal = self.args.strip()
        if goal and goal != "":
            self.drink_item(goal)
        else:
            self.drink_anything()


class CmdEat(Command):
    """
    Eat something edible in your inventory.

    Usage:

      eat [ food-item ]

    This doesn't tell others of this particular activity.
    """

    key = "eat"
    aliases = ["consume", "bite"]

    def eat_item(self, name):
        """Find item in inventory, name, and call 'do_eat' on it."""
        notfound = f"You don't have {name} in your inventory."
        item = self.caller.search(name, location=self.caller,
                                  nofound_string=notfound)
        if item:
            if item.has_method('do_eat'):
                item.do_eat(self.caller)
            else:
                self.caller.msg(f"The {item.name} is not edible.")

    def eat_anything(self):
        """Eat something in your inventory, but only if you have one thing."""
        def not_gone(item):
            return (item.db.amount or 0) > 0
        items = self.search_contents_by_func('do_eat', not_gone)
        if len(items) == 0:
            self.caller.msg("You have nothing to eat.")
        else:
            item = items[0]
            if len(items) > 1:
                self.caller.msg(f"Eating the {item.name}.")
            item.do_eat(self.caller)

    def func(self):
        """
        If given the name of something to eat, find and eat it.

        Otherwise, eat the first item you find in your inventory.
        """
        goal = self.args.strip()
        if goal and goal != "":
            self.eat_item(goal)
        else:
            self.eat_anything()


class CmdFeed(MuxCommand):
    """
    Feed or give something to an object that can eat.
    Typically this is used to feed wood to a fire, or
    berries to a beast.

    Usage:

      |gfeed <target>|n

    Or:

      |gfeed <food> to <target>|n

    Where 'food' is something you have in your inventory,
    and target is a pet or something that wants to eat.

    If you don't specify the 'food', the target will eat
    what you might have they're interested in.
    """
    key = "feed"
    rhs_split = ("=", " to ")

    def func(self):
        """
        Implements the feed (or give) command.
        """
        if not self.args:
            self.caller.msg("Feed what?")
            return

        feeder = self.caller
        if self.rhs:
            eater = feeder.search(self.rhs)
            food = feeder.search(self.lhs, location=feeder)
            if not food:
                return
        else:
            eater = feeder.search(self.lhs)
            food = None

        if eater:
            if eater.has_method('feed'):
                eater.feed(feeder, food)
                if food:
                    try:
                        food.delete()
                    # Allow the eater to delete the object.
                    except ObjectDoesNotExist:
                        pass
            else:
                name = eater.get_display_name(feeder)
                feeder.msg(f"You can't feed, {name}.")
        else:
            feeder.msg(f"Don't see a '{self.lhs}' to feed.")

        # Pass this off to CmdGive?
        #
        # feeder.execute_cmd("give " + self.args)
        #
        # supercmd = CmdGive()
        # supercmd.caller = feeder
        # supercmd.args = self.args
        # supercmd.lhs = self.lhs
        # supercmd.rhs = self.rhs
        # supercmd.func()
