# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose with or without fee is hereby granted,
# provided that the above copyright notice and this permission notice
# appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import io
import struct

import dns.exception
import dns.rdata
import dns.util

_pows = (1, 10, 100, 1000, 10000, 100000, 1000000, 10000000,
         100000000, 1000000000, 10000000000)

def _exponent_of(what, desc):
    exp = None
    for i in range(len(_pows)):
        if what // _pows[i] == 0:
            exp = i - 1
            break
    if exp is None or exp < 0:
        raise dns.exception.SyntaxError("%s value out of bounds" % desc)
    return exp

def _float_to_tuple(what):
    if what < 0:
        sign = -1
        what *= -1
    else:
        sign = 1
    what = int(round(what * 3600000))
    degrees = what // 3600000
    what -= degrees * 3600000
    minutes = what // 60000
    what -= minutes * 60000
    seconds = what // 1000
    what -= seconds * 1000
    return (degrees * sign, minutes, seconds, what)

def _tuple_to_float(what):
    if what[0] < 0:
        sign = -1
        value = float(what[0]) * -1
    else:
        sign = 1
        value = float(what[0])
    value += float(what[1]) / 60.0
    value += float(what[2]) / 3600.0
    value += float(what[3]) / 3600000.0
    return sign * value

def _encode_size(what, desc):
    what = int(what);
    exponent = _exponent_of(what, desc) & 0xF
    base = what // pow(10, exponent) & 0xF
    return base * 16 + exponent

def _decode_size(what, desc):
    exponent = what & 0x0F
    if exponent > 9:
        raise dns.exception.SyntaxError("bad %s exponent" % desc)
    base = (what & 0xF0) >> 4
    if base > 9:
        raise dns.exception.SyntaxError("bad %s base" % desc)
    return int(base) * pow(10, exponent)

