import colorsys
import contextlib
import functools
import hashlib
import os
import pdb
import random
import warnings

from .common_words import common_words, common_words_source


_skip = ["breakword", "contextlib"]
_custom_pdb = pdb.Pdb(skip=_skip)


def _breakpoint():
    if "PYTEST_CURRENT_TEST" in os.environ:
        import pytest

        pytest.set_trace(skip=_skip)

    else:
        _custom_pdb.set_trace()


_acquired = {}
_default_log = print


def set_default_logger(logger):
    """Set the default logger used by log."""
    global _default_log
    _default_log = logger


def get_logger(logger=None):
    """Return the logger, or the default logger if None."""
    return logger or _default_log


def file_source(filename, exclude=set()):
    """Return a list of words picked from the given file.

    One word per line. Words in the exclude set are removed from the list.
    """

    def make():
        if filename not in _acquired:
            if not os.path.exists(filename):
                return []
            words = {word.lower() for word in open(filename).read().split("\n")}
            words -= set(exclude)
            _acquired[filename] = list(sorted(words))
        return list(_acquired[filename])

    return make


class WordGroup:
    """Deterministic word generator.

    Generates a sequence of words from common_words, and then from
    /usr/share/dict/words, if it exists. The sequence deterministically depends
    on the name of the group.
    """

    def __init__(self, name, sources):
        self.name = name
        self.hash = int(hashlib.md5(self.name.encode()).hexdigest(), base=16)
        self.words = []
        self.sources = list(reversed(sources))
        self.random = random.Random(self.hash)
        self.index = 0
        self.current = None

    def gen(self):
        """Generate the next word."""
        if not self.words:
            if not self.sources:
                self.words = []
            else:
                next_source = self.sources.pop()
                self.words = next_source()
                self.random.shuffle(self.words)
        if self.words:
            self.current = self.words.pop()
        else:
            self.index += 1
            self.current = str(self.index)
        return self.current

    def after(self, **kw):
        after(group=self, **kw)

    def log(self, *objs, **kw):
        log(*objs, group=self, **kw)

    def word(self, **kw):
        word(group=self, **kw)

    def brk(self, **kw):
        brk(group=self, **kw)

    def logbrk(self, *objs, **kw):
        logbrk(*objs, group=self, **kw)

    def wordbrk(self, **kw):
        wordbrk(group=self, **kw)

    def term_color(self):
        """Generate a terminal color for this group."""
        return 30 + (self.hash % 8)

    def rgb(self):
        """Generate an RGB color string for this group."""
        # We generate the color in the YIQ space first because the Y component,
        # corresponding to brightness, is fairly accurate, so we can easily
        # restrict it to a range that looks decent on a white background, and
        # then convert to RGB with the standard colorsys package. The IQ
        # components control hue and have bizarre valid ranges.
        h = self.hash
        # 0.3 <= Y <= 0.6
        y = 0.3 + ((h & 0xFF) / 0xFF) * 0.4
        h >>= 16
        i = (((h & 0xFF) - 0x80) / 0x80) * 0.5957
        h >>= 16
        q = (((h & 0xFF) - 0x80) / 0x80) * 0.5226
        r, g, b = colorsys.yiq_to_rgb(y, i, q)
        r = int(r * 255)
        g = int(g * 255)
        b = int(b * 255)
        return f"rgb({r}, {g}, {b})"


class GroupsDict(dict):
    """Dictionary from str to WordGroup.

    Keys can be accessed using attribute syntax, e.g. groups.xyz instead of
    groups["xyz"].
    """

    def __getattr__(self, attr):
        return self[attr]

    def __missing__(self, group):
        self[group] = grp = WordGroup(
            name=group,
            sources=[
                common_words_source,
                file_source("/usr/share/dict/words", exclude=common_words),
            ],
        )
        return grp


groups = GroupsDict()


