import math
import doctest
import typing 

# ========================================================
# ========================================================
# === DURATION
# ========================================================
# ========================================================

class Duration:
    """ Class to convert dates for astronomy

    Date formats are:

        * day = days. e.g. 12.3457
        * dhms = day, hour, minutes seconds. e.g. 2d12h34m55.23s
    """
    
# ========================================================
# === internal methods
# ========================================================

    def _init(self,duration="") -> None:
        """ Initialize internal attributes.

        :param duration: Duration in any supported format (cf. help(Duration))
        :type date: any 
        :returns: None
        :rtype: None
 
        :Example:
            
        >>> objduration = Duration()
        >>> objduration._init()
        
        """
        self._init_duration = duration
        self._init_durationformat = 0
        self._computed_day = 0
        self._day = 0
        self._computed_dhms = 0
        self._dhms = "0d00h00m00s"
        self._dhms_format = "0.3"

    def _is_number(self,s) -> bool:
        """ Return True if the string is a number else return False.

        :param s: A string to test
        :type s: string
        :returns: True is the string can be concerted into a float
        :rtype: bool
 
        :Example:

        >>> objdate = Date()
        >>> objdate._is_number("3e5")
        True
        >>> objdate._is_number("3a5")
        False
        
        """
        try:
            float(s)
            return True
        except ValueError:
            pass 
        try:
            import unicodedata
            unicodedata.numeric(s)
            return True
        except (TypeError, ValueError):
            pass 
        return False

    def _day2dhms(self, day, dhms_format) -> str:
        if (self._computed_day == 0):
            return ""        
        symbols = str(dhms_format)       
        digits = ""
        separator=0; # 0=dhms 1=space 2=:
        sign=0; #
        zeros=0; # 0=spaces 1=leading zeros
        k=0
        for car in symbols:
            if (car==' '): 
                separator=1
            if (car=='_'): 
                separator=1
            if (car==':'): 
                separator=2
            if (car=='+'): 
                sign=1
            if (self._is_number(car)) or car=='.': 
                digits = symbols[k:]
                break
            k += 1
        #	int unit=0; // 0=deg 1=hours
        #	int modulo=0; // 0=360 1=180
        #	int separator=0; // 0=hdms 1=space 2=:
        #	int sign=0; // 0=[0:modulo] 1=[-modulo/2:modulo/2]
        # === trailing format (digits)
        nb_decimalsec = 2
        ld = len(digits)
        if (ld>0):
            # e.g. digits = ".3"
            car = digits[0]
            if car=='0':
                zeros=1
                digits = digits[1:]
            # now digits = ".3" and zeros=1
            kd = digits.find(".")
            if kd >= 0:
                if (self._is_number(digits[kd+1:])==True):
                    nb_decimalsec = int(digits[kd+1:])
                # now digits_nb_decimal="3"
        #print("ld="+str(ld))            
        #print("symbols="+symbols)
        #print("digits="+digits)
        #print("separator="+str(separator))
        #print("sign="+str(sign))
        #print("zeros="+str(zeros))
        #print("nb_decimal="+str(nb_decimalsec))
        # --- sign of the input angle
        s = 1
        if day<0:
            s = -1
            day = -day
        # --- compute the trheee components of dd hh mm ss
        r = day
        dd = int(math.floor(r))
        hh = 0
        mm = 0
        ss = 0
        r = (r-dd)*24
        hh = int(math.floor(r))
        r = (r-hh)*60
        mm = int(math.floor(r))
        ss = (r-mm)*60
        #print("hh ="+str(hh))
        #print("mm ="+str(mm))
        #print("ss ="+str(ss))
        # === Compute the result
        result = ""
        # --- sign
        if sign==1 and s>=0:
            result += "+"
        elif s<0:
            result += "-"
        # --- dd
        fstring  = "{:d}"
        result += fstring.format(dd)
        # --- separator dd hh
        if separator==1:
            result += " "
        elif separator==2:
            result += ":"
        else:
            result += "d"
        # --- hh
        if zeros==0:
            fstring  = "{:d}"
        else:
            fstring  = "{:02d}"
        result += fstring.format(hh)
        # --- separator hh mm
        if separator==1:
            result += " "
        elif separator==2:
            result += ":"
        else:
            result += "h"
        # --- mm
        if zeros==0:
            fstring  = "{:d}"
        else:
            fstring  = "{:02d}"
        result += fstring.format(mm)
        # --- separator mm ss
        if separator==1:
            result += " "
        elif separator==2:
            result += ":"
        else:
            result += "m"
        # --- ss
        if zeros==0:
            fstring  = "{:."+str(nb_decimalsec)+"f}"
        else:
            if nb_decimalsec==0:
                fstring  = "{:0"+str(nb_decimalsec+2)+".0f}"
            else:
                fstring  = "{:0"+str(nb_decimalsec+3)+"."+str(nb_decimalsec)+"f}"
        #print("fstring="+fstring)
        result += fstring.format(ss)
        # --- separator ss
        if separator==0:
            result += "s"
        # -- end
        return 0, result

    def _duration2day(self, duration) -> float:
        """ Return a duration in day unit from a duration in ymdhms format.

        :param duration: A string formated as "2d7h23m12.5s"
        :type duration: string
        :returns: duration expressed in day and fraction of day
        :rtype: float
 
        :Example:

        >>> objduration = Date()
        >>> objduration._duration2day("-0d7h23m12.5s")
        0.30778356481481484
        
        """
        str_duration = str(duration)
        lst_durations = str_duration.split()
        str_duration = " ".join(lst_durations)
        numbs = "+-.E0123456789"
        kd = -1
        kh = -1
        km = -1
        ks = -1
        da = 0
        ho = 0
        mi = 0
        se = 0
        sign = 1
        k = 0
        knum = -1
        dur = 0
        #print("km={} ks={}".format(km,ks))
        for car in str_duration:
            if (car == '-') and (knum == -1):
                sign = -1
            if (car in numbs) and (knum == -1):
                knum = k
            #print("k={} sign={} car={} knum=={} kd={} kh={} km={} ks={}".format(k,sign,car,knum,kd,kh,km,ks))
            if (car=='d' or car=='D') and (kd == -1) and (knum >= 0) :                
                k1 = knum
                k2 = k
                str_val = str_duration[k1:k2]
                if (self._is_number(str_val) == True):                
                    kd = k1
                    da = math.fabs(float(str_val))
                    knum = -1
                    dur += da
            if (car=='h' or car=='H') and (kh == -1) and (knum >= 0) :                
                k1 = knum
                k2 = k
                str_val = str_duration[k1:k2]
                #print("str_val={}".format(str_val))
                if (self._is_number(str_val) == True):                
                    kh = k1
                    ho = math.fabs(float(str_val))
                    knum = -1
                    dur += ho/24.
            if ( (car=='m' or car=='M') and (km == -1) ) and (knum >= 0) :
                k1 = knum
                k2 = k
                str_val = str_duration[k1:k2]
                #print("str_val={}".format(str_val))
                if (self._is_number(str_val) == True):                
                    km = k1
                    mi = math.fabs(float(str_val))
                    knum = -1
                    dur += mi/1440.
            if (car=='s' or car=='S') and (ks == -1) and (knum >= 0) :                
                k1 = knum
                k2 = k
                str_val = str_duration[k1:k2]
                #print("str_val={}".format(str_val))
                if (self._is_number(str_val) == True):                
                    ks = k1
                    se = math.fabs(float(str_val))
                    knum = -1
                    dur += se/86400.        
            k += 1
            if (car not in numbs):
                knum = -1
            #print("END k={} car={} knum=={} numbs={}".format(k,car,knum,numbs))
        if (dur==0):
            lst_durations = str_duration.split()
            #print("(1) lst_durations={}".format(lst_durations))
            lst_durations = [ math.fabs(float(x)) for x in lst_durations if (self._is_number(x) == True) ]
            #print("(2) lst_durations={}".format(lst_durations))
            n_duration = len(lst_durations)
            if (n_duration==1):
                da = lst_durations[0]
                dur = da
            elif (n_duration>=2):
                se = lst_durations[-1]
                dur += se/86400.
                mi = lst_durations[-2]
                dur += mi/1440.
                if (n_duration>=3):
                    ho = lst_durations[-3]
                    dur += ho/24.
                    if (n_duration>=3):
                        da = lst_durations[-4]
                        dur += da
        dur *= sign
        decode = (sign, da, ho, mi, se)
        return decode, dur

    def _duration_compare(self, duration, operator):
        """ Comparaison of durations for various operators.

        :param duration: An duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :param operator : Operator such as == != > >= < <=
        :type operator : string
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(6.12) ; objduration2.duration("5d2m") ; objduration1._duration_compare(objduration2,">")
        6.12
        '5d2m'
        True
        
        .. note:: Does not account for the modulo.
        """
        if isinstance(duration, Duration) == False:
            duration =  Duration(duration)
        if self._computed_day == 0:
            self.day()
        if duration._computed_day == 0:
            duration.day()
        res = False
        if (self._computed_day == 1) and (duration._computed_day == 1):
            toeval = str(self._day)+" "+operator+" "+str(duration._day)
            res = eval(toeval)
        return res