class LOC(dns.rdata.Rdata):
    """LOC record

    @ivar latitude: latitude
    @type latitude: (int, int, int, int) tuple specifying the degrees, minutes,
    seconds, and milliseconds of the coordinate.
    @ivar longitude: longitude
    @type longitude: (int, int, int, int) tuple specifying the degrees,
    minutes, seconds, and milliseconds of the coordinate.
    @ivar altitude: altitude
    @type altitude: float
    @ivar size: size of the sphere
    @type size: float
    @ivar horizontal_precision: horizontal precision
    @type horizontal_precision: float
    @ivar vertical_precision: vertical precision
    @type vertical_precision: float
    @see: RFC 1876"""

    __slots__ = ['latitude', 'longitude', 'altitude', 'size',
                 'horizontal_precision', 'vertical_precision']

    def __init__(self, rdclass, rdtype, latitude, longitude, altitude,
                 size=1.0, hprec=10000.0, vprec=10.0):
        """Initialize a LOC record instance.

        The parameters I{latitude} and I{longitude} may be either a 4-tuple
        of integers specifying (degrees, minutes, seconds, milliseconds),
        or they may be floating point values specifying the number of
        degrees.  The other parameters are floats."""

        super(LOC, self).__init__(rdclass, rdtype)
        if isinstance(latitude, int):
            latitude = float(latitude)
        if isinstance(latitude, float):
            latitude = _float_to_tuple(latitude)
        self.latitude = latitude
        if isinstance(longitude, int):
            longitude = float(longitude)
        if isinstance(longitude, float):
            longitude = _float_to_tuple(longitude)
        self.longitude = longitude
        self.altitude = float(altitude)
        self.size = float(size)
        self.horizontal_precision = float(hprec)
        self.vertical_precision = float(vprec)

    def to_text(self, origin=None, relativize=True, **kw):
        if self.latitude[0] > 0:
            lat_hemisphere = 'N'
            lat_degrees = self.latitude[0]
        else:
            lat_hemisphere = 'S'
            lat_degrees = -1 * self.latitude[0]
        if self.longitude[0] > 0:
            long_hemisphere = 'E'
            long_degrees = self.longitude[0]
        else:
            long_hemisphere = 'W'
            long_degrees = -1 * self.longitude[0]
        text = "%d %d %d.%03d %s %d %d %d.%03d %s %0.2fm" % (
            lat_degrees, self.latitude[1], self.latitude[2], self.latitude[3],
            lat_hemisphere, long_degrees, self.longitude[1], self.longitude[2],
            self.longitude[3], long_hemisphere, self.altitude / 100.0
            )

        if self.size != 1.0 or self.horizontal_precision != 10000.0 or \
           self.vertical_precision != 10.0:
            text += " %0.2fm %0.2fm %0.2fm" % (
                self.size / 100.0, self.horizontal_precision / 100.0,
                self.vertical_precision / 100.0
            )
        return text

    def from_text(cls, rdclass, rdtype, tok, origin = None, relativize = True):
        latitude = [0, 0, 0, 0]
        longitude = [0, 0, 0, 0]
        size = 1.0
        hprec = 10000.0
        vprec = 10.0

        latitude[0] = tok.get_int()
        t = tok.get_string()
        if t.isdigit():
            latitude[1] = int(t)
            t = tok.get_string()
            if '.' in t:
                (seconds, milliseconds) = t.split('.')
                if not seconds.isdigit():
                    raise dns.exception.SyntaxError('bad latitude seconds value')
                latitude[2] = int(seconds)
                if latitude[2] >= 60:
                    raise dns.exception.SyntaxError('latitude seconds >= 60')
                l = len(milliseconds)
                if l == 0 or l > 3 or not milliseconds.isdigit():
                    raise dns.exception.SyntaxError('bad latitude milliseconds value')
                if l == 1:
                    m = 100
                elif l == 2:
                    m = 10
                else:
                    m = 1
                latitude[3] = m * int(milliseconds)
                t = tok.get_string()
            elif t.isdigit():
                latitude[2] = int(t)
                t = tok.get_string()
        if t == 'S':
            latitude[0] *= -1
        elif t != 'N':
            raise dns.exception.SyntaxError('bad latitude hemisphere value')

        longitude[0] = tok.get_int()
        t = tok.get_string()
        if t.isdigit():
            longitude[1] = int(t)
            t = tok.get_string()
            if '.' in t:
                (seconds, milliseconds) = t.split('.')
                if not seconds.isdigit():
                    raise dns.exception.SyntaxError('bad longitude seconds value')
                longitude[2] = int(seconds)
                if longitude[2] >= 60:
                    raise dns.exception.SyntaxError('longitude seconds >= 60')
                l = len(milliseconds)
                if l == 0 or l > 3 or not milliseconds.isdigit():
                    raise dns.exception.SyntaxError('bad longitude milliseconds value')
                if l == 1:
                    m = 100
                elif l == 2:
                    m = 10
                else:
                    m = 1
                longitude[3] = m * int(milliseconds)
                t = tok.get_string()
            elif t.isdigit():
                longitude[2] = int(t)
                t = tok.get_string()
        if t == 'W':
            longitude[0] *= -1
        elif t != 'E':
            raise dns.exception.SyntaxError('bad longitude hemisphere value')

        t = tok.get_string()
        if t[-1] == 'm':
            t = t[0 : -1]
        altitude = float(t) * 100.0	# m -> cm

        token = tok.get().unescape()
        if not token.is_eol_or_eof():
            value = token.value
            if value[-1] == 'm':
                value = value[0 : -1]
            size = float(value) * 100.0	# m -> cm
            token = tok.get().unescape()
            if not token.is_eol_or_eof():
                value = token.value
                if value[-1] == 'm':
                    value = value[0 : -1]
                hprec = float(value) * 100.0	# m -> cm
                token = tok.get().unescape()
                if not token.is_eol_or_eof():
                    value = token.value
                    if value[-1] == 'm':
                        value = value[0 : -1]
                        vprec = float(value) * 100.0	# m -> cm
                        tok.get_eol()

        return cls(rdclass, rdtype, latitude, longitude, altitude,
                   size, hprec, vprec)

    from_text = classmethod(from_text)

    def to_wire(self, file, compress = None, origin = None):
        if self.latitude[0] < 0:
            sign = -1
            degrees = -1 * self.latitude[0]
        else:
            sign = 1
            degrees = self.latitude[0]
        milliseconds = (degrees * 3600000 +
                        self.latitude[1] * 60000 +
                        self.latitude[2] * 1000 +
                        self.latitude[3]) * sign
        latitude = 0x80000000 + milliseconds
        if self.longitude[0] < 0:
            sign = -1
            degrees = -1 * self.longitude[0]
        else:
            sign = 1
            degrees = self.longitude[0]
        milliseconds = (degrees * 3600000 +
                        self.longitude[1] * 60000 +
                        self.longitude[2] * 1000 +
                        self.longitude[3]) * sign
        longitude = 0x80000000 + milliseconds
        altitude = int(self.altitude) + 10000000
        size = _encode_size(self.size, "size")
        hprec = _encode_size(self.horizontal_precision, "horizontal precision")
        vprec = _encode_size(self.vertical_precision, "vertical precision")
        wire = struct.pack("!BBBBIII", 0, size, hprec, vprec, latitude,
                           longitude, altitude)
        file.write(wire)

    def from_wire(cls, rdclass, rdtype, wire, current, rdlen, origin = None):
        (version, size, hprec, vprec, latitude, longitude, altitude) = \
                  struct.unpack("!BBBBIII", wire[current : current + rdlen])
        if latitude > 0x80000000:
            latitude = float(latitude - 0x80000000) / 3600000
        else:
            latitude = -1 * float(0x80000000 - latitude) / 3600000
        if latitude < -90.0 or latitude > 90.0:
            raise dns.exception.FormError("bad latitude")
        if longitude > 0x80000000:
            longitude = float(longitude - 0x80000000) / 3600000
        else:
            longitude = -1 * float(0x80000000 - longitude) / 3600000
        if longitude < -180.0 or longitude > 180.0:
            raise dns.exception.FormError("bad longitude")
        altitude = float(altitude) - 10000000.0
        size = _decode_size(size, "size")
        hprec = _decode_size(hprec, "horizontal precision")
        vprec = _decode_size(vprec, "vertical precision")
        return cls(rdclass, rdtype, latitude, longitude, altitude,
                   size, hprec, vprec)

    from_wire = classmethod(from_wire)

    def _cmp(self, other):
        f = io.BytesIO()
        self.to_wire(f)
        wire1 = f.getvalue()
        f.seek(0)
        f.truncate()
        other.to_wire(f)
        wire2 = f.getvalue()
        f.close()

        return dns.util.cmp(wire1, wire2)

    def _get_float_latitude(self):
        return _tuple_to_float(self.latitude)

    def _set_float_latitude(self, value):
        self.latitude = _float_to_tuple(value)

    float_latitude = property(_get_float_latitude, _set_float_latitude,
                              doc="latitude as a floating point value")

    def _get_float_longitude(self):
        return _tuple_to_float(self.longitude)

    def _set_float_longitude(self, value):
        self.longitude = _float_to_tuple(value)

    float_longitude = property(_get_float_longitude, _set_float_longitude,
                               doc="longitude as a floating point value")
