# Copyright 2004-2005 Joe Wreschnig, Michael Urman
#           2011 Christoph Reiter
#           2016 Ryan Dellenbaugh
#        2016-22 Nick Boultbee
#           2018 Peter Strulo
#
# 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.

from __future__ import annotations

import operator
import time
from enum import auto, Enum
from numbers import Real
from typing import TypeVar, List, Iterable, Optional

from quodlibet.formats import FILESYSTEM_TAGS, TIME_TAGS
from quodlibet.formats._audio import SIZE_TAGS, DURATION_TAGS
from quodlibet.unisearch import compile
from quodlibet.util import parse_date
from senf import fsn2text, fsnative

T = TypeVar("T")


class Error(ValueError):
    pass


class ParseError(Error):
    pass


class Node:

    def search(self, data: T) -> bool:
        raise NotImplementedError

    def filter(self, sequence: Iterable[T]) -> List[T]:
        return [s for s in sequence if self.search(s)]

    def _unpack(self) -> Node:
        return self

    def __or__(self, other: Node) -> Node:
        return NotImplemented

    def __and__(self, other: Node) -> Node:
        return NotImplemented

    def __neg__(self) -> Node:
        return Neg(self._unpack())

    @property
    def valid(self) -> bool:
        return True


class Regex(Node):

    def __init__(self, pattern: str, mod_string: str):
        self.pattern = str(pattern)
        self.mod_string = str(mod_string)

        ignore_case = "c" not in self.mod_string or "i" in self.mod_string
        dot_all = "s" in self.mod_string
        asym = "d" in self.mod_string
        try:
            re = compile(self.pattern, ignore_case, dot_all, asym)
            self.search = re  # type: ignore
        except ValueError:
            raise ParseError(
                "The regular expression /%s/ is invalid." % self.pattern)

    def __repr__(self):
        return "<Regex pattern=%s mod=%s>" % (self.pattern, self.mod_string)


class True_(Node):
    """Always True"""

    def search(self, data):
        return True

    def filter(self, sequence):
        return list(sequence)

    def __repr__(self):
        return "<True>"

    def __or__(self, other):
        return self

    def __and__(self, other):
        other = other._unpack()
        return other


class False_(Node):
    """Always False"""

    def search(self, data):
        return False

    def filter(self, sequence):
        return []

    def __repr__(self):
        return "<False>"

    def __or__(self, other):
        other = other._unpack()
        return other

    def __and__(self, other):
        return self


class Union(Node):
    """True if the object matches any of its REs."""

    def __init__(self, res: List[Node]):
        self.res = res

    def search(self, data):
        for re in self.res:
            if re.search(data):
                return True
        return False

    def __repr__(self):
        return "<Union %r>" % self.res

    def __or__(self, other):
        other = other._unpack()

        if isinstance(other, Union):
            return Union(self.res + list(other.res))
        elif isinstance(other, True_):
            return other.__or__(self)

        return Union(self.res + [other])

    def __and__(self, other):
        other = other._unpack()

        if isinstance(other, (Inter, True_)):
            return other.__and__(self)

        return Inter([self, other])


class Inter(Node):
    """True if the object matches all of its REs."""

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

    def search(self, data):
        for re in self.res:
            if not re.search(data):
                return False
        return True

    def filter(self, sequence):
        current = sequence
        for re in self.res:
            current = filter(re.search, current)
        if not isinstance(current, list):
            current = list(current)
        return current

    def __repr__(self):
        return "<Inter %r>" % self.res

    def __and__(self, other):
        other = other._unpack()

        if isinstance(other, Inter):
            return Inter(self.res + other.res)

        if isinstance(other, True_):
            return other.__and__(self)

        return Inter(self.res + [other])

    def __or__(self, other):
        other = other._unpack()
        if isinstance(other, (Union, True_)):
            return other.__or__(self)
        return Union([self, other])