# ========================================================
# === duration methods
# ========================================================

    def duration_duration2day(self,duration) -> typing.Tuple[int, float]:
        """ Compute day number from any duration format

        :param date: A string formated as "2d7h23m12.5s" or "2 7 23 12.5"
        :type date: string
        :returns: A tuple of decode, julian day. decode : (sign, da, ho, mi, se)
        :rtype: tuple(tuple, float)
 
        :Example:

        >>> objduration = Duration()
        >>> objduration.duration_duration2day("2d7h23m12.5s")
        ((1, 2.0, 7.0, 23.0, 12.5), 2.3077835648148146)

        .. note:: Prefer using objduration.duration() followed by objduration.day().            
        """
        decode, day = self._duration2day(duration)
        self._computed_day = 1
        return decode, day

# ========================================================
# === get/set methods
# ========================================================

    def duration(self, duration=""):
        """ Set the input duration in any format

        :param duration: duration is a duration in any supported format (cf. help(Duration))
        :type duration: any
        :returns: The input duration.
        :rtype: string
 
        :Example:

        >>> objduration = Duration()
        >>> objduration.duration("2d7h23m12.5s")
        '2d7h23m12.5s'
        
        .. note:: After using objdate.duration() get conversions with methods as objdate.day() or objdate.dhms().            
        """
        if duration != "":
            if (duration != self._init_duration):
                self._init(duration)
        return self._init_duration

    def day(self):
        """ Get the date in julian day format

        :returns: The julian day.
        :rtype: float
 
        :Example:

        >>> objduration = Duration()
        >>> objduration.duration("2d7h23m12.5s")
        '2d7h23m12.5s'
        >>> objduration.day()
        2.3077835648148146
        
        .. note:: After using objdate.duration() get conversion with objdate.day().
        """
        if (self._computed_day == 0):
            init_durationformat, day = self.duration_duration2day(self._init_duration)
            self._init_durationformat = init_durationformat
            self._computed_day = 1        
            self._day = day
        return self._day

    def dhms(self, dhms_format):
        """ Get the date in dhms format

        :param dhms_format: dhms format (cf. help(Duration))
        :type dhms_format: string
        :returns: The formated string.
        :rtype: string
 
        :Example:

        >>> objduration = Duration()
        >>> objduration.duration("2d7h3m12.5s")
        '2d7h3m12.5s'
        >>> objduration.dhms("0")
        '2d07h03m12.50s'
        
        .. note:: After using objdate.duration() get conversion with objdate.dhms().
        """
        if (self._computed_day == 0):
            self.day()
        if (self._computed_dhms == 0) or (self._dhms_format != dhms_format) :
            error, dhms = self._day2dhms(self._day,dhms_format)
            self._dhms_format = dhms_format
            self._dhms = dhms
            self._computed_dhms = 1
        return self._dhms