class BreakwordState:
    """State of the breakword generator after generation.

    Attributes:
        group: The WordGroup in use.
        word: The last word generated by the group.
        match: Whether the word matches the word we are watching for.
        logger: Logging function to use (e.g. print).
    """

    def __init__(self, group, word, match, logger=None):
        self.group = group
        self.word = word
        self.match = match
        self._logger = logger

    @property
    def logger(self):
        return get_logger(self._logger)

    def breakpoint(self, force=False):
        """Set a breakpoint if self.match or force is True."""
        if force or self.match:
            _breakpoint()

    def log(self, *objs, force=False):
        """Log the given objects if self.match or force is True."""
        if force or self.match:
            self.logger(self, *objs)

    def __bool__(self):
        """Return whether the state matches the watched word."""
        return self.match

    def __str__(self):
        gname = f"{self.group.name}:" if self.group.name else ""
        color = self.group.term_color()
        return f"\033[1;{color};40m⏎ {gname}{self.word}\033[0m"


def _get_watch(group, watch):
    def parse(w):
        if w is not None and ":" in w:
            g, w = w.split(":")
            g = groups[g]
            if g != group:
                return None
            return w
        else:
            return w

    if watch is None:
        watch = os.environ.get("BREAKWORD")

    if watch is None:
        watch = []

    if isinstance(watch, str):
        watch = watch.split(",")

    results = [w for w in [parse(w) for w in watch] if w is not None]
    return results


def breakword(group=None, gen=True, watch=None, **kwargs):
    """Generate a word in the given group and return a BreakwordState.

    Arguments:
        group: The WordGroup to use to generate the word, or some arbitrary
            string to identify the WordGroup.
        gen: Whether to generate a new word or reuse the last one (defaults
            to True).
        watch: A word or "group:word" expression representing a word to
            watch for. If None, no word is watched. Can also be a list.
    """
    if group is None:
        group = groups[""]

    watch = _get_watch(group, watch)
    word = group.gen() if gen else group.current
    match = word is not None and word in watch
    return BreakwordState(group, word, match, **kwargs)


def after(watch=None, **kw):
    """Return a state that is truthy when the last generated word == watch."""
    return breakword(watch=watch, gen=False, **kw)


def word(**kw):
    """Generate a word and return it as a string."""
    return breakword(**kw).word


def log(*objs, **kw):
    """Generate a word and log it alongside a set of objects."""
    breakword(**kw).log(*objs, force=True)


def brk(**kw):
    """Set a breakpoint when the last word generated is the one watched for."""
    breakword(gen=False, **kw).breakpoint()


def wordbrk(**kw):
    """Call word() then brk()."""
    bw = breakword(**kw)
    bw.breakpoint()
    return bw.word


def logbrk(*objs, **kw):
    """Call log() then brk()."""
    bw = breakword(**kw)
    bw.log(*objs, force=True)
    bw.breakpoint()


def breakpoint(*args, **kwargs):
    """Drop-in breakpoint function.

    When no argument is given it works like the normal breakpoint, otherwise
    it works like logbrk.
    """
    if args:
        logbrk(*args, **kwargs)
    else:
        _breakpoint()


def _track(cls, method, field, wordfn):
    m = getattr(cls, method)

    @functools.wraps(m)
    def new_method(self, *args, **kwargs):
        result = m(self, *args, **kwargs)
        setattr(self, field, wordfn())
        return result

    setattr(cls, method, new_method)
    return cls


def track_creation(*classes, function=wordbrk, field="breakword", **kwargs):
    """Track instances of the given classes.

    The __init__ of each class is modified to also set the breakword field
    to a freshly generated word. Use the BREAKWORD environment variable to
    set a breakpoint to where the instance was created.
    """
    for cls in classes:
        _track(
            cls,
            method="__init__",
            field=field,
            wordfn=lambda: function(**kwargs),
        )
    return cls


def track(obj, function=wordbrk, field="breakword", all=False, **kwargs):
    """Return the object, but track it with the `breakword` attribute."""
    word = function(**kwargs)
    try:
        if all:
            li = getattr(obj, field, [])
            if isinstance(li, list):
                li.append(word)
            else:
                warnings.warn(
                    f"breakword.track was unable to accumulate words into object.{field} because that field is already used"
                )
            setattr(obj, field, li)
        else:
            setattr(obj, field, word)
    except AttributeError:
        warnings.warn(
            f"breakword.track cannot track object of type {type(obj)}"
        )
    return obj
