"""Collaborative nonsense story writing."""

import logging
import random
import re
import time

from django.conf import settings
from django.utils import timezone
from irc.client import NickMask

from ircbot.lib import Plugin
from storycraft.models import StorycraftGame, StorycraftPlayer, StorycraftLine


log = logging.getLogger('storycraft.ircplugin')


class Storycraft(Plugin):

    """Play Storycraft, the game of varying authors adding to a story mostly devoid of context.

    This is a game for people who like making up facts, insulting each other, telling
    insane stories, making in-jokes, and generally having a good time writing what can
    fairly only be described as pure nonsense. It does not assume that all players are
    available at any given time, so games may take a while, but the module allows for
    concurrency in the number of games running.
    """

    def start(self):
        """Hook handler functions into the IRC library."""

        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!storycraft\s+status$',
                                                         self.handle_status)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+new\s+game(\s+with\s+config\s+(.*)$|$)',
                                                         self.handle_new_game)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+game\s+(\d+)\s+join$',
                                                         self.handle_join_game)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+list\s+games\s+(open|in progress|completed|my games|waiting for me)$',
                                                         self.handle_list_games)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+game\s+(\d+)\s+start$',
                                                         self.handle_start_game)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+game\s+(\d+)\s+show\s+line$',
                                                         self.handle_show_line)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+game\s+(\d+)\s+add\s+line\s+(.*)$',
                                                         self.handle_add_line)
        self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
                                                         r'^!storycraft\s+game\s+(\d+)\s+status$',
                                                         self.handle_game_status)

        super(Storycraft, self).start()

    def stop(self):
        """Unhook handler functions into the IRC library."""

        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_status)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_new_game)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_join_game)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_list_games)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_start_game)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_show_line)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_add_line)
        self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_game_status)

        super(Storycraft, self).stop()

    def handle_status(self, connection, event, match):
        """Print information about the storycraft games, or one specific game."""

        # do the general status of all games
        count_completed = self._get_completed_games().count()
        count_in_progress = self._get_in_progress_games().count()
        count_open = self._get_open_games().count()
        count_free = self._get_free_game_count()

        return self.bot.reply(event, "Storycraft - {0:d} games completed, {1:d} in progress, {2:d} open. "
                              "{3:d} slots free.".format(count_completed, count_in_progress, count_open, count_free))

    def handle_game_status(self, connection, event, match):
        """Print information about one specific game."""

        game_id = int(match.group(1))

        # do the status of one game
        game = self._get_game_details(game_id)
        if game:
            # get game string
            status_str = game.summary()

            return self.bot.reply(event, status_str)
        else:
            return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id))

    def handle_new_game(self, connection, event, match):
        """Add a game to the system, which users can join."""

        nick = NickMask(event.source).nick

        # ensure the system isn't at capacity
        count_free = self._get_free_game_count()
        if count_free > 0:
            # get the default settings
            master_channel = settings.STORYCRAFT_MASTER_CHANNEL
            game_length = settings.STORYCRAFT_DEFAULT_GAME_LENGTH
            line_length = settings.STORYCRAFT_DEFAULT_LINE_LENGTH
            lines_per_turn = settings.STORYCRAFT_DEFAULT_LINES_PER_TURN

            options = match.group(2)
            if options:
                # override settings with provided options
                tuples = options.split(';')
                for option in tuples:
                    optpair = option.split(',')

                    if len(optpair) == 2:
                        if optpair[0] == 'game_length':
                            game_length = int(optpair[1])
                        elif optpair[0] == 'line_length':
                            line_length = int(optpair[1])
                        elif optpair[0] == 'lines_per_turn':
                            lines_per_turn = int(optpair[1])

            # add a new game
            game = self._add_new_game(game_length, line_length, lines_per_turn, nick, event.source)
            if game:
                # add the player to the game, too
                player = StorycraftPlayer.objects.create(nick=nick, nickmask=event.source, game=game)

                # tell the control channel
                self.bot.reply(None, "{0:s} created a game of storycraft - do '!storycraft game "
                                     "{1:d} join' to take part!".format(nick, game.pk),
                               explicit_target=master_channel)

                log.debug("%s added a new game", nick)
                return self.bot.reply(event, "Game #{0:d} has been created. When everyone has joined, do "
                                      "'!storycraft game {0:d} start'".format(game.pk))
            else:
                return self.bot.reply(event, "Error creating game.")
        else:
            return self.bot.reply(event, "All slots are full.")

    def handle_join_game(self, connection, event, match):
        """Add a player to an open game."""

        nick = NickMask(event.source).nick
        game_id = int(match.group(1))

        # ensure the game exists
        game = self._get_game_details(game_id)
        if game:
            # check that the game hasn't started yet
            if game.status == StorycraftGame.STATUS_OPEN:
                # see if userhost is already in the game
                if not self._get_player_exists_in_game(game_id, event.source):
                    # add the player
                    player = StorycraftPlayer.objects.create(nick=nick, nickmask=event.source, game=game)
                    if player:
                        # output results
                        master_channel = settings.STORYCRAFT_MASTER_CHANNEL

                        self.bot.reply(None, "{0:s} joined storycraft #{1:d}!".format(nick, game_id),
                                       explicit_target=master_channel)
                        log.debug("%s joined game #%d", nick, game_id)
                        return self.bot.reply(event, "{0:s}, welcome to the game.".format(nick))
                    else:
                        return self.bot.reply(event, "Error joining game.")
                else:
                    return self.bot.reply(event, "You are already in game #{0:d}.".format(game_id))
            else:
                return self.bot.reply(event, "Game #{0:d} is not open for new players.".format(game_id))
        else:
            return self.bot.reply("Game #{0:d} does not exist.".format(game_id))

    def handle_list_games(self, connection, event, match):
        """Get the listing of either open or in progress games."""

        nick = NickMask(event.source).nick
        category = match.group(1)

        games = list()
        if category == 'open':
            games = self._get_open_games()
        elif category == 'in progress':
            games = self._get_in_progress_games()
        elif category == 'completed':
            games = self._get_completed_games()
        elif category == 'my games':
            games = self._get_active_games_with_player(nick)
        elif category == 'waiting for me':
            games = self._get_games_waiting_on_player(nick)

        if len(games) > 5:
            # just list the game ids
            gameids = []
            for game in games:
                gameids.append(str(game.id))

            return self.bot.reply(event, "Too many to list! ids: {0:s}".format(','.join(gameids)))
        else:
            # show game details, since there's not many
            gamestrs = []
            for game in games:
                gamestrs.append(game.summary())

            return self.bot.reply(event, "\n".join(gamestrs))

    def handle_start_game(self, connection, event, match):
        """Start a game, closing the period to join and starting line trading."""

        nick = NickMask(event.source).nick
        game_id = int(match.group(1))

        # retrieve the game
        game = self._get_game_details(game_id)
        if game:
            # check that the user is the owner of the game
            if event.source == game.owner_nickmask:
                # check that the game is open
                if game.status == StorycraftGame.STATUS_OPEN:
                    # start the game
                    game.status = StorycraftGame.STATUS_IN_PROGRESS
                    game.clean()
                    game.save()

                    # determine the first person to send a line
                    if self._pick_next_player(game_id):
                        # indicate the status
                        line = game.get_lines()[0]
                        player = self._get_player_by_id(line.player_id)

                        master_channel = settings.STORYCRAFT_MASTER_CHANNEL

                        # tell the control channel
                        self.bot.reply(None, "{0:s} started storycraft #{1:d}! - first player "
                                             "is {2:s}, do '!storycraft game {1:d} show line' when the game is "
                                             "assigned to you.".format(nick, game_id, player.nick),
                                       explicit_target=master_channel)
                        log.debug("%s started game #%d", nick, game_id)

                        return self.bot.reply(event, "Game #{0:d} started.".format(game_id))
                    else:
                        return self.bot.reply(event, "Failed to assign game to first player.")
                else:
                    return self.bot.reply("Game #{0:d} is not open.".format(game_id))
            else:
                return self.bot.reply(event, "You are not the owner of #{0:d}.".format(game_id))
        else:
            return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id))

    def handle_show_line(self, connection, event, match):
        """List the line to continue with, if queryer is the assignee."""

        game_id = int(match.group(1))

        # retrieve the game
        game = self._get_game_details(game_id)
        if game:
            # check that the game is in progress
            if game.status == StorycraftGame.STATUS_IN_PROGRESS:
                # get the most recent line
                lines = game.get_lines()
                if lines:
                    line = lines[0]

                    # check that the line is a prompt for input
                    if line.line == '':
                        player = self._get_player_by_id(line.player_id)

                        # check that the caller is the person the line is assigned to
                        if player.nickmask == event.source:
                            # check previous line
                            if len(lines) > 1:
                                # get previous line and display it
                                repline = lines[1]
                                return self.bot.reply(event, "The current line is '{0:s}'. Continue the story "
                                                      "with '!storycraft game {1:d} add line YOUR TEXT'."
                                                      "".format(repline.line, game_id))
                            else:
                                # player is starting the story
                                return self.bot.reply(event, "You are starting the story! Add the first line "
                                                      "with '!storycraft game {0:d} add line YOUR TEXT'."
                                                      "".format(game_id))
                        else:
                            return self.bot.reply(event, "You are not the assignee of the current game.")
                    else:
                        return self.bot.reply(event, "Game is in progress but the most recent line is not "
                                              "available - internal consistency error.")
                else:
                    return self.bot.reply(event, "Game #{0:d} has no lines - internal consistency error."
                                          "".format(game_id))
            else:
                return self.bot.reply(event, "Game #{0:d} has not been started.".format(game_id))
        else:
            return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id))

    def handle_add_line(self, connection, event, match):
        """Add a line to an in progress game."""

        nick = NickMask(event.source).nick
        game_id = int(match.group(1))
        input_line = match.group(2)

        # retrieve the game
        game = self._get_game_details(game_id)
        if game:
            # check that the game is in progress
            if game.status == StorycraftGame.STATUS_IN_PROGRESS:
                # get the most recent line
                lines = game.get_lines()
                if lines:
                    line = lines[0]

                    # check that the line is a prompt for input
                    if line.line == '':
                        player = self._get_player_by_id(line.player_id)
                        if player.nickmask == event.source:
                            # check the input line length
                            if len(input_line) <= game.line_length:
                                # add line
                                line.line = input_line
                                line.clean()
                                line.save()

                                # determine the first person to send a line
                                if self._pick_next_player(game_id):
                                    # indicate the status
                                    game = self._get_game_details(game_id)
                                    line = game.get_lines()[0]
                                    last_line = game.get_lines()[1]
                                    player = self._get_player_by_id(line.player_id)

                                    return_msg = 'Line logged.'

                                    # get progress, so user knows how much time is left
                                    progress_str = game.get_progress_string()

                                    # message the new owner
                                    if player.nick == nick:
                                        # simpily notify them they're up again
                                        return_msg = 'Line logged. Please add another. ' + progress_str
                                    else:
                                        # notify the new owner, too
                                        self.bot.reply(None, "You have a new line in storycraft "
                                                             "#{0:d}: '{1:s}' {2:s}"
                                                             "".format(game_id, last_line.line, progress_str),
                                                       explicit_target=player.nick)

                                    master_channel = settings.STORYCRAFT_MASTER_CHANNEL

                                    log.debug("%s added a line to #%d", nick, game_id)
                                    # log output
                                    if game.status == StorycraftGame.STATUS_IN_PROGRESS:
                                        self.bot.reply(None, "{0:s} added a line to storycraft "
                                                             "#{1:d}! - next player is {2:s}"
                                                             "".format(nick, game_id, player.nick),
                                                       explicit_target=master_channel)
                                        return self.bot.reply(event, return_msg)
                                    else:
                                        self.bot.reply(None, "{0:s} finished storycraft #{1:d}!"
                                                             "".format(nick, game_id),
                                                       explicit_target=master_channel)
                                        return self.bot.reply(event, "Line logged (and game completed).")
                                else:
                                    return self.bot.reply(event, "Failed to assign game to next player.")
                            else:
                                return self.bot.reply(event, "The maximum line length for this game is {0:d} "
                                                      "characters. (You were over by {1:d}.)"
                                                      "".format(game.line_length, len(input_line)-game.line_length))
                        else:
                            return self.bot.reply(event, "You are not the assignee of the current game.")
                    else:
                        return self.bot.reply(event, "Game is in progress but the most recent line is not "
                                              "available - internal consistency error.")
                else:
                    return self.bot.reply(event, "Game #{0:d} has no lines - internal consistency error."
                                          "".format(game_id))
            else:
                return self.bot.reply(event, "Game #{0:d} has not been started.".format(game_id))
        else:
            return self.bot.reply(event, "Game #{0:d} does not exist.".format(game_id))

    @staticmethod
    def _get_game_details(game_id):
        """Get the details of one game."""

        try:
            game = StorycraftGame.objects.get(pk=game_id)
            return game
        except StorycraftGame.DoesNotExist:
            return None

    @staticmethod
    def _add_new_game(game_length, line_length, lines_per_turn, nick, nickmask):
        """Add a new game to the system."""

        game = StorycraftGame.objects.create(game_length=game_length, line_length=line_length,
                                             lines_per_turn=lines_per_turn, owner_nick=nick,
                                             owner_nickmask=nickmask)
        return game

    @staticmethod
    def _get_player_list_for_game(game_id):
        """Get the list of players in one game."""

        try:
            game = StorycraftGame.objects.get(pk=game_id)
            players = StorycraftPlayer.objects.filter(game=game)
            return players
        except StorycraftGame.DoesNotExist:
            return []

    def _get_game_exists(self, game_id):
        """Return the existence of a game."""

        return self._get_game_details(game_id) is not None

    def _pick_next_player(self, game_id):
        """Based on the game's settings and state, set who will be replying next."""

        game = self._get_game_details(game_id)
        if game:
            count_by_latest_player = 0
            latest_player_id = 0

            # get the lines in this game, latest first
            lines = game.get_lines()

            for line in lines:
                if latest_player_id == 0:
                    latest_player_id = line.player_id

                if latest_player_id == line.player_id:
                    count_by_latest_player += 1
                else:
                    break

            # now we know how many times the most recent person has responded,
            # figure out what to do
            if count_by_latest_player == 0:
                # must be a new game, get a random player
                players = self._get_player_list_for_game(game.id)
                random_player = players[random.randint(1, len(players))-1]
                return self._assign_game_to_player(game_id, random_player.id)
            elif count_by_latest_player >= game.lines_per_turn and len(lines) < game.game_length:
                # player has reached the max, get a random different player to assign
                players = self._get_player_list_for_game(game.id)
                random_player_id = latest_player_id
                if len(players) > 1:
                    while random_player_id == latest_player_id:
                        random_player = players[random.randint(1, len(players))-1]
                        random_player_id = random_player.id
                return self._assign_game_to_player(game_id, random_player_id)
            elif count_by_latest_player >= game.lines_per_turn and len(lines) >= game.game_length:
                # end of game
                game.status = StorycraftGame.STATUS_COMPLETED
                game.clean()
                game.save()
                return game
            elif count_by_latest_player < game.lines_per_turn:
                # player should continue getting lines
                return self._assign_game_to_player(game_id, latest_player_id)

    @staticmethod
    def _assign_game_to_player(game_id, player_id):
        """Assign the game to a player, prompting them for responses."""

        try:
            game = StorycraftGame.objects.get(pk=game_id)
        except StorycraftGame.DoesNotExist:
            return None

        try:
            player = StorycraftPlayer.objects.get(pk=player_id)
        except StorycraftPlayer.DoesNotExist:
            return None

        line = StorycraftLine.objects.create(game=game, player=player)
        return line

    @staticmethod
    def _get_player_by_id(player_id):
        """Get the player details based on id."""

        try:
            player = StorycraftPlayer.objects.get(pk=player_id)
            return player
        except StorycraftPlayer.DoesNotExist:
            return None

    @staticmethod
    def _get_player_by_userhost_in_game(game_id, nickmask):
        """Get the player details if they exist in the given game."""

        try:
            game = StorycraftGame.objects.get(pk=game_id)
        except StorycraftGame.DoesNotExist:
            return None

        players = StorycraftPlayer.objects.filter(nickmask=nickmask, game=game)
        if len(players) == 1:
            return players[0]
        else:
            return None

    def _get_completed_games(self):
        """Get the games with COMPLETED status."""

        return self._get_games_of_type(StorycraftGame.STATUS_COMPLETED)

    def _get_in_progress_games(self):
        """Get the games with IN PROGRESS status."""

        return self._get_games_of_type(StorycraftGame.STATUS_IN_PROGRESS)

    def _get_open_games(self):
        """Get the games with OPEN status."""

        return self._get_games_of_type(StorycraftGame.STATUS_OPEN)

    @staticmethod
    def _get_active_games_with_player(player_nick):
        """Return the in progress/open games that include the player nick."""

        players = StorycraftPlayer.objects.filter(nick=player_nick)
        game_ids = [x.game.pk for x in players]

        return StorycraftGame.objects.filter(pk__in=game_ids).filter(status__in=[StorycraftGame.STATUS_OPEN,
                                                                                 StorycraftGame.STATUS_IN_PROGRESS])

    @staticmethod
    def _get_games_waiting_on_player(player_nick):
        """Return the games where the player nick is the owner of a pending line.

        Include the games that the player started and are open, since they're waiting
        on the person too.
        """

        try:
            player = StorycraftPlayer.objects.get(nick=player_nick)
        except StorycraftPlayer.DoesNotExist:
            return StorycraftGame.objects.none()

        latest_lines = StorycraftLine.objects.filter(player=player).latest('time')
        return [x.game for x in latest_lines if x.line == '']

    @staticmethod
    def _get_games_of_type(status):
        """Return the games of the specified type."""

        return StorycraftGame.objects.filter(status=status)

    def _get_player_exists_in_game(self, game_id, nickmask):
        """Return the existence of a player in a game."""

        return self._get_player_by_userhost_in_game(game_id, nickmask) is not None

    def _get_free_game_count(self):
        """Get the number of slots available, given current in progess/open games."""

        all_slots = settings.STORYCRAFT_CONCURRENT_GAMES
        count_in_progress = self._get_in_progress_games().count()
        count_open = self._get_open_games().count()
        return all_slots - count_in_progress - count_open


plugin = Storycraft