# ========================================================
# === debug methods
# ========================================================
    
    def infos(self, action) -> None:
        """ To get informations about this class
        
        :param action: A command to run a debug action (see examples).
        :type action: string
        
        :Example:
            
        Duration().infos("doctest")
        Duration().infos("doc_methods")
        Duration().infos("internal_attributes")
        Duration().infos("public_methods")        
        """
        if (action == "doc_methods"):
            publics = [x for x in dir(self) if x[0]!="_"]
            for public in publics:
                varname = "{}".format(public)
                if (callable(getattr(self,varname))==True):
                    print("\n{:=^40}".format(" method "+varname+" "))
                    t = "Duration()."+varname+".__doc__"
                    tt =eval(t)
                    print(tt)
        if (action == "doctest"):
            if __name__ == "__main__":
                print("\n{:~^40}".format("doctest"))
                #doctest.testmod(verbose=True, extraglobs={'objangle': Duration()})
                doctest.testmod(verbose=True)
        if (action == "internal_attributes"):
            internals = [x for x in dir(self) if x[0]=="_" and x[1]!="_"]
            for internal in internals:
                varname = "{}".format(internal)
                #if (hasattr(self,varname)==True):
                if (callable(getattr(self,varname))==False):
                    print(varname + "=" + str(getattr(self,varname)))
        if (action == "public_methods"):
            publics = [x for x in dir(self) if x[0]!="_"]
            for public in publics:
                varname = "{}".format(public)
                if (callable(getattr(self,varname))==True):
                    print(varname)

