"""
Lightweight options machinery.

Based on https://github.com/topper-123/optioneer, but simplified (don't deal
with nested options, deprecated options, ..), just the attribute-style dict
like holding the options and giving a nice repr.
"""
from collections import namedtuple
import textwrap


Option = namedtuple("Option", "key default_value doc validator callback")


class Options(object):
    """Provide attribute-style access to configuration dict."""

    def __init__(self, options):
        super().__setattr__("_options", options)
        # populate with default values
        config = {}
        for key, option in options.items():
            config[key] = option.default_value

        super().__setattr__("_config", config)

    def __setattr__(self, key, value):
        # you can't set new keys
        if key in self._config:
            option = self._options[key]
            if option.validator:
                option.validator(value)
            self._config[key] = value
            if option.callback:
                option.callback(key, value)
        else:
            msg = "You can only set the value of existing options"
            raise AttributeError(msg)

    def __getattr__(self, key):
        try:
            return self._config[key]
        except KeyError:
            raise AttributeError("No such option")

    def __dir__(self):
        return list(self._config.keys())

    def __repr__(self):
        cls = self.__class__.__name__
        description = ""
        for key, option in self._options.items():
            descr = "{key}: {cur!r} [default: {default!r}]\n".format(
                key=key, cur=self._config[key], default=option.default_value
            )
            description += descr

            if option.doc:
                doc_text = "\n".join(textwrap.wrap(option.doc, width=70))
            else:
                doc_text = "No description available."
            doc_text = textwrap.indent(doc_text, prefix="    ")
            description += doc_text + "\n"
        space = "\n  "
        description = description.replace("\n", space)
        return "{}({}{})".format(cls, space, description)


def _validate_display_precision(value):
    if value is not None:
        if not isinstance(value, int) or not (0 <= value <= 16):
            raise ValueError("Invalid value, needs to be an integer [0-16]")


display_precision = Option(
    key="display_precision",
    default_value=None,
    doc=(
        "The precision (maximum number of decimals) of the coordinates in "
        "the WKT representation in the Series/DataFrame display. "
        "By default (None), it tries to infer and use 3 decimals for projected "
        "coordinates and 5 decimals for geographic coordinates."
    ),
    validator=_validate_display_precision,
    callback=None,
)


def _validate_bool(value):
    if not isinstance(value, bool):
        raise TypeError("Expected bool value, got {0}".format(type(value)))


def _default_use_pygeos():
    import geopandas._compat as compat

    return compat.USE_PYGEOS


def _callback_use_pygeos(key, value):
    assert key == "use_pygeos"
    import geopandas._compat as compat

    compat.set_use_pygeos(value)


use_pygeos = Option(
    key="use_pygeos",
    default_value=_default_use_pygeos(),
    doc=(
        "Whether to use PyGEOS to speed up spatial operations. The default is True "
        "if PyGEOS is installed, and follows the USE_PYGEOS environment variable "
        "if set."
    ),
    validator=_validate_bool,
    callback=_callback_use_pygeos,
)


options = Options({"display_precision": display_precision, "use_pygeos": use_pygeos})