class Neg(Node):
    """True if the object doesn't match its RE."""

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

    def search(self, data):
        return not self.res.search(data)

    def __repr__(self):
        return "<Neg %r>" % self.res

    def __and__(self, other):
        other = other._unpack()
        if isinstance(other, True_):
            return other.__and__(self)
        return Inter([self, other])

    def __or__(self, other):
        other = other._unpack()
        if isinstance(other, True_):
            return other.__or__(self)
        return Union([self, other])

    def __neg__(self):
        return self.res


class Numcmp(Node):
    """Numeric comparisons"""

    operators = {
        "<": operator.lt,
        "<=": operator.le,
        ">": operator.gt,
        ">=": operator.ge,
        "=": operator.eq,
        "==": operator.eq,
        "!=": operator.ne,
    }

    def __init__(self, expr: Numexpr, op: str, expr2: Numexpr):
        self._expr = expr
        self._op = self.operators[op]
        self._expr2 = expr2
        units = expr2.units()
        if units and isinstance(expr, NumexprTag) and not expr.valid_for_units(units):
            raise ParseError(f"Wrong units for {expr}")

    def search(self, data):
        time_ = time.time()
        use_date = self._expr.use_date() or self._expr2.use_date()
        val = self._expr.evaluate(data, time_, use_date)
        val2 = self._expr2.evaluate(data, time_, use_date)
        if val is not None and val2 is not None:
            return self._op(val, val2)
        return False

    def __repr__(self):
        return "<Numcmp expr=%r, op=%r, expr2=%r>" % (
            self._expr, self._op.__name__, self._expr2)

    def __and__(self, other):
        other = other._unpack()
        if isinstance(other, True_):
            return other.__and__(self)
        return Inter([self, other])

    def __or__(self, other):
        other = other._unpack()
        if isinstance(other, True_):
            return other.__or__(self)
        return Union([self, other])


class Numexpr:
    """Expression in numeric comparison"""

    def evaluate(self, data: T, time: float, use_date: bool):
        """Evaluate the expression to a number.
        :param data: is the audiofile to evaluate for
        :param time: is the current time
        :param use_date: whether to evaluate as a date
                         (used to handle expressions like 2015-02-11
                          that look like dates and subtraction)
        """
        raise NotImplementedError

    def use_date(self) -> bool:
        """Returns whether to force the final comparison to compare the date
        values instead of the number values."""
        return False

    def units(self) -> Optional[Units]:
        """Returns optional (converted) units for this number"""
        return None


class NumexprTag(Numexpr):
    """Numeric tag"""

    def __init__(self, tag: str):
        self._tag = tag
        self._ftag = "~#" + self._tag

    def valid_for_units(self, units: Units) -> bool:
        """Returns true if the given unit is valid for this expression's tag"""
        return self._ftag in UNITS_TO_TAGS[units]

    def evaluate(self, data, time, use_date):
        if self._tag == 'date':
            date = data('date')
            if not date:
                return None
            try:
                num = parse_date(date)
            except ValueError:
                return None
        else:
            num = data(self._ftag, None)
        if num is not None:
            # Strip aggregate function from tag
            func_start = self._ftag.find(":")
            tag = self._ftag[:func_start] if func_start >= 0 else self._ftag
            if tag in TIME_TAGS:
                num = time - num
            return round(num, 2)
        return None

    def __repr__(self):
        return "<NumexprTag tag=%r>" % self._tag

    def use_date(self):
        return self._tag == 'date'


class NumexprUnary(Numexpr):
    """Unary numeric operation (like -)"""

    operators = {
        '-': operator.neg
    }

    def __init__(self, op: str, expr: Numexpr):
        self.__op = self.operators[op]
        self.__expr = expr

    def evaluate(self, data, time, use_date):
        val = self.__expr.evaluate(data, time, use_date)
        if val is not None:
            return self.__op(val)
        return None

    def __repr__(self):
        return "<NumexprUnary op=%r expr=%r>" % (self.__op, self.__expr)

    def use_date(self):
        return self.__expr.use_date()


