package com.dji.sdk.cloudapi.map;

import com.dji.sdk.common.AMap;
import com.dji.sdk.common.CoordinateUtil;
import com.dji.sdk.exception.CloudSDKErrorEnum;
import com.dji.sdk.exception.CloudSDKException;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.util.CollectionUtils;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author sean
 * @version 0.2
 * @date 2021/11/30
 */
@Schema(description = "line geometry")
public class ElementLineStringGeometry extends ElementGeometryType {

    @Schema(example = "LineString")
    @NotNull
    private final String type = ElementResourceTypeEnum.LINE_STRING.getTypeName();

    @Schema(example = "[[113.943109, 22.577378]]")
    @NotNull
    @Size(min = 2)
    private Double[][] coordinates;

    public ElementLineStringGeometry() {
        super();
    }

    @Override
    public List<ElementCoordinate> convertToList() {
        if (this.coordinates.length < 2) {
            throw new CloudSDKException(CloudSDKErrorEnum.INVALID_PARAMETER);
        }
        List<ElementCoordinate> coordinateList = new ArrayList<>();
        for (Double[] coordinate : this.coordinates) {
            coordinateList.add(new ElementCoordinate()
                    .setLongitude(coordinate[0])
                    .setLatitude(coordinate[1]));
        }
        return coordinateList;
    }

    @Override
    public void adapterCoordinateType(List<ElementCoordinate> coordinateList) {
        if (CollectionUtils.isEmpty(coordinateList) || coordinateList.size() < 2) {
            throw new CloudSDKException(CloudSDKErrorEnum.INVALID_PARAMETER);
        }
        this.coordinates = new Double[coordinateList.size()][2];
        for (int i = 0; i < this.coordinates.length; i++) {
            this.coordinates[i][0] = coordinateList.get(i).getLongitude();
            this.coordinates[i][1] = coordinateList.get(i).getLatitude();
        }
    }

    @Override
    public String toString() {
        return "ElementLineStringGeometry{" +
                "coordinates=" + Arrays.toString(coordinates) +
                '}';
    }

    public Double[][] getCoordinates() {
        return coordinates;
    }

    public ElementLineStringGeometry setCoordinates(Double[][] coordinates) {
        this.coordinates = coordinates;
        return this;
    }

    @Override
    public String getType() {
        return type;
    }

