# -*- coding: utf-8 -*-

# Copyright (c) 2016, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license.  See the LICENSE file for details.

import datetime
import dateutil.relativedelta

from aniso8601.date import parse_date
from aniso8601.time import parse_time
from aniso8601 import compat

def parse_duration(isodurationstr, relative=False):
    #Given a string representing an ISO 8601 duration, return a
    #datetime.timedelta (or dateutil.relativedelta.relativedelta
    #if relative=True) that matches the given duration. Valid formats are:
    #
    #PnYnMnDTnHnMnS (or any reduced precision equivalent)
    #P<date>T<time>

    if isodurationstr[0] != 'P':
        raise ValueError('ISO 8601 duration must start with a P.')

    #If Y, M, D, H, S, or W are in the string, assume it is a specified duration
    if _has_any_component(isodurationstr, ['Y', 'M', 'D', 'H', 'S', 'W']) is True:
        return _parse_duration_prescribed(isodurationstr, relative)
    else:
        return _parse_duration_combined(isodurationstr, relative)

def _parse_duration_prescribed(durationstr, relative):
    #durationstr can be of the form PnYnMnDTnHnMnS or PnW

    #Make sure the end character is valid
    #https://bitbucket.org/nielsenb/aniso8601/issues/9/durations-with-trailing-garbage-are-parsed
    if durationstr[-1] not in ['Y', 'M', 'D', 'H', 'S', 'W']:
        raise ValueError('ISO 8601 duration must end with a valid character.')

    #Make sure only the lowest order element has decimal precision
    if durationstr.count('.') > 1:
        raise ValueError('ISO 8601 allows only lowest order element to have a decimal fraction.')
    elif durationstr.count('.') == 1:
        #There should only ever be 1 letter after a decimal if there is more
        #then one, the string is invalid
        lettercount = 0;

        for character in durationstr.split('.')[1]:
            if character.isalpha() is True:
                lettercount += 1

            if lettercount > 1:
                raise ValueError('ISO 8601 duration must end with a single valid character.')

    #Do not allow W in combination with other designators
    #https://bitbucket.org/nielsenb/aniso8601/issues/2/week-designators-should-not-be-combinable
    if durationstr.find('W') != -1 and _has_any_component(durationstr, ['Y', 'M', 'D', 'H', 'S']) is True:
        raise ValueError('ISO 8601 week designators may not be combined with other time designators.')

    #Parse the elements of the duration
    if durationstr.find('T') == -1:
        #Make sure no time portion is included
        #https://bitbucket.org/nielsenb/aniso8601/issues/7/durations-with-time-components-before-t
        if _has_any_component(durationstr, ['H', 'S']):
            raise ValueError('ISO 8601 time components not allowed in duration without prescribed time.')

        if _component_order_correct(durationstr, ['P', 'Y', 'M', 'D', 'W']) is False:
            raise ValueError('ISO 8601 duration components must be in the correct order.')

        if durationstr.find('Y') != -1:
            years = _parse_duration_element(durationstr, 'Y')
        else:
            years = 0

        if durationstr.find('M') != -1:
            months = _parse_duration_element(durationstr, 'M')
        else:
            months = 0

        if durationstr.find('W') != -1:
            weeks = _parse_duration_element(durationstr, 'W')
        else:
            weeks = 0

        if durationstr.find('D') != -1:
            days = _parse_duration_element(durationstr, 'D')
        else:
            days = 0

        #No hours, minutes or seconds
        hours = 0
        minutes = 0
        seconds = 0
    else:
        firsthalf = durationstr[:durationstr.find('T')]
        secondhalf = durationstr[durationstr.find('T'):]

        #Make sure no time portion is included in the date half
        #https://bitbucket.org/nielsenb/aniso8601/issues/7/durations-with-time-components-before-t
        if _has_any_component(firsthalf, ['H', 'S']):
            raise ValueError('ISO 8601 time components not allowed in date portion of duration.')

        if _component_order_correct(firsthalf, ['P', 'Y', 'M', 'D', 'W']) is False:
            raise ValueError('ISO 8601 duration components must be in the correct order.')

        #Make sure no date component is included in the time half
        if _has_any_component(secondhalf, ['Y', 'D']):
            raise ValueError('ISO 8601 time components not allowed in date portion of duration.')

        if _component_order_correct(secondhalf, ['T', 'H', 'M', 'S']) is False:
            raise ValueError('ISO 8601 time components in duration must be in the correct order.')

        if firsthalf.find('Y') != -1:
            years = _parse_duration_element(firsthalf, 'Y')
        else:
            years = 0

        if firsthalf.find('M') != -1:
            months = _parse_duration_element(firsthalf, 'M')
        else:
            months = 0

        if firsthalf.find('D') != -1:
            days = _parse_duration_element(firsthalf, 'D')
        else:
            days = 0

        if secondhalf.find('H') != -1:
            hours = _parse_duration_element(secondhalf, 'H')
        else:
            hours = 0

        if secondhalf.find('M') != -1:
            minutes = _parse_duration_element(secondhalf, 'M')
        else:
            minutes = 0

        if secondhalf.find('S') != -1:
            seconds = _parse_duration_element(secondhalf, 'S')
        else:
            seconds = 0

        #Weeks can't be included
        weeks = 0

    if relative == True:
        if int(years) != years or int(months) != months:
            #https://github.com/dateutil/dateutil/issues/40
            raise ValueError('Fractional months and years are not defined for relative intervals.')

        return dateutil.relativedelta.relativedelta(years=int(years), months=int(months), weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)
    else:
        #Note that weeks can be handled without conversion to days
        totaldays = years * 365 + months * 30 + days

        return datetime.timedelta(weeks=weeks, days=totaldays, hours=hours, minutes=minutes, seconds=seconds)