class NumexprBinary(Numexpr):
    """Binary numeric operation (like + or *)"""

    operators = {
        '-': operator.sub,
        '+': operator.add,
        '*': operator.mul,
        '/': operator.floordiv,
    }

    precedence = {
        operator.sub: 1,
        operator.add: 1,
        operator.mul: 2,
        operator.floordiv: 2,
    }

    def __init__(self, op: str, expr: Numexpr, expr2: Numexpr):
        self.__op = self.operators[op]
        self.__expr = expr
        self.__expr2 = expr2
        # Rearrange expressions for operator precedence
        if (isinstance(expr, NumexprBinary)
                and self.precedence[expr.__op] < self.precedence[self.__op]):
            self.__expr = expr.__expr
            self.__op = expr.__op
            expr.__expr = expr.__expr2
            expr.__op = self.operators[op]
            expr.__expr2 = expr2
            self.__expr2 = expr

    def evaluate(self, data, time, use_date):
        val = self.__expr.evaluate(data, time, use_date)
        val2 = self.__expr2.evaluate(data, time, use_date)
        if val is not None and val2 is not None:
            try:
                return self.__op(val, val2)
            except ZeroDivisionError:
                return val * float('inf')
        return None

    def __repr__(self):
        return "<NumexprBinary op=%r expr=%r expr2=%r>" % (
            self.__op, self.__expr, self.__expr2)

    def use_date(self):
        return self.__expr.use_date() or self.__expr2.use_date()


class NumexprGroup(Numexpr):
    """Parenthesized group in numeric expression"""

    def __init__(self, expr: Numexpr):
        self.__expr = expr

    def evaluate(self, data, time, use_date):
        return self.__expr.evaluate(data, time, use_date)

    def __repr__(self):
        return "<NumexprGroup expr=%r>" % (self.__expr)

    def use_date(self):
        return self.__expr.use_date()


class NumexprNumber(Numexpr):
    """Number in numeric expression"""

    def __init__(self, value: Real, units: Units = None):
        self._value = float(value)
        self._units = units

    def evaluate(self, data, time, use_date):
        return self._value

    def units(self) -> Optional[Units]:
        return self._units

    def __repr__(self):
        return "<NumexprNumber value=%.2f>" % (self._value)


class NumexprNow(Numexpr):
    """Current time, with optional offset"""

    def __init__(self, offset=0):
        self.__offset = offset

    def evaluate(self, data, time, use_date):
        return time - self.__offset

    def __repr__(self):
        return "<NumexprNow offset=%r>" % (self.__offset)


class NumexprNumberOrDate(Numexpr):
    """An ambiguous value like 2015-09-25 than can be interpreted as either
    a number or a date."""

    def __init__(self, date):
        self.date = parse_date(date)
        parts = date.split('-')
        self.number = int(parts[0])
        if len(parts) > 1:
            self.number -= int(parts[1])
        if len(parts) > 2:
            self.number -= int(parts[2])

    def evaluate(self, data, time, use_date):
        if use_date:
            return self.date
        else:
            return self.number

    def __repr__(self):
        return ('<NumexprNumberOrDate number=%r date=%r>' %
                (self.number, self.date))


class Units(Enum):
    SECONDS = auto()
    BYTES = auto()


UNITS_TO_TAGS = {Units.SECONDS: TIME_TAGS | DURATION_TAGS, Units.BYTES: SIZE_TAGS}


