# -*- coding: utf-8 -*-
from decimal import Decimal, setcontext, ExtendedContext

from geolucidate.links.google import google_maps_link
from geolucidate.links.tools import MapLink
from geolucidate.parser import parser_re

setcontext(ExtendedContext)


def _cleanup(parts):
    """
    Normalize up the parts matched by :obj:`parser.parser_re` to
    degrees, minutes, and seconds.

    >>> _cleanup({'latdir': 'south', 'longdir': 'west',
    ...          'latdeg':'60','latmin':'30',
    ...          'longdeg':'50','longmin':'40'})
    ['S', '60', '30', '00', 'W', '50', '40', '00']

    >>> _cleanup({'latdir': 'south', 'longdir': 'west',
    ...          'latdeg':'60','latmin':'30', 'latdecsec':'.50',
    ...          'longdeg':'50','longmin':'40','longdecsec':'.90'})
    ['S', '60', '30.50', '00', 'W', '50', '40.90', '00']

    """

    latdir = (parts['latdir'] or parts['latdir2']).upper()[0]
    longdir = (parts['longdir'] or parts['longdir2']).upper()[0]

    latdeg = parts.get('latdeg')
    longdeg = parts.get('longdeg')

    latmin = parts.get('latmin', '00') or '00'
    longmin = parts.get('longmin', '00') or '00'

    latdecsec = parts.get('latdecsec', '')
    longdecsec = parts.get('longdecsec', '')

    if (latdecsec and longdecsec):
        latmin += latdecsec
        longmin += longdecsec
        latsec = '00'
        longsec = '00'
    else:
        latsec = parts.get('latsec', '') or '00'
        longsec = parts.get('longsec', '') or '00'

    return [latdir, latdeg, latmin, latsec, longdir, longdeg, longmin, longsec]


def _convert(latdir, latdeg, latmin, latsec,
             longdir, longdeg, longmin, longsec):
    """
    Convert normalized degrees, minutes, and seconds to decimal degrees.
    Quantize the converted value based on the input precision and
    return a 2-tuple of strings.

    >>> _convert('S','50','30','30','W','50','30','30')
    ('-50.508333', '-50.508333')

    >>> _convert('N','50','27','55','W','127','27','65')
    ('50.459167', '-127.460833')

    """
    if (latsec != '00' or longsec != '00'):
        precision = Decimal('0.000001')
    elif (latmin != '00' or longmin != '00'):
        precision = Decimal('0.001')
    else:
        precision = Decimal('1')

    latitude = Decimal(latdeg)
    latmin = Decimal(latmin)
    latsec = Decimal(latsec)

    longitude = Decimal(longdeg)
    longmin = Decimal(longmin)
    longsec = Decimal(longsec)

    if latsec > 59 or longsec > 59:
        # Assume that 'seconds' greater than 59 are actually a decimal
        # fraction of minutes
        latitude += (latmin +
                     (latsec / Decimal('100'))) / Decimal('60')
        longitude += (longmin +
                      (longsec / Decimal('100'))) / Decimal('60')
    else:
        latitude += (latmin +
                     (latsec / Decimal('60'))) / Decimal('60')
        longitude += (longmin +
                      (longsec / Decimal('60'))) / Decimal('60')

    if latdir == 'S':
        latitude *= Decimal('-1')

    if longdir == 'W':
        longitude *= Decimal('-1')

    lat_str = str(latitude.quantize(precision))
    long_str = str(longitude.quantize(precision))

    return (lat_str, long_str)


def replace(string, sub_function=google_maps_link()):
    """
    Replace detected coordinates with a map link, using the given substitution
    function.

    The substitution function will be passed a :class:`~.MapLink` instance, and
    should return a string which will be substituted by :func:`re.sub` in place
    of the detected coordinates.

    >>> replace("58147N/07720W")
    '<a href="http://maps.google.com/maps?q=58.235278%2C-77.333333+%2858147N%2F07720W%29&ll=58.235278%2C-77.333333&t=h" title="58147N/07720W (58.235278, -77.333333)">58147N/07720W</a>'

    >>> replace("5814N/07720W", google_maps_link('satellite'))
    '<a href="http://maps.google.com/maps?q=58.233%2C-77.333+%285814N%2F07720W%29&ll=58.233%2C-77.333&t=k" title="5814N/07720W (58.233, -77.333)">5814N/07720W</a>'

    >>> from geolucidate.links.bing import bing_maps_link
    >>> replace("58N/077W", bing_maps_link('map'))
    '<a href="http://bing.com/maps/default.aspx?style=r&cp=58~-77&sp=Point.58_-77_58N%2F077W&v=2" title="58N/077W (58, -77)">58N/077W</a>'

    """

    def do_replace(match):
        original_string = match.group()
        (latitude, longitude) = _convert(*_cleanup(match.groupdict()))
        return sub_function(MapLink(original_string, latitude, longitude))

    return parser_re.sub(do_replace, string)


