#
# LOFAR Transients Key Project
#
# Uncertain quantities.
# Based on:
# (c) Robert Jordens <jordens@debian.org>
# Made available freely under the Python license
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/535164
import math
from sourcefinder.utility.coordinates import cmp


class Uncertain(object):
    """Represents a numeric value with a known small uncertainty (error,
    standard deviation...).

    Numeric operators are overloaded to work with other Uncertain or
    numeric objects.  The uncertainty (error) must be small. Otherwise
    the linearization employed here becomes wrong.
    """

    def __init__(self, value=0., error=0., *a, **t):
        self.value = value
        self.error = abs(error)
        super(Uncertain, self).__init__(*a, **t)

    def __str__(self):
        return "%g+-%g" % (self.value, self.error)

    def __repr__(self):
        return "Uncertain(%s, %s)" % (self.value, self.error)

    def __float__(self):
        return float(self.value)

    def assign(self, other):
        if isinstance(other, Uncertain):
            self.value = other.value
            self.error = other.error
        else:
            self.value = other
            self.error = 0.

    def __abs__(self):
        return Uncertain(abs(self.value), self.error)

    def __add__(self, other):
        if isinstance(other, Uncertain):
            v = self.value + other.value
            e = (self.error ** 2 + other.error ** 2) ** .5
            return Uncertain(v, e)
        else:
            return Uncertain(self.value + other, self.error)

    def __radd__(self, other):
        return self + other  # __add__

    def __sub__(self, other):
        return self + (-other)  # other.__neg__ and __add__

    def __rsub__(self, other):
        return -self + other  # __neg__ and __add__

    def __mul__(self, other):
        if isinstance(other, Uncertain):
            v = self.value * other.value
            e = ((self.error * other.value) ** 2 +
                 (other.error * self.value) ** 2) ** .5
            return Uncertain(v, e)
        else:
            return Uncertain(self.value * other, self.error * other)

    def __rmul__(self, other):
        return self * other  # __mul__

    def __neg__(self):
        return self * -1  # __mul__

    def __pos__(self):
        return self

    def __round__(self, pos):
        return round(self.value, pos)

    def __truediv__(self, other):
        return self * (1. / other)

    def __rdiv__(self, other):
        return (self / other) ** -1.  # __pow__ and __div__

    def __pow__(self, other):
        if isinstance(other, Uncertain):
            v = self.value ** other.value
            e = ((self.error * other.value *
                  self.value ** (other.value - 1.0)) ** 2 +
                 (other.error * math.log(self.value) *
                  self.value ** other.value) ** 2
                 ) ** .5
            return Uncertain(v, e)
        else:
            return Uncertain(self.value ** other,
                             self.error * other * self.value ** (other - 1))

    def __rpow__(self, other):
        assert not isinstance(other, Uncertain)
        # otherwise other.__pow__ would have been called
        return Uncertain(other ** self.value,
                         self.error * math.log(other) * other ** self.value)

    def __lt__(self, compare):
        try:
            return self.value < compare.value
        except AttributeError:
            return self.value < compare

    def __gt__(self, compare):
        try:
            return self.value > compare.value
        except AttributeError:
            return self.value > compare

    def exp(self):
        return math.e ** self

    def log(self):
        return Uncertain(math.log(self.value), self.error / self.value)

    def max(self):
        return self.value + self.error

    def min(self):
        return self.value - self.error