def _parse_duration_combined(durationstr, relative):
    #Period of the form P<date>T<time>

    #Split the string in to its component parts
    datepart, timepart = durationstr[1:].split('T') #We skip the 'P'

    datevalue = parse_date(datepart)
    timevalue = parse_time(timepart)

    if relative is True:
        return dateutil.relativedelta.relativedelta(years=datevalue.year, months=datevalue.month, days=datevalue.day, hours=timevalue.hour, minutes=timevalue.minute, seconds=timevalue.second, microseconds=timevalue.microsecond)
    else:
        totaldays = datevalue.year * 365 + datevalue.month * 30 + datevalue.day

        return datetime.timedelta(days=totaldays, hours=timevalue.hour, minutes=timevalue.minute, seconds=timevalue.second, microseconds=timevalue.microsecond)

def _parse_duration_element(durationstr, elementstr):
    #Extracts the specified portion of a duration, for instance, given:
    #durationstr = 'T4H5M6.1234S'
    #elementstr = 'H'
    #
    #returns 4
    #
    #Note that the string must start with a character, so its assumed the
    #full duration string would be split at the 'T'

    durationstartindex = 0
    durationendindex = durationstr.find(elementstr)

    for characterindex in compat.range(durationendindex - 1, 0, -1):
        if durationstr[characterindex].isalpha() is True:
            durationstartindex = characterindex
            break

    durationstartindex += 1

    if ',' in durationstr:
        #Replace the comma with a 'full-stop'
        durationstr = durationstr.replace(',', '.')

    return float(durationstr[durationstartindex:durationendindex])

def _has_any_component(durationstr, components):
    #Given a duration string, and a list of components, returns True
    #if any of the listed components are present, False otherwise.
    #
    #For instance:
    #durationstr = 'P1Y'
    #components = ['Y', 'M']
    #
    #returns True
    #
    #durationstr = 'P1Y'
    #components = ['M', 'D']
    #
    #returns False

    for component in components:
        if durationstr.find(component) != -1:
            return True

    return False

def _component_order_correct(durationstr, componentorder):
    #Given a duration string, and a list of components, returns
    #True if the components are in the same order as the
    #component order list, False otherwise. Characters that
    #are present in the component order list but not in the
    #duration string are ignored.
    #
    #https://bitbucket.org/nielsenb/aniso8601/issues/8/durations-with-components-in-wrong-order
    #
    #durationstr = 'P1Y1M1D'
    #components = ['P', 'Y', 'M', 'D']
    #
    #returns True
    #
    #durationstr = 'P1Y1M'
    #components = ['P', 'Y', 'M', 'D']
    #
    #returns True
    #
    #durationstr = 'P1D1Y1M'
    #components = ['P', 'Y', 'M', 'D']
    #
    #returns False

    componentindex = 0

    for characterindex in compat.range(len(durationstr)):
        character = durationstr[characterindex]

        if character in componentorder:
            #This is a character we need to check the order of
            if character in componentorder[componentindex:]:
                componentindex = componentorder.index(character)
            else:
                #A character is out of order
                return False

    return True
