/*
 * Copyright 2002-2019 Drew Noakes and contributors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 *
 * More information about this project is available at:
 *
 *    https://drewnoakes.com/code/exif/
 *    https://github.com/drewnoakes/metadata-extractor
 */
package com.drew.metadata.exif;

import com.drew.lang.GeoLocation;
import com.drew.lang.Rational;
import com.drew.lang.annotations.NotNull;
import com.drew.lang.annotations.Nullable;
import com.drew.metadata.TagDescriptor;

import java.text.DecimalFormat;

import static com.drew.metadata.exif.GpsDirectory.*;

/**
 * Provides human-readable string representations of tag values stored in a {@link GpsDirectory}.
 *
 * @author Drew Noakes https://drewnoakes.com
 */
@SuppressWarnings("WeakerAccess")
public class GpsDescriptor extends TagDescriptor<GpsDirectory>
{
    public GpsDescriptor(@NotNull GpsDirectory directory)
    {
        super(directory);
    }

    @Override
    @Nullable
    public String getDescription(int tagType)
    {
        switch (tagType) {
            case TAG_VERSION_ID:
                return getGpsVersionIdDescription();
            case TAG_ALTITUDE:
                return getGpsAltitudeDescription();
            case TAG_ALTITUDE_REF:
                return getGpsAltitudeRefDescription();
            case TAG_STATUS:
                return getGpsStatusDescription();
            case TAG_MEASURE_MODE:
                return getGpsMeasureModeDescription();
            case TAG_DOP:
                return getGpsDopDescription();
            case TAG_SPEED_REF:
                return getGpsSpeedRefDescription();
            case TAG_SPEED:
                return getGpsSpeedDescription();
            case TAG_TRACK_REF:
            case TAG_IMG_DIRECTION_REF:
            case TAG_DEST_BEARING_REF:
                return getGpsDirectionReferenceDescription(tagType);
            case TAG_TRACK:
            case TAG_IMG_DIRECTION:
            case TAG_DEST_BEARING:
                return getGpsDirectionDescription(tagType);
            case TAG_DEST_LATITUDE:
                return getGpsDestLatitudeDescription();
            case TAG_DEST_LONGITUDE:
                return getGpsDestLongitudeDescription();
            case TAG_DEST_DISTANCE_REF:
                return getGpsDestinationReferenceDescription();
            case TAG_DEST_DISTANCE:
                return getGpsDestDistanceDescription();
            case TAG_TIME_STAMP:
                return getGpsTimeStampDescription();
            case TAG_LONGITUDE:
                // three rational numbers -- displayed in HH"MM"SS.ss
                return getGpsLongitudeDescription();
            case TAG_LATITUDE:
                // three rational numbers -- displayed in HH"MM"SS.ss
                return getGpsLatitudeDescription();
            case TAG_PROCESSING_METHOD:
                return getGpsProcessingMethodDescription();
            case TAG_AREA_INFORMATION:
                return getGpsAreaInformationDescription();
            case TAG_DIFFERENTIAL:
                return getGpsDifferentialDescription();
            case TAG_H_POSITIONING_ERROR:
                return getGpsHPositioningErrorDescription();
            default:
                return super.getDescription(tagType);
        }
    }

    @Nullable
    private String getGpsVersionIdDescription()
    {
        return getVersionBytesDescription(TAG_VERSION_ID, 1);
    }

    @Nullable
    public String getGpsLatitudeDescription()
    {
        GeoLocation location = _directory.getGeoLocation();
        return location == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(location.getLatitude());
    }

    @Nullable
    public String getGpsLongitudeDescription()
    {
        GeoLocation location = _directory.getGeoLocation();
        return location == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(location.getLongitude());
    }

    @Nullable
    public String getGpsTimeStampDescription()
    {
        // time in hour, min, sec
        Rational[] timeComponents = _directory.getRationalArray(TAG_TIME_STAMP);
        DecimalFormat df = new DecimalFormat("00.000");
        return timeComponents == null
            ? null
            : String.format("%02d:%02d:%s UTC",
                timeComponents[0].intValue(),
                timeComponents[1].intValue(),
                df.format(timeComponents[2].doubleValue()));
    }

    @Nullable
    public String getGpsDestLatitudeDescription()
    {
        return getGeoLocationDimension(TAG_DEST_LATITUDE, TAG_DEST_LATITUDE_REF, "S");
    }

    @Nullable
    public String getGpsDestLongitudeDescription()
    {
        return getGeoLocationDimension(TAG_DEST_LONGITUDE, TAG_DEST_LONGITUDE_REF, "W");
    }

    @Nullable
    private String getGeoLocationDimension(int tagValue, int tagRef, String positiveRef)
    {
        Rational[] values = _directory.getRationalArray(tagValue);
        String ref = _directory.getString(tagRef);

        if (values == null || values.length != 3 || ref == null)
            return null;

        Double dec = GeoLocation.degreesMinutesSecondsToDecimal(
            values[0], values[1], values[2], ref.equalsIgnoreCase(positiveRef));

        return dec == null ? null : GeoLocation.decimalToDegreesMinutesSecondsString(dec);
    }

