# coding=utf-8
#
# Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
"""
Basic common utility functions for calculated things
"""
import os
import sys
import random
import re
import math
from argparse import ArgumentTypeError
from itertools import tee, cycle

ABORT_STATUS = -5

(X, Y) = range(2)
PY3 = sys.version_info[0] == 3

# pylint: disable=line-too-long
# Taken from https://www.w3.org/Graphics/SVG/1.1/paths.html#PathDataBNF
DIGIT_REX_PART = r"[0-9]"
DIGIT_SEQUENCE_REX_PART = rf"(?:{DIGIT_REX_PART}+)"
INTEGER_CONSTANT_REX_PART = DIGIT_SEQUENCE_REX_PART
SIGN_REX_PART = r"[+-]"
EXPONENT_REX_PART = rf"(?:[eE]{SIGN_REX_PART}?{DIGIT_SEQUENCE_REX_PART})"
FRACTIONAL_CONSTANT_REX_PART = rf"(?:{DIGIT_SEQUENCE_REX_PART}?\.{DIGIT_SEQUENCE_REX_PART}|{DIGIT_SEQUENCE_REX_PART}\.)"
FLOATING_POINT_CONSTANT_REX_PART = rf"(?:{FRACTIONAL_CONSTANT_REX_PART}{EXPONENT_REX_PART}?|{DIGIT_SEQUENCE_REX_PART}{EXPONENT_REX_PART})"
NUMBER_REX = re.compile(
    rf"(?:{SIGN_REX_PART}?{FLOATING_POINT_CONSTANT_REX_PART}|{SIGN_REX_PART}?{INTEGER_CONSTANT_REX_PART})"
)
# pylint: enable=line-too-long


def _pythonpath():
    for pth in os.environ.get("PYTHONPATH", "").split(":"):
        if os.path.isdir(pth):
            yield pth


def get_user_directory():
    """Return the user directory where extensions are stored.

    .. versionadded:: 1.1"""
    if "INKSCAPE_PROFILE_DIR" in os.environ:
        return os.path.abspath(
            os.path.expanduser(
                os.path.join(os.environ["INKSCAPE_PROFILE_DIR"], "extensions")
            )
        )

    home = os.path.expanduser("~")
    for pth in _pythonpath():
        if pth.startswith(home):
            return pth
    return None


def get_inkscape_directory():
    """Return the system directory where inkscape's core is.

    .. versionadded:: 1.1"""
    for pth in _pythonpath():
        if os.path.isdir(os.path.join(pth, "inkex")):
            return pth
    raise ValueError("Unable to determine the location of Inkscape")


class KeyDict(dict):
    """
    A normal dictionary, except asking for anything not in the dictionary
    always returns the key itself. This is used for translation dictionaries.
    """

    def __getitem__(self, key):
        try:
            return super().__getitem__(key)
        except KeyError:
            return key


def parse_percent(val: str):
    """Parse strings that are either values (i.e., '3.14159') or percentages
    (i.e. '75%') to a float.

    .. versionadded:: 1.2"""
    val = val.strip()
    if val.endswith("%"):
        return float(val[:-1]) / 100
    return float(val)


def Boolean(value):
    """ArgParser function to turn a boolean string into a python boolean"""
    if value.upper() == "TRUE":
        return True
    if value.upper() == "FALSE":
        return False
    return None


def to_bytes(content):
    """Ensures the content is bytes

    .. versionadded:: 1.1"""
    if isinstance(content, bytes):
        return content
    return str(content).encode("utf8")


def debug(what):
    """Print debug message if debugging is switched on"""
    errormsg(what)
    return what


def do_nothing(*args, **kwargs):  # pylint: disable=unused-argument
    """A blank function to do nothing

    .. versionadded:: 1.1"""


def errormsg(msg):
    """Intended for end-user-visible error messages.

    (Currently just writes to stderr with an appended newline, but could do
    something better in future: e.g. could add markup to distinguish error
    messages from status messages or debugging output.)

    Note that this should always be combined with translation::

      import inkex
      ...
      inkex.errormsg(_("This extension requires two selected paths."))
    """
    try:
        sys.stderr.write(msg)
    except TypeError:
        sys.stderr.write(str(msg))
    except UnicodeEncodeError:
        # Python 2:
        # Fallback for cases where sys.stderr.encoding is not Unicode.
        # Python 3:
        # This will not work as write() does not accept byte strings, but AFAIK
        # we should never reach this point as the default error handler is
        # 'backslashreplace'.

        # This will be None by default if stderr is piped, so use ASCII as a
        # last resort.
        encoding = sys.stderr.encoding or "ascii"
        sys.stderr.write(msg.encode(encoding, "backslashreplace"))

    # Write '\n' separately to avoid dealing with different string types.
    sys.stderr.write("\n")


class AbortExtension(Exception):
    """Raised to print a message to the user without backtrace"""


class DependencyError(NotImplementedError):
    """Raised when we need an external python module that isn't available"""


class FragmentError(Exception):
    """Raised when trying to do rooty things on an xml fragment"""


def to(kind):  # pylint: disable=invalid-name
    """
    Decorator which will turn a generator into a list, tuple or other object type.
    """

    def _inner(call):
        def _outer(*args, **kw):
            return kind(call(*args, **kw))

        return _outer

    return _inner


def strargs(string, kind=float):
    """Returns a list of floats from a string

    .. versionchanged:: 1.1
        also splits at -(minus) signs by adding a space in front of the - sign

    .. versionchanged:: 1.2
        Full support for the `SVG Path data BNF
        <https://www.w3.org/Graphics/SVG/1.1/paths.html#PathDataBNF>`_
    """
    return [kind(val) for val in NUMBER_REX.findall(string)]


class classproperty:  # pylint: disable=invalid-name, too-few-public-methods
    """Combine classmethod and property decorators"""

    def __init__(self, func):
        self.func = func

    def __get__(self, obj, owner):
        return self.func(owner)


def filename_arg(name):
    """Existing file to read or option used in script arguments"""
    filename = os.path.abspath(os.path.expanduser(name))
    if not os.path.isfile(filename):
        raise ArgumentTypeError(f"File not found: {name}")
    return filename


def pairwise(iterable, start=True):
    "Iterate over a list with overlapping pairs (see itertools recipes)"
    first, then = tee(iterable)
    starter = [(None, next(then, None))]
    if not start:
        starter = []
    return starter + list(zip(first, then))


def circular_pairwise(l):
    """Iterate over a list with overlapping pairs in a periodic way, i.e.
    [1, 2, 3] -> [(1, 2), (2, 3), (3, 1)]

    ..versionadded:: 1.3.1"""
    second = cycle(l)
    next(second)
    return zip(l, second)


EVAL_GLOBALS = {}
EVAL_GLOBALS.update(random.__dict__)
EVAL_GLOBALS.update(math.__dict__)


def math_eval(function, variable="x"):
    """Interpret a function string. All functions from math and random may be used.

    .. versionadded:: 1.1

    Returns:
        a lambda expression if sucessful; otherwise None.
    """
    try:
        if function != "":
            return eval(
                f"lambda {variable}: " + (function.strip('"') or "t"), EVAL_GLOBALS, {}
            )
    # handle incomplete/invalid function gracefully
    except SyntaxError:
        pass
    return None


def is_number(string):
    """Checks if a value is a number

    .. versionadded:: 1.2"""
    try:
        float(string)
        return True
    except ValueError:
        return False