def numexprUnit(value, unit):
    """Process numeric units and return NumexprNumber"""

    unit = unit.lower().strip()
    converted = Units.SECONDS
    # Time units
    if unit.startswith("second"):
        value = value
    elif unit.startswith("minute"):
        value *= 60
    elif unit.startswith("hour"):
        value *= 60 * 60
    elif unit.startswith("day"):
        value *= 24 * 60 * 60
    elif unit.startswith("week"):
        value *= 7 * 24 * 60 * 60
    elif unit.startswith("month"):
        value *= 30 * 24 * 60 * 60
    elif unit.startswith("year"):
        value *= 365 * 24 * 60 * 60
    # Size units
    elif unit.startswith("g"):
        value *= 1024 ** 3
        converted = Units.BYTES
    elif unit.startswith("m"):
        value *= 1024 ** 2
        converted = Units.BYTES
    elif unit.startswith("k"):
        value *= 1024
        converted = Units.BYTES
    elif unit.startswith("b"):
        converted = Units.SECONDS
    elif unit:
        raise ParseError("No such unit: %r" % unit)
    return NumexprNumber(value, converted)


def numexprTagOrSpecial(tag):
    """Handle special values that look like tags"""

    if tag == "now":
        return NumexprNow()
    if tag == "today":
        return NumexprNow(offset=24 * 60 * 60)
    else:
        return NumexprTag(tag)


class Tag(Node):
    """See if a property of the object matches its RE."""

    # Shorthand for common tags.
    ABBRS = {"a": "artist",
             "b": "album",
             "v": "version",
             "t": "title",
             "n": "tracknumber",
             "d": "date",
             }

    def __init__(self, names, res):
        self.res = res
        self._names = []
        self.__intern = []
        self.__fs = []

        names = [Tag.ABBRS.get(n.lower(), n.lower()) for n in names]
        for name in names:
            if name[:1] == "~":
                if name.startswith("~#"):
                    raise ValueError("numeric tags not supported")
                if name in FILESYSTEM_TAGS:
                    self.__fs.append(name)
                else:
                    self.__intern.append(name)
            else:
                self._names.append(name)

    def search(self, data):
        search = self.res.search
        fs_default = fsnative()

        for name in self._names:
            val = data.get(name)
            if val is None:
                if name in ("filename", "mountpoint"):
                    val = fsn2text(data.get("~" + name, fs_default))
                else:
                    val = data.get("~" + name, u"")

            if search(val):
                return True

        for name in self.__intern:
            if search(data(name)):
                return True

        for name in self.__fs:
            if search(fsn2text(data(name, fs_default))):
                return True

        return False

    def __repr__(self):
        names = self._names + self.__intern
        return ("<Tag names=%r, res=%r>" % (names, self.res))

    def __and__(self, other):
        other = other._unpack()
        if isinstance(other, True_):
            return other.__and__(self)
        return Inter([self, other])

    def __or__(self, other):
        other = other._unpack()
        if isinstance(other, True_):
            return other.__or__(self)
        return Union([self, other])


class Extension(Node):
    """Plugin-defined query extension

    Syntax is @(plugin_name) or @(plugin_name: body)

    Raises a ParseError if no plugin is loaded for the name, or if the plugin
    fails to parse the body"""

    def __init__(self, name, body):
        # pulls in gtk+
        from quodlibet.plugins.query import QUERY_HANDLER, QueryPluginError

        self.__name = name
        self.__valid = True
        self.__body = body

        try:
            self.__plugin = QUERY_HANDLER.get_plugin(name)
        except KeyError:
            self.__valid = False
            return

        try:
            self.__body = self.__plugin.parse_body(body)
        except QueryPluginError:
            self.__valid = False
            return

    def search(self, data):
        return self.__valid and self.__plugin.search(data, self.__body)

    @property
    def valid(self) -> bool:
        return self.__valid

    def __repr__(self):
        return ('<Extension name=%r valid=%r body=%r>'
                % (self.__name, self.__valid, self.__body))

    def __and__(self, other):
        other = other._unpack()

        if isinstance(other, (Inter, True_)):
            return other.__and__(self)

        return Inter([self] + [other])

    def __or__(self, other):
        other = other._unpack()

        if isinstance(other, (Union, True_)):
            return other.__or__(self)

        return Union([self, other])