# ========================================================
# === special methods
# ========================================================
        
    def __init__(self, duration=""):
        """ Object initialization where duration is the input in any format.

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: any
        
        """
        self._init(duration)

    def __add__(self, duration):
        """ Add a duration to a duration.

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The result of the addition
        :rtype: Duration()
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.25) ; objduration2.duration("56d28m") ; objduration = objduration1 + objduration2 ; objduration.dhms("+0.3")
        12.25
        '56d28m'
        '+68d06h28m00.000s'
        """
        if isinstance(duration, Duration) == False:
            duration =  Duration(duration)
        res = Duration()
        if self._computed_day == 0:
            self.day()
        if duration._computed_day == 0:
            duration.day()     
        if (self._computed_day == 1) and (duration._computed_day == 1):
            day = self._day + duration._day
            res = Duration(day)
        return res

    def __radd__(self, duration):
        """ Right addition a duration to a duration.
        """
        return self + duration    

    def __iadd__(self, duration):
        """ Add a duration to a duration.
        """
        return self + duration
    
    def __sub__(self, duration):
        """ Subtract a duration to a duration.

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The result of the addition
        :rtype: Duration()
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.25) ; objduration2.duration("56d28m") ; objduration = objduration1 - objduration2 ; objduration.dhms("+0.3")
        12.25
        '56d28m'
        '-43d18h28m00.000s'
        """
        if isinstance(duration, Duration) == False:
            duration =  Duration(duration)
        res = Duration()
        if self._computed_day == 0:
            self.day()
        if duration._computed_day == 0:
            duration.day()  
        if (self._computed_day == 1) and (duration._computed_day == 1):
            day = self._day - duration._day
            res = Duration(day)
        return res

    def __rsub__(self, duration):
        """ Right subtraction a duration to a duration.
        """
        if isinstance(duration, Duration) == True:
            return self - duration    
        else:
            return duration    
            
    def __isub__(self, duration):
        """ Subtract a duration to a duration.

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The result of the addition
        :rtype: Duration()
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.25) ; objduration2.duration("56d28m") ; objduration1 -= objduration2 ; objduration1.dhms("+0.3")
        12.25
        '56d28m'
        '-43d18h28m00.000s'
        """
        return self - duration

    def __eq__(self, duration):
        """ Comparaison of durations. Return True if durations are defined and equals.

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 == objduration2
        12.345
        '56d28m'
        False
        """
        return self._duration_compare(duration, "==")

    def __ne__(self, duration):
        """ Comparaison of durations. Return True if durations are defined and not equals.

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 != objduration2
        12.345
        '56d28m'
        True
        """
        return self._duration_compare(duration, "!=")

    def __gt__(self, duration):
        """ Comparaison of durations. Return True if self > duration

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 > objduration2
        12.345
        '56d28m'
        False
        """
        return self._duration_compare(duration, ">")

    def __ge__(self, duration):
        """ Comparaison of durations. Return True if self >= duration

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 >= objduration2
        12.345
        '56d28m'
        False
        """
        return self._duration_compare(duration, ">=")

    def __lt__(self, duration):
        """ Comparaison of durations. Return True if self < duration

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 < objduration2
        12.345
        '56d28m'
        True
        """
        return self._duration_compare(duration, "<")

    def __le__(self, duration):
        """ Comparaison of durations. Return True if self <= duration

        :param duration: A duration in any supported format (cf. help(Duration))
        :type duration: Duration()
        :returns: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objduration1 = Duration()
        >>> objduration2 = Duration()
        >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 <= objduration2
        12.345
        '56d28m'
        True
        """
        return self._duration_compare(duration, "<=")

    def __mul__(self, multiplier):
        """ multiplication of duration by a float or int. Return a duration

        :param multiplier: A real number
        :type multiplier: float
        :returns: A duration
        :rtype: Duration()
        
        :Example:

        >>> objduration = Duration()
        >>> objduration.duration(30.43) ; (objduration*2).day()
        30.43
        60.86
        
        """
        if isinstance(multiplier, (int, float)) == False:
            raise TypeError
            return ""
        if self._computed_day == 0:
            self.day()
        durationmult = Duration()
        if (self._computed_day == 1):
            day = self._day * multiplier
            durationmult.duration(day)
        return durationmult

    def __rmul__(self, multiplier):
        """ Right multiplication of a duration by a float or int.
        """
        return self * multiplier

    def __truediv__(self, divisor):
        """ division of an angle by a float or int. Return a duration

        :param divisor: A real number
        :type divisor: float
        :returns: A duration
        :rtype: Duration()
        
        :Example:

        >>> objduration = Duration()
        >>> objduration.duration(30.43) ; (objduration/2).day()
        30.43
        15.215
        
        """
        if isinstance(divisor, (int, float)) == False:
            raise TypeError
            return ""
        if (divisor==0):
            raise ZeroDivisionError
        if self._computed_day == 0:
            self.day()
        durationdiv = Duration()
        if (self._computed_day == 1):
            day = self._day / divisor
            durationdiv.duration(day)
        return durationdiv