def get_replacements(string, sub_function=google_maps_link()):
    """
    Return a dict whose keys are instances of :class:`re.Match` and
    whose values are the corresponding replacements.  Use
    :func:`get_replacements` when the replacement cannot be performed
    through ordinary string substitution by :func:`re.sub`, as in
    :func:`replace`.


    >>> get_replacements("4630 NORTH 5705 WEST 58147N/07720W")
    ... #doctest: +ELLIPSIS
    {<re.Match object...>: '<a href="..." title="...">4630 NORTH 5705 WEST</a>', <re.Match object...>: '<a href="..." title="...">58147N/07720W</a>'}

    >>> test_string = "4630 NORTH 5705 WEST 58147N/07720W"
    >>> replacements = get_replacements(test_string)
    >>> offset = 0
    >>> out = bytearray(test_string, encoding="ascii", errors="replace")
    >>> for (match, link) in replacements.items():
    ...     start = match.start() + offset
    ...     end = match.end() + offset
    ...     out[start:end] = bytearray(link, encoding="ascii", errors="replace")
    ...     offset += (len(link) - len(match.group()))
    >>> out.decode(encoding="ascii") == replace(test_string)
    True
    """

    substitutions = {}
    matches = parser_re.finditer(string)

    for match in matches:
        (latitude, longitude) = _convert(*_cleanup(match.groupdict()))
        substitutions[match] = sub_function(MapLink(match.group(),
                                                    latitude, longitude))

    return substitutions

[((8, 0, 8, 27), 'decimal.setcontext', 'setcontext', ({(8, 11, 8, 26): 'ExtendedContext'}, {}), '(ExtendedContext)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((73, 15, 73, 30), 'decimal.Decimal', 'Decimal', ({(73, 23, 73, 29): 'latdeg'}, {}), '(latdeg)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((74, 13, 74, 28), 'decimal.Decimal', 'Decimal', ({(74, 21, 74, 27): 'latmin'}, {}), '(latmin)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((75, 13, 75, 28), 'decimal.Decimal', 'Decimal', ({(75, 21, 75, 27): 'latsec'}, {}), '(latsec)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((77, 16, 77, 32), 'decimal.Decimal', 'Decimal', ({(77, 24, 77, 31): 'longdeg'}, {}), '(longdeg)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((78, 14, 78, 30), 'decimal.Decimal', 'Decimal', ({(78, 22, 78, 29): 'longmin'}, {}), '(longmin)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((79, 14, 79, 30), 'decimal.Decimal', 'Decimal', ({(79, 22, 79, 29): 'longsec'}, {}), '(longsec)', False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((106, 33, 106, 51), 'geolucidate.links.google.google_maps_link', 'google_maps_link', ({}, {}), '()', False, 'from geolucidate.links.google import google_maps_link\n'), ((132, 11, 132, 44), 'geolucidate.parser.parser_re.sub', 'parser_re.sub', ({(132, 25, 132, 35): 'do_replace', (132, 37, 132, 43): 'string'}, {}), '(do_replace, string)', False, 'from geolucidate.parser import parser_re\n'), ((135, 42, 135, 60), 'geolucidate.links.google.google_maps_link', 'google_maps_link', ({}, {}), '()', False, 'from geolucidate.links.google import google_maps_link\n'), ((162, 14, 162, 40), 'geolucidate.parser.parser_re.finditer', 'parser_re.finditer', ({(162, 33, 162, 39): 'string'}, {}), '(string)', False, 'from geolucidate.parser import parser_re\n'), ((67, 20, 67, 39), 'decimal.Decimal', 'Decimal', ({(67, 28, 67, 38): '"""0.000001"""'}, {}), "('0.000001')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((95, 20, 95, 33), 'decimal.Decimal', 'Decimal', ({(95, 28, 95, 32): '"""-1"""'}, {}), "('-1')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((98, 21, 98, 34), 'decimal.Decimal', 'Decimal', ({(98, 29, 98, 33): '"""-1"""'}, {}), "('-1')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((69, 20, 69, 36), 'decimal.Decimal', 'Decimal', ({(69, 28, 69, 35): '"""0.001"""'}, {}), "('0.001')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((71, 20, 71, 32), 'decimal.Decimal', 'Decimal', ({(71, 28, 71, 31): '"""1"""'}, {}), "('1')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((85, 50, 85, 63), 'decimal.Decimal', 'Decimal', ({(85, 58, 85, 62): '"""60"""'}, {}), "('60')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((87, 52, 87, 65), 'decimal.Decimal', 'Decimal', ({(87, 60, 87, 64): '"""60"""'}, {}), "('60')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((90, 49, 90, 62), 'decimal.Decimal', 'Decimal', ({(90, 57, 90, 61): '"""60"""'}, {}), "('60')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((92, 51, 92, 64), 'decimal.Decimal', 'Decimal', ({(92, 59, 92, 63): '"""60"""'}, {}), "('60')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((130, 28, 130, 73), 'geolucidate.links.tools.MapLink', 'MapLink', ({(130, 36, 130, 51): 'original_string', (130, 53, 130, 61): 'latitude', (130, 63, 130, 72): 'longitude'}, {}), '(original_string, latitude, longitude)', False, 'from geolucidate.links.tools import MapLink\n'), ((85, 31, 85, 45), 'decimal.Decimal', 'Decimal', ({(85, 39, 85, 44): '"""100"""'}, {}), "('100')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((87, 33, 87, 47), 'decimal.Decimal', 'Decimal', ({(87, 41, 87, 46): '"""100"""'}, {}), "('100')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((90, 31, 90, 44), 'decimal.Decimal', 'Decimal', ({(90, 39, 90, 43): '"""60"""'}, {}), "('60')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n'), ((92, 33, 92, 46), 'decimal.Decimal', 'Decimal', ({(92, 41, 92, 45): '"""60"""'}, {}), "('60')", False, 'from decimal import Decimal, setcontext, ExtendedContext\n')]