package cgeo.geocaching.geopoint;

import cgeo.geocaching.R;
import cgeo.geocaching.geopoint.Geopoint.GeopointException;

import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parse coordinates.
 */
public class GeopointParser
{
    private static class ResultWrapper {
        final double result;
        final int matcherPos;
        final int matcherLength;

        public ResultWrapper(final double result, int matcherPos, int stringLength) {
            this.result = result;
            this.matcherPos = matcherPos;
            this.matcherLength = stringLength;
        }
    }

    //                                                         ( 1  )    ( 2  )         ( 3  )       ( 4  )       (        5        )
    private static final Pattern patternLat = Pattern.compile("\\b([NS])\\s*(\\d+)°?(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE);
    private static final Pattern patternLon = Pattern.compile("\\b([WE])\\s*(\\d+)°?(?:\\s*(\\d+)(?:[.,](\\d+)|'?\\s*(\\d+(?:[.,]\\d+)?)(?:''|\")?)?)?", Pattern.CASE_INSENSITIVE);

    private enum LatLon
    {
        LAT,
        LON
    }

    /**
     * Parses a pair of coordinates (latitude and longitude) out of a String.
     * Accepts following formats and combinations of it:
     * X DD
     * X DD°
     * X DD° MM
     * X DD° MM.MMM
     * X DD° MM SS
     * 
     * as well as:
     * DD.DDDDDDD
     * 
     * Both . and , are accepted, also variable count of spaces (also 0)
     * 
     * @param text
     *            the string to parse
     * @return an Geopoint with parsed latitude and longitude
     * @throws ParseException
     *             if lat or lon could not be parsed
     */
    public static Geopoint parse(final String text)
    {
        final ResultWrapper latitudeWrapper = parseHelper(text, LatLon.LAT);
        final double lat = latitudeWrapper.result;
        // cut away the latitude part when parsing the longitude
        final ResultWrapper longitudeWrapper = parseHelper(text.substring(latitudeWrapper.matcherPos + latitudeWrapper.matcherLength), LatLon.LON);

        if (longitudeWrapper.matcherPos - (latitudeWrapper.matcherPos + latitudeWrapper.matcherLength) >= 10) {
            throw new ParseException("Distance between latitude and longitude text is to large.", LatLon.LON);
        }

        final double lon = longitudeWrapper.result;
        return new Geopoint(lat, lon);
    }

    /**
     * Parses a pair of coordinates (latitude and longitude) out of a String.
     * Accepts following formats and combinations of it:
     * X DD
     * X DD°
     * X DD° MM
     * X DD° MM.MMM
     * X DD° MM SS
     * 
     * as well as:
     * DD.DDDDDDD
     * 
     * Both . and , are accepted, also variable count of spaces (also 0)
     * 
     * @param latitude
     *            the latitude string to parse
     * @param longitude
     *            the longitude string to parse
     * @return an Geopoint with parsed latitude and longitude
     * @throws ParseException
     *             if lat or lon could not be parsed
     */
    public static Geopoint parse(final String latitude, final String longitude)
    {
        final double lat = parseLatitude(latitude);
        final double lon = parseLongitude(longitude);

        return new Geopoint(lat, lon);
    }

    /*
     * (non JavaDoc)
     * Helper for coordinates-parsing.
     */
    private static ResultWrapper parseHelper(final String text, final LatLon latlon)
    {

        final Pattern pattern = LatLon.LAT == latlon ? patternLat : patternLon;
        final Matcher matcher = pattern.matcher(text);

        if (matcher.find()) {
            final double sign = matcher.group(1).equalsIgnoreCase("S") || matcher.group(1).equalsIgnoreCase("W") ? -1.0 : 1.0;
            final double degree = Integer.valueOf(matcher.group(2)).doubleValue();

            double minutes = 0.0;
            double seconds = 0.0;

            if (null != matcher.group(3)) {
                minutes = Integer.valueOf(matcher.group(3)).doubleValue();

                if (null != matcher.group(4)) {
                    seconds = Double.parseDouble("0." + matcher.group(4)) * 60.0;
                } else if (null != matcher.group(5)) {
                    seconds = Double.parseDouble(matcher.group(5));
                }
            }

            return new ResultWrapper(sign * (degree + minutes / 60.0 + seconds / 3600.0), matcher.start(), matcher.group().length());

        } else {

            // Nothing found with "N 52...", try to match string as decimaldegree
            try {
                final String[] items = StringUtils.split(text.trim());
                if (items.length > 0) {
                    final int index = (latlon == LatLon.LON ? items.length - 1 : 0);
                    final int pos = (latlon == LatLon.LON ? text.lastIndexOf(items[index]) : text.indexOf(items[index]));
                    return new ResultWrapper(Double.parseDouble(items[index]), pos, items[index].length());
                }
            } catch (NumberFormatException e) {
                // The right exception will be raised below.
            }
        }

        throw new ParseException("Could not parse coordinates as " + latlon + ": \"" + text + "\"", latlon);
    }

    /**
     * Parses latitude out of a given string.
     *
     * @see parse()
     * @param text
     *            the string to be parsed
     * @return the latitude as decimal degree
     * @throws ParseException
     *             if latitude could not be parsed
     */
    public static double parseLatitude(final String text)
    {
        return parseHelper(text, LatLon.LAT).result;
    }

    /**
     * Parses longitude out of a given string.
     *
     * @see parse()
     * @param text
     *            the string to be parsed
     * @return the longitude as decimal degree
     * @throws ParseException
     *             if longitude could not be parsed
     */
    public static double parseLongitude(final String text)
    {
        return parseHelper(text, LatLon.LON).result;
    }

    public static class ParseException
            extends GeopointException
    {
        private static final long serialVersionUID = 1L;
        public final int resource;

        public ParseException(final String msg, final LatLon faulty)
        {
            super(msg);
            resource = faulty == LatLon.LAT ? R.string.err_parse_lat : R.string.err_parse_lon;
        }
    }
}

@@ -1424,33 +1424,32 @@
 DD%C2%B0 MM SS%0A     *
- 
 %0A     * as well 
@@ -1468,33 +1468,32 @@
 D.DDDDDDD%0A     *
- 
 %0A     * Both . a
@@ -1548,33 +1548,32 @@
  (also 0)%0A     *
- 
 %0A     * @param t
@@ -2724,25 +2724,24 @@
 MM SS%0A     *
- 
 %0A     * as w
@@ -2768,25 +2768,24 @@
 DDDDD%0A     *
- 
 %0A     * Both
@@ -2852,17 +2852,16 @@
 )%0A     *
- 
 %0A     * 
@@ -4370,16 +4370,34 @@
 group(5)
+.replace(%22,%22, %22.%22)
 );%0A     

Fix #936: Parsing of decimal-seconds with comma as separator fails