    /**
     * Calculates the distance between two points on Earth using the Haversine formula.
     *
     * @return The distance in meters.
     */
    public double getCalculateSegmentLength() {
        List<ElementCoordinate> list = convertToList();
        ElementCoordinate coord1 = list.get(0);
        ElementCoordinate coord2 = list.get(1);
        final double EARTH_RADIUS = 6371000; // Earth radius in meters
        double lat1 = Math.toRadians(coord1.getLatitude());
        double lon1 = Math.toRadians(coord1.getLongitude());
        double lat2 = Math.toRadians(coord2.getLatitude());
        double lon2 = Math.toRadians(coord2.getLongitude());

        double dLat = lat2 - lat1;
        double dLon = lon2 - lon1;

        double a = Math.pow(Math.sin(dLat / 2), 2) +
                Math.cos(lat1) * Math.cos(lat2) *
                        Math.pow(Math.sin(dLon / 2), 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        return EARTH_RADIUS * c;
    }

    public double calculateSegmentLength(ElementCoordinate coord1, ElementCoordinate coord2) {
        final double EARTH_RADIUS = 6371000; // Earth radius in meters
        double lat1 = Math.toRadians(coord1.getLatitude());
        double lon1 = Math.toRadians(coord1.getLongitude());
        double lat2 = Math.toRadians(coord2.getLatitude());
        double lon2 = Math.toRadians(coord2.getLongitude());

        double dLat = lat2 - lat1;
        double dLon = lon2 - lon1;

        double a = Math.pow(Math.sin(dLat / 2), 2) +
                Math.cos(lat1) * Math.cos(lat2) *
                        Math.pow(Math.sin(dLon / 2), 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        return EARTH_RADIUS * c;
    }

    /**
     * Calculates the distance between two points on Earth using Vincenty's formula.
     *
     * @param coord1 The first coordinate.
     * @param coord2 The second coordinate.
     * @return The distance in meters.
     */
    private double calculateSegmentLengthVincenty(ElementCoordinate coord1, ElementCoordinate coord2) {
        final double EARTH_RADIUS_EQUATOR = 6378137; // Semi-major axis of the WGS84 ellipsoid
        final double EARTH_RADIUS_POLE = 6356752.314245; // Semi-minor axis of the WGS84 ellipsoid
        final double FLATTENING = (EARTH_RADIUS_EQUATOR - EARTH_RADIUS_POLE) / EARTH_RADIUS_EQUATOR; // Flattening factor
        final double EARTH_RADIUS = (EARTH_RADIUS_EQUATOR + EARTH_RADIUS_POLE) / 2.0;

        double lat1 = Math.toRadians(coord1.getLatitude());
        double lon1 = Math.toRadians(coord1.getLongitude());
        double lat2 = Math.toRadians(coord2.getLatitude());
        double lon2 = Math.toRadians(coord2.getLongitude());

        double U1 = Math.atan((1 - FLATTENING) * Math.tan(lat1));
        double U2 = Math.atan((1 - FLATTENING) * Math.tan(lat2));
        double L = lon2 - lon1;
        double lambda = L;
        double lambdaP = 0.0;
        double iterLimit = 100;
        double sinLambda = 0.0;
        double cosLambda = 0.0;
        double sinSigma = 0.0;
        double cosSigma = 0.0;
        double sigma = 0.0;
        double sinAlpha = 0.0;
        double cosSqAlpha = 0.0;
        double cos2SigmaM = 0.0;
        double uSquared = 0.0;
        double A = 0.0;
        double B = 0.0;
        double deltaSigma = 0.0;
        double sinU1 = Math.sin(U1);
        double cosU1 = Math.cos(U1);
        double sinU2 = Math.sin(U2);
        double cosU2 = Math.cos(U2);

        do {
            sinLambda = Math.sin(lambda);
            cosLambda = Math.cos(lambda);
            sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) +
                    (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda));
            if (sinSigma == 0.0) {
                return 0.0; // coincident points
            }
            cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda;
            sigma = Math.atan2(sinSigma, cosSigma);
            sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma;
            cosSqAlpha = 1 - sinAlpha * sinAlpha;
            cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha;
            if (Double.isNaN(cos2SigmaM)) {
                cos2SigmaM = 0; // equatorial line: cosSqAlpha = 0 (§6)
            }
            uSquared = cosSqAlpha * (EARTH_RADIUS_EQUATOR * EARTH_RADIUS_EQUATOR - EARTH_RADIUS_POLE * EARTH_RADIUS_POLE) / (EARTH_RADIUS_POLE * EARTH_RADIUS_POLE);
            A = 1 + uSquared / 16384 * (4096 + uSquared * (-768 + uSquared * (320 - 175 * uSquared)));
            B = uSquared / 1024 * (256 + uSquared * (-128 + uSquared * (74 - 47 * uSquared)));
            deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) -
                    B / 6 * cos2SigmaM * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
            lambdaP = lambda;
            lambda = L + (1 - B) * FLATTENING * sinAlpha * (sigma + deltaSigma); // Use the flattening factor here
        } while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0);

        if (iterLimit == 0) {
            return 0.0; // formula failed to converge
        }

        double s = EARTH_RADIUS * (sigma - deltaSigma);

        return s;
    }

    /**
     * Calculates the total length of the line string.
     *
     * @return The total length in meters.
     */
    public double getCalculateTotalLength() {
        double totalLength = 0.0;
        List<ElementCoordinate> coordinateList = convertToList();

        for (int i = 0; i < coordinateList.size() - 1; i++) {
            ElementCoordinate coord1 = coordinateList.get(i);
            AMap aMap = CoordinateUtil.transform(coord1.getLongitude(),coord1.getLatitude());
            coord1.setLongitude(aMap.getLongitude());
            coord1.setLatitude(aMap.getLatitude());

            ElementCoordinate coord2 = coordinateList.get(i + 1);
            aMap = CoordinateUtil.transform(coord2.getLongitude(),coord2.getLatitude());
            coord2.setLongitude(aMap.getLongitude());
            coord2.setLatitude(aMap.getLatitude());

            totalLength += calculateSegmentLengthVincenty(coord1, coord2);
        }

        return totalLength;
    }

    public static void main(String[] args) {
        ElementLineStringGeometry lineStringGeometry = new ElementLineStringGeometry();
        AMap aMap = CoordinateUtil.transform(121.59529228016724, 31.2771648028396);
        AMap aMap2 = CoordinateUtil.transform(121.55949257988347, 31.27563389661576);
        AMap aMap3 = CoordinateUtil.transform(121.55866186357967, 31.27687211836336);
        Double[][] coordinates = new Double[][]{
                {aMap.getLongitude(),aMap.getLatitude()},
                {aMap2.getLongitude(),aMap2.getLatitude()},
                {aMap3.getLongitude(),aMap3.getLatitude()}
//                {121.59529228016724, 31.2771648028396},
//                {121.55949257988347, 31.27563389661576},
//                {121.55866186357967, 31.27687211836336}


        };
        lineStringGeometry.setCoordinates(coordinates);

        double totalLength = lineStringGeometry.getCalculateTotalLength();
        System.out.println("The total length of the line string is: " + totalLength + " meters");
    }

}