    @Nullable
    public String getGpsDestinationReferenceDescription()
    {
        final String value = _directory.getString(TAG_DEST_DISTANCE_REF);
        if (value == null)
            return null;
        String distanceRef = value.trim();
        if ("K".equalsIgnoreCase(distanceRef)) {
            return "kilometers";
        } else if ("M".equalsIgnoreCase(distanceRef)) {
            return "miles";
        } else if ("N".equalsIgnoreCase(distanceRef)) {
            return "knots";
        } else {
            return "Unknown (" + distanceRef + ")";
        }
    }

    @Nullable
    public String getGpsDestDistanceDescription()
    {
        final Rational value = _directory.getRational(TAG_DEST_DISTANCE);
        if (value == null)
            return null;
        final String unit = getGpsDestinationReferenceDescription();
        return String.format("%s %s",
            new DecimalFormat("0.##").format(value.doubleValue()),
            unit == null ? "unit" : unit.toLowerCase());
    }

    @Nullable
    public String getGpsDirectionDescription(int tagType)
    {
        Rational angle = _directory.getRational(tagType);
        // provide a decimal version of rational numbers in the description, to avoid strings like "35334/199 degrees"
        String value = angle != null
            ? new DecimalFormat("0.##").format(angle.doubleValue())
            : _directory.getString(tagType);
        return value == null || value.trim().length() == 0 ? null : value.trim() + " degrees";
    }

    @Nullable
    public String getGpsDirectionReferenceDescription(int tagType)
    {
        final String value = _directory.getString(tagType);
        if (value == null)
            return null;
        String gpsDistRef = value.trim();
        if ("T".equalsIgnoreCase(gpsDistRef)) {
            return "True direction";
        } else if ("M".equalsIgnoreCase(gpsDistRef)) {
            return "Magnetic direction";
        } else {
            return "Unknown (" + gpsDistRef + ")";
        }
    }

    @Nullable
    public String getGpsDopDescription()
    {
        final Rational value = _directory.getRational(TAG_DOP);
        return value == null ? null : new DecimalFormat("0.##").format(value.doubleValue());
    }

    @Nullable
    public String getGpsSpeedRefDescription()
    {
        final String value = _directory.getString(TAG_SPEED_REF);
        if (value == null)
            return null;
        String gpsSpeedRef = value.trim();
        if ("K".equalsIgnoreCase(gpsSpeedRef)) {
            return "km/h";
        } else if ("M".equalsIgnoreCase(gpsSpeedRef)) {
            return "mph";
        } else if ("N".equalsIgnoreCase(gpsSpeedRef)) {
            return "knots";
        } else {
            return "Unknown (" + gpsSpeedRef + ")";
        }
    }

    @Nullable
    public String getGpsSpeedDescription()
    {
        final Rational value = _directory.getRational(TAG_SPEED);
        if (value == null)
            return null;
        final String unit = getGpsSpeedRefDescription();
        return String.format("%s %s",
            new DecimalFormat("0.##").format(value.doubleValue()),
            unit == null ? "unit" : unit.toLowerCase());
    }

    @Nullable
    public String getGpsMeasureModeDescription()
    {
        final String value = _directory.getString(TAG_MEASURE_MODE);
        if (value == null)
            return null;
        String gpsSpeedMeasureMode = value.trim();
        if ("2".equalsIgnoreCase(gpsSpeedMeasureMode)) {
            return "2-dimensional measurement";
        } else if ("3".equalsIgnoreCase(gpsSpeedMeasureMode)) {
            return "3-dimensional measurement";
        } else {
            return "Unknown (" + gpsSpeedMeasureMode + ")";
        }
    }

    @Nullable
    public String getGpsStatusDescription()
    {
        final String value = _directory.getString(TAG_STATUS);
        if (value == null)
            return null;
        String gpsStatus = value.trim();
        if ("A".equalsIgnoreCase(gpsStatus)) {
            return "Active (Measurement in progress)";
        } else if ("V".equalsIgnoreCase(gpsStatus)) {
            return "Void (Measurement Interoperability)";
        } else {
            return "Unknown (" + gpsStatus + ")";
        }
    }

    @Nullable
    public String getGpsAltitudeRefDescription()
    {
        return getIndexedDescription(TAG_ALTITUDE_REF, "Sea level", "Below sea level");
    }

    @Nullable
    public String getGpsAltitudeDescription()
    {
        final Rational value = _directory.getRational(TAG_ALTITUDE);
        return value == null ? null : new DecimalFormat("0.##").format(value.doubleValue()) + " metres";
    }

    @Nullable
    public String getGpsProcessingMethodDescription()
    {
        return getEncodedTextDescription(TAG_PROCESSING_METHOD);
    }

    @Nullable
    public String getGpsAreaInformationDescription()
    {
        return getEncodedTextDescription(TAG_AREA_INFORMATION);
    }

    @Nullable
    public String getGpsDifferentialDescription()
    {
        return getIndexedDescription(TAG_DIFFERENTIAL, "No Correction", "Differential Corrected");
    }

    @Nullable
    public String getGpsHPositioningErrorDescription()
    {
        final Rational value = _directory.getRational(TAG_H_POSITIONING_ERROR);
        return value == null ? null : new DecimalFormat("0.##").format(value.doubleValue()) + " metres";
    }

    @Nullable
    public String getDegreesMinutesSecondsDescription()
    {
        GeoLocation location = _directory.getGeoLocation();
        return location == null ? null : location.toDMSString();
    }
}
