# -*- coding: utf-8 -*-
'''
Provides the :class:`Arrow <arrow.arrow.Arrow>` class, an enhanced ``datetime``
replacement.

'''

from __future__ import absolute_import

from datetime import datetime, timedelta, tzinfo
from dateutil import tz as dateutil_tz
from dateutil.relativedelta import relativedelta
import calendar
import sys
import warnings


from arrow import util, locales, parser, formatter


class Arrow(object):
    '''An :class:`Arrow <arrow.arrow.Arrow>` object.

    Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing
    additional functionality.

    :param year: the calendar year.
    :param month: the calendar month.
    :param day: the calendar day.
    :param hour: (optional) the hour. Defaults to 0.
    :param minute: (optional) the minute, Defaults to 0.
    :param second: (optional) the second, Defaults to 0.
    :param microsecond: (optional) the microsecond. Defaults 0.
    :param tzinfo: (optional) the ``tzinfo`` object.  Defaults to ``None``.

    If tzinfo is None, it is assumed to be UTC on creation.

    Usage::

        >>> import arrow
        >>> arrow.Arrow(2013, 5, 5, 12, 30, 45)
        <Arrow [2013-05-05T12:30:45+00:00]>

    '''

    resolution = datetime.resolution

    _ATTRS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond']
    _ATTRS_PLURAL = ['{0}s'.format(a) for a in _ATTRS]
    _MONTHS_PER_QUARTER = 3

    def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0,
                 tzinfo=None):

        if util.isstr(tzinfo):
            tzinfo = parser.TzinfoParser.parse(tzinfo)
        tzinfo = tzinfo or dateutil_tz.tzutc()

        self._datetime = datetime(year, month, day, hour, minute, second,
            microsecond, tzinfo)


    # factories: single object, both original and from datetime.

    @classmethod
    def now(cls, tzinfo=None):
        '''Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now".

        :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time.

        '''

        utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())
        dt = utc.astimezone(dateutil_tz.tzlocal() if tzinfo is None else tzinfo)

        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, dt.tzinfo)

    @classmethod
    def utcnow(cls):
        ''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now" in UTC
        time.

        '''

        dt = datetime.utcnow()

        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, dateutil_tz.tzutc())

    @classmethod
    def fromtimestamp(cls, timestamp, tzinfo=None):
        ''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp.

        :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.
        :param tzinfo: (optional) a ``tzinfo`` object.  Defaults to local time.

        '''

        tzinfo = tzinfo or dateutil_tz.tzlocal()
        timestamp = cls._get_timestamp_from_input(timestamp)
        dt = datetime.fromtimestamp(timestamp, tzinfo)

        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, tzinfo)

    @classmethod
    def utcfromtimestamp(cls, timestamp):
        '''Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp, in UTC time.

        :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.

        '''

        timestamp = cls._get_timestamp_from_input(timestamp)
        dt = datetime.utcfromtimestamp(timestamp)

        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, dateutil_tz.tzutc())

    @classmethod
    def fromdatetime(cls, dt, tzinfo=None):
        ''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``datetime`` and optional
        ``tzinfo`` object.

        :param dt: the ``datetime``
        :param tzinfo: (optional) a ``tzinfo`` object.  Defaults to UTC.

        '''

        tzinfo = tzinfo or dt.tzinfo or dateutil_tz.tzutc()

        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, tzinfo)

    @classmethod
    def fromdate(cls, date, tzinfo=None):
        ''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``date`` and optional
        ``tzinfo`` object.  Time values are set to 0.

        :param date: the ``date``
        :param tzinfo: (optional) a ``tzinfo`` object.  Defaults to UTC.
        '''

        tzinfo = tzinfo or dateutil_tz.tzutc()

        return cls(date.year, date.month, date.day, tzinfo=tzinfo)

    @classmethod
    def strptime(cls, date_str, fmt, tzinfo=None):
        ''' Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a date string and format,
        in the style of ``datetime.strptime``.

        :param date_str: the date string.
        :param fmt: the format string.
        :param tzinfo: (optional) an optional ``tzinfo``
        '''

        dt = datetime.strptime(date_str, fmt)
        tzinfo = tzinfo or dt.tzinfo

        return cls(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, tzinfo)


    # factories: ranges and spans

    @classmethod
    def range(cls, frame, start, end=None, tz=None, limit=None):
        ''' Returns an array of :class:`Arrow <arrow.arrow.Arrow>` objects, representing
        an iteration of time between two inputs.

        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
        :param start: A datetime expression, the start of the range.
        :param end: (optional) A datetime expression, the end of the range.
        :param tz: (optional) A timezone expression.  Defaults to UTC.
        :param limit: (optional) A maximum number of tuples to return.

        **NOTE**: the **end** or **limit** must be provided.  Call with **end** alone to
        return the entire range, with **limit** alone to return a maximum # of results from the
        start, and with both to cap a range at a maximum # of results.

        Supported frame values: year, quarter, month, week, day, hour, minute, second

        Recognized datetime expressions:

            - An :class:`Arrow <arrow.arrow.Arrow>` object.
            - A ``datetime`` object.

        Recognized timezone expressions:

            - A ``tzinfo`` object.
            - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
            - A ``str`` in ISO-8601 style, as in '+07:00'.
            - A ``str``, one of the following:  'local', 'utc', 'UTC'.

        Usage:

            >>> start = datetime(2013, 5, 5, 12, 30)
            >>> end = datetime(2013, 5, 5, 17, 15)
            >>> for r in arrow.Arrow.range('hour', start, end):
            ...     print repr(r)
            ...
            <Arrow [2013-05-05T12:30:00+00:00]>
            <Arrow [2013-05-05T13:30:00+00:00]>
            <Arrow [2013-05-05T14:30:00+00:00]>
            <Arrow [2013-05-05T15:30:00+00:00]>
            <Arrow [2013-05-05T16:30:00+00:00]>

        '''

        _, frame_relative, relative_steps = cls._get_frames(frame)

        tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)

        start = cls._get_datetime(start).replace(tzinfo=tzinfo)
        end, limit = cls._get_iteration_params(end, limit)
        end = cls._get_datetime(end).replace(tzinfo=tzinfo)

        current = cls.fromdatetime(start)
        results = []

        while current <= end and len(results) < limit:
            results.append(current)

            values = [getattr(current, f) for f in cls._ATTRS]
            current = cls(*values, tzinfo=tzinfo) + relativedelta(**{frame_relative: relative_steps})

        return results


    @classmethod
    def span_range(cls, frame, start, end, tz=None, limit=None):
        ''' Returns an array of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects,
        representing a series of timespans between two inputs.

        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
        :param start: A datetime expression, the start of the range.
        :param end: (optional) A datetime expression, the end of the range.
        :param tz: (optional) A timezone expression.  Defaults to UTC.
        :param limit: (optional) A maximum number of tuples to return.

        **NOTE**: the **end** or **limit** must be provided.  Call with **end** alone to
        return the entire range, with **limit** alone to return a maximum # of results from the
        start, and with both to cap a range at a maximum # of results.

        Supported frame values: year, quarter, month, week, day, hour, minute, second

        Recognized datetime expressions:

            - An :class:`Arrow <arrow.arrow.Arrow>` object.
            - A ``datetime`` object.

        Recognized timezone expressions:

            - A ``tzinfo`` object.
            - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
            - A ``str`` in ISO-8601 style, as in '+07:00'.
            - A ``str``, one of the following:  'local', 'utc', 'UTC'.

        Usage:

            >>> start = datetime(2013, 5, 5, 12, 30)
            >>> end = datetime(2013, 5, 5, 17, 15)
            >>> for r in arrow.Arrow.span_range('hour', start, end):
            ...     print r
            ...
            (<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T12:59:59.999999+00:00]>)
            (<Arrow [2013-05-05T13:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>)
            (<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T14:59:59.999999+00:00]>)
            (<Arrow [2013-05-05T15:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>)
            (<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T16:59:59.999999+00:00]>)

        '''
        tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)
        start = cls.fromdatetime(start, tzinfo).span(frame)[0]
        _range = cls.range(frame, start, end, tz, limit)
        return [r.span(frame) for r in _range]


    # representations

    def __repr__(self):

        dt = self._datetime
        attrs = ', '.join([str(i) for i in [dt.year, dt.month, dt.day, dt.hour, dt.minute,
            dt.second, dt.microsecond]])

        return '<{0} [{1}]>'.format(self.__class__.__name__, self.__str__())

    def __str__(self):
        return self._datetime.isoformat()

    def __format__(self, formatstr):

        if len(formatstr) > 0:
            return self.format(formatstr)

        return str(self)

    def __hash__(self):
        return self._datetime.__hash__()


    # attributes & properties

    def __getattr__(self, name):

        if name == 'week':
            return self.isocalendar()[1]

        if name == 'quarter':
            return int((self.month-1)/self._MONTHS_PER_QUARTER) + 1

        if not name.startswith('_'):
            value = getattr(self._datetime, name, None)

            if value is not None:
                return value

        return object.__getattribute__(self, name)

    @property
    def tzinfo(self):
        ''' Gets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object. '''

        return self._datetime.tzinfo

    @tzinfo.setter
    def tzinfo(self, tzinfo):
        ''' Sets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object. '''

        self._datetime = self._datetime.replace(tzinfo=tzinfo)

    @property
    def datetime(self):
        ''' Returns a datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''

        return self._datetime

    @property
    def naive(self):
        ''' Returns a naive datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''

        return self._datetime.replace(tzinfo=None)

    @property
    def timestamp(self):
        ''' Returns a timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''

        return calendar.timegm(self._datetime.utctimetuple())

    @property
    def float_timestamp(self):
        ''' Returns a floating-point representation of the :class:`Arrow <arrow.arrow.Arrow>` object. '''

        return self.timestamp + float(self.microsecond) / 1000000


    # mutation and duplication.

    def clone(self):
        ''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, cloned from the current one.

        Usage:

            >>> arw = arrow.utcnow()
            >>> cloned = arw.clone()

        '''

        return self.fromdatetime(self._datetime)

    def replace(self, **kwargs):
        ''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated
        according to inputs.

        Use single property names to set their value absolutely:

        >>> import arrow
        >>> arw = arrow.utcnow()
        >>> arw
        <Arrow [2013-05-11T22:27:34.787885+00:00]>
        >>> arw.replace(year=2014, month=6)
        <Arrow [2014-06-11T22:27:34.787885+00:00]>

        You can also provide a timezone expression can also be replaced:

        >>> arw.replace(tzinfo=tz.tzlocal())
        <Arrow [2013-05-11T22:27:34.787885-07:00]>

       Use plural property names to shift their current value relatively (**deprecated**):

       >>> arw.replace(years=1, months=-1)
       <Arrow [2014-04-11T22:27:34.787885+00:00]>

        Recognized timezone expressions:

            - A ``tzinfo`` object.
            - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
            - A ``str`` in ISO-8601 style, as in '+07:00'.
            - A ``str``, one of the following:  'local', 'utc', 'UTC'.

        '''

        absolute_kwargs = {}
        relative_kwargs = {}  # TODO: DEPRECATED; remove in next release

        for key, value in kwargs.items():

            if key in self._ATTRS:
                absolute_kwargs[key] = value
            elif key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']:
                # TODO: DEPRECATED
                warnings.warn("replace() with plural property to shift value"
                              "is deprecated, use shift() instead",
                              DeprecationWarning)
                relative_kwargs[key] = value
            elif key in ['week', 'quarter']:
                raise AttributeError('setting absolute {0} is not supported'.format(key))
            elif key !='tzinfo':
                raise AttributeError('unknown attribute: "{0}"'.format(key))

        # core datetime does not support quarters, translate to months.
        relative_kwargs.setdefault('months', 0)
        relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER

        current = self._datetime.replace(**absolute_kwargs)
        current += relativedelta(**relative_kwargs) # TODO: DEPRECATED

        tzinfo = kwargs.get('tzinfo')

        if tzinfo is not None:
            tzinfo = self._get_tzinfo(tzinfo)
            current = current.replace(tzinfo=tzinfo)

        return self.fromdatetime(current)

    def shift(self, **kwargs):
        ''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated
        according to inputs.

        Use plural property names to shift their current value relatively:

        >>> import arrow
        >>> arw = arrow.utcnow()
        >>> arw
        <Arrow [2013-05-11T22:27:34.787885+00:00]>
        >>> arw.shift(years=1, months=-1)
        <Arrow [2014-04-11T22:27:34.787885+00:00]>

        '''

        relative_kwargs = {}

        for key, value in kwargs.items():

            if key in self._ATTRS_PLURAL or key in ['weeks', 'quarters']:
                relative_kwargs[key] = value
            else:
                raise AttributeError()

        # core datetime does not support quarters, translate to months.
        relative_kwargs.setdefault('months', 0)
        relative_kwargs['months'] += relative_kwargs.pop('quarters', 0) * self._MONTHS_PER_QUARTER

        current = self._datetime + relativedelta(**relative_kwargs)

        return self.fromdatetime(current)

    def to(self, tz):
        ''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, converted
        to the target timezone.

        :param tz: an expression representing a timezone.

        Recognized timezone expressions:

            - A ``tzinfo`` object.
            - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
            - A ``str`` in ISO-8601 style, as in '+07:00'.
            - A ``str``, one of the following:  'local', 'utc', 'UTC'.

        Usage::

            >>> utc = arrow.utcnow()
            >>> utc
            <Arrow [2013-05-09T03:49:12.311072+00:00]>

            >>> utc.to('US/Pacific')
            <Arrow [2013-05-08T20:49:12.311072-07:00]>

            >>> utc.to(tz.tzlocal())
            <Arrow [2013-05-08T20:49:12.311072-07:00]>

            >>> utc.to('-07:00')
            <Arrow [2013-05-08T20:49:12.311072-07:00]>

            >>> utc.to('local')
            <Arrow [2013-05-08T20:49:12.311072-07:00]>

            >>> utc.to('local').to('utc')
            <Arrow [2013-05-09T03:49:12.311072+00:00]>

        '''

        if not isinstance(tz, tzinfo):
            tz = parser.TzinfoParser.parse(tz)

        dt = self._datetime.astimezone(tz)

        return self.__class__(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            dt.microsecond, dt.tzinfo)

    def span(self, frame, count=1):
        ''' Returns two new :class:`Arrow <arrow.arrow.Arrow>` objects, representing the timespan
        of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.

        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).
        :param count: (optional) the number of frames to span.

        Supported frame values: year, quarter, month, week, day, hour, minute, second

        Usage::

            >>> arrow.utcnow()
            <Arrow [2013-05-09T03:32:36.186203+00:00]>

            >>> arrow.utcnow().span('hour')
            (<Arrow [2013-05-09T03:00:00+00:00]>, <Arrow [2013-05-09T03:59:59.999999+00:00]>)

            >>> arrow.utcnow().span('day')
            (<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-09T23:59:59.999999+00:00]>)

            >>> arrow.utcnow().span('day', count=2)
            (<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-10T23:59:59.999999+00:00]>)

        '''

        frame_absolute, frame_relative, relative_steps = self._get_frames(frame)

        if frame_absolute == 'week':
            attr = 'day'
        elif frame_absolute == 'quarter':
            attr = 'month'
        else:
            attr = frame_absolute

        index = self._ATTRS.index(attr)
        frames = self._ATTRS[:index + 1]

        values = [getattr(self, f) for f in frames]

        for i in range(3 - len(values)):
            values.append(1)

        floor = self.__class__(*values, tzinfo=self.tzinfo)

        if frame_absolute == 'week':
            floor = floor + relativedelta(days=-(self.isoweekday() - 1))
        elif frame_absolute == 'quarter':
            floor = floor + relativedelta(months=-((self.month - 1) % 3))

        ceil = floor + relativedelta(
            **{frame_relative: count * relative_steps}) + relativedelta(microseconds=-1)

        return floor, ceil

    def floor(self, frame):
        ''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "floor"
        of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
        Equivalent to the first element in the 2-tuple returned by
        :func:`span <arrow.arrow.Arrow.span>`.

        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).

        Usage::

            >>> arrow.utcnow().floor('hour')
            <Arrow [2013-05-09T03:00:00+00:00]>
        '''

        return self.span(frame)[0]

    def ceil(self, frame):
        ''' Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "ceiling"
        of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
        Equivalent to the second element in the 2-tuple returned by
        :func:`span <arrow.arrow.Arrow.span>`.

        :param frame: the timeframe.  Can be any ``datetime`` property (day, hour, minute...).

        Usage::

            >>> arrow.utcnow().ceil('hour')
            <Arrow [2013-05-09T03:59:59.999999+00:00]>
        '''

        return self.span(frame)[1]


    # string output and formatting.

    def format(self, fmt='YYYY-MM-DD HH:mm:ssZZ', locale='en_us'):
        ''' Returns a string representation of the :class:`Arrow <arrow.arrow.Arrow>` object,
        formatted according to a format string.

        :param fmt: the format string.

        Usage::

            >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ')
            '2013-05-09 03:56:47 -00:00'

            >>> arrow.utcnow().format('X')
            '1368071882'

            >>> arrow.utcnow().format('MMMM DD, YYYY')
            'May 09, 2013'

            >>> arrow.utcnow().format()
            '2013-05-09 03:56:47 -00:00'

        '''

        return formatter.DateTimeFormatter(locale).format(self._datetime, fmt)


    def humanize(self, other=None, locale='en_us', only_distance=False):
        ''' Returns a localized, humanized representation of a relative difference in time.

        :param other: (optional) an :class:`Arrow <arrow.arrow.Arrow>` or ``datetime`` object.
            Defaults to now in the current :class:`Arrow <arrow.arrow.Arrow>` object's timezone.
        :param locale: (optional) a ``str`` specifying a locale.  Defaults to 'en_us'.
        :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part.

        Usage::

            >>> earlier = arrow.utcnow().replace(hours=-2)
            >>> earlier.humanize()
            '2 hours ago'

            >>> later = later = earlier.replace(hours=4)
            >>> later.humanize(earlier)
            'in 4 hours'

        '''

        locale = locales.get_locale(locale)

        if other is None:
            utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())
            dt = utc.astimezone(self._datetime.tzinfo)

        elif isinstance(other, Arrow):
            dt = other._datetime

        elif isinstance(other, datetime):
            if other.tzinfo is None:
                dt = other.replace(tzinfo=self._datetime.tzinfo)
            else:
                dt = other.astimezone(self._datetime.tzinfo)

        else:
            raise TypeError()

        delta = int(util.total_seconds(self._datetime - dt))
        sign = -1 if delta < 0 else 1
        diff = abs(delta)
        delta = diff

        if diff < 10:
            return locale.describe('now', only_distance=only_distance)

        if diff < 45:
            return locale.describe('seconds', sign, only_distance=only_distance)

        elif diff < 90:
            return locale.describe('minute', sign, only_distance=only_distance)
        elif diff < 2700:
            minutes = sign * int(max(delta / 60, 2))
            return locale.describe('minutes', minutes, only_distance=only_distance)

        elif diff < 5400:
            return locale.describe('hour', sign, only_distance=only_distance)
        elif diff < 79200:
            hours = sign * int(max(delta / 3600, 2))
            return locale.describe('hours', hours, only_distance=only_distance)

        elif diff < 129600:
            return locale.describe('day', sign, only_distance=only_distance)
        elif diff < 2160000:
            days = sign * int(max(delta / 86400, 2))
            return locale.describe('days', days, only_distance=only_distance)

        elif diff < 3888000:
            return locale.describe('month', sign, only_distance=only_distance)
        elif diff < 29808000:
            self_months = self._datetime.year * 12 + self._datetime.month
            other_months = dt.year * 12 + dt.month

            months = sign * int(max(abs(other_months - self_months), 2))

            return locale.describe('months', months, only_distance=only_distance)

        elif diff < 47260800:
            return locale.describe('year', sign, only_distance=only_distance)
        else:
            years = sign * int(max(delta / 31536000, 2))
            return locale.describe('years', years, only_distance=only_distance)


    # math

    def __add__(self, other):

        if isinstance(other, (timedelta, relativedelta)):
            return self.fromdatetime(self._datetime + other, self._datetime.tzinfo)

        raise TypeError()

    def __radd__(self, other):
        return self.__add__(other)

    def __sub__(self, other):

        if isinstance(other, (timedelta, relativedelta)):
            return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)

        elif isinstance(other, datetime):
            return self._datetime - other

        elif isinstance(other, Arrow):
            return self._datetime - other._datetime

        raise TypeError()

    def __rsub__(self, other):

        if isinstance(other, datetime):
            return other - self._datetime

        raise TypeError()


    # comparisons

    def _cmperror(self, other):
        raise TypeError('can\'t compare \'{0}\' to \'{1}\''.format(
            type(self), type(other)))

    def __eq__(self, other):

        if not isinstance(other, (Arrow, datetime)):
            return False

        return self._datetime == self._get_datetime(other)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __gt__(self, other):

        if not isinstance(other, (Arrow, datetime)):
            self._cmperror(other)

        return self._datetime > self._get_datetime(other)

    def __ge__(self, other):

        if not isinstance(other, (Arrow, datetime)):
            self._cmperror(other)

        return self._datetime >= self._get_datetime(other)

    def __lt__(self, other):

        if not isinstance(other, (Arrow, datetime)):
            self._cmperror(other)

        return self._datetime < self._get_datetime(other)

    def __le__(self, other):

        if not isinstance(other, (Arrow, datetime)):
            self._cmperror(other)

        return self._datetime <= self._get_datetime(other)


    # datetime methods

    def date(self):
        ''' Returns a ``date`` object with the same year, month and day. '''

        return self._datetime.date()

    def time(self):
        ''' Returns a ``time`` object with the same hour, minute, second, microsecond. '''

        return self._datetime.time()

    def timetz(self):
        ''' Returns a ``time`` object with the same hour, minute, second, microsecond and tzinfo. '''

        return self._datetime.timetz()

    def astimezone(self, tz):
        ''' Returns a ``datetime`` object, adjusted to the specified tzinfo.

        :param tz: a ``tzinfo`` object.

        '''

        return self._datetime.astimezone(tz)

    def utcoffset(self):
        ''' Returns a ``timedelta`` object representing the whole number of minutes difference from UTC time. '''

        return self._datetime.utcoffset()

    def dst(self):
        ''' Returns the daylight savings time adjustment. '''
        return self._datetime.dst()

    def timetuple(self):
        ''' Returns a ``time.struct_time``, in the current timezone. '''

        return self._datetime.timetuple()

    def utctimetuple(self):
        ''' Returns a ``time.struct_time``, in UTC time. '''

        return self._datetime.utctimetuple()

    def toordinal(self):
        ''' Returns the proleptic Gregorian ordinal of the date. '''

        return self._datetime.toordinal()

    def weekday(self):
        ''' Returns the day of the week as an integer (0-6). '''

        return self._datetime.weekday()

    def isoweekday(self):
        ''' Returns the ISO day of the week as an integer (1-7). '''

        return self._datetime.isoweekday()

    def isocalendar(self):
        ''' Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). '''

        return self._datetime.isocalendar()

    def isoformat(self, sep='T'):
        '''Returns an ISO 8601 formatted representation of the date and time. '''

        return self._datetime.isoformat(sep)

    def ctime(self):
        ''' Returns a ctime formatted representation of the date and time. '''

        return self._datetime.ctime()

    def strftime(self, format):
        ''' Formats in the style of ``datetime.strptime``.

        :param format: the format string.

        '''

        return self._datetime.strftime(format)

    def for_json(self):
        '''Serializes for the ``for_json`` protocol of simplejson.'''
        return self.isoformat()

    # internal tools.

    @staticmethod
    def _get_tzinfo(tz_expr):

        if tz_expr is None:
            return dateutil_tz.tzutc()
        if isinstance(tz_expr, tzinfo):
            return tz_expr
        else:
            try:
                return parser.TzinfoParser.parse(tz_expr)
            except parser.ParserError:
                raise ValueError('\'{0}\' not recognized as a timezone'.format(
                    tz_expr))

    @classmethod
    def _get_datetime(cls, expr):

        if isinstance(expr, Arrow):
            return expr.datetime

        if isinstance(expr, datetime):
            return expr

        try:
            expr = float(expr)
            return cls.utcfromtimestamp(expr).datetime
        except:
            raise ValueError(
                '\'{0}\' not recognized as a timestamp or datetime'.format(expr))

    @classmethod
    def _get_frames(cls, name):

        if name in cls._ATTRS:
            return name, '{0}s'.format(name), 1

        elif name in ['week', 'weeks']:
            return 'week', 'weeks', 1
        elif name in ['quarter', 'quarters']:
            return 'quarter', 'months', 3

        raise AttributeError()

    @classmethod
    def _get_iteration_params(cls, end, limit):

        if end is None:

            if limit is None:
                raise Exception('one of \'end\' or \'limit\' is required')

            return cls.max, limit

        else:
            if limit is None:
                return end, sys.maxsize
            return end, limit

    @staticmethod
    def _get_timestamp_from_input(timestamp):

        try:
            return float(timestamp)
        except:
            raise ValueError('cannot parse \'{0}\' as a timestamp'.format(timestamp))

Arrow.min = Arrow.fromdatetime(datetime.min)
Arrow.max = Arrow.fromdatetime(datetime.max)
