/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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.
 */

package android.location;

import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * A class that contains information about a GNSS antenna. GNSS antenna characteristics can change
 * with device configuration, such as when a device is folded open or closed. Antenna information is
 * delivered to registered instances of {@link Listener}.
 *
 * <p> Antenna info parameters are measured for each specific device model by the device
 * manufacturers and provided to the Android framework.
 *
 * <p> {@link GnssAntennaInfo} is returned per frequency band at the center frequency of the band.
 */
public final class GnssAntennaInfo implements Parcelable {
    private final double mCarrierFrequencyMHz;
    private final PhaseCenterOffset mPhaseCenterOffset;
    private final @Nullable SphericalCorrections mPhaseCenterVariationCorrections;
    private final @Nullable SphericalCorrections mSignalGainCorrections;

    /**
     * Used for receiving GNSS antenna info from the GNSS engine.
     */
    public interface Listener {
        /**
         * Invoked on a change to GNSS antenna info.
         */
        void onGnssAntennaInfoReceived(@NonNull List<GnssAntennaInfo> gnssAntennaInfos);
    }

    /**
     * Class containing information about the antenna phase center offset (PCO). PCO is defined with
     * respect to the origin of the Android sensor coordinate system, e.g., center of primary screen
     * for mobiles - see sensor or form factor documents for details. Uncertainties are reported
     * to 1-sigma.
     */
    public static final class PhaseCenterOffset implements Parcelable {
        private final double mOffsetXMm;
        private final double mOffsetXUncertaintyMm;
        private final double mOffsetYMm;
        private final double mOffsetYUncertaintyMm;
        private final double mOffsetZMm;
        private final double mOffsetZUncertaintyMm;

        public PhaseCenterOffset(
                double offsetXMm, double offsetXUncertaintyMm,
                double offsetYMm, double offsetYUncertaintyMm,
                double offsetZMm, double offsetZUncertaintyMm) {
            mOffsetXMm = offsetXMm;
            mOffsetYMm = offsetYMm;
            mOffsetZMm = offsetZMm;
            mOffsetXUncertaintyMm = offsetXUncertaintyMm;
            mOffsetYUncertaintyMm = offsetYUncertaintyMm;
            mOffsetZUncertaintyMm = offsetZUncertaintyMm;
        }

        public static final @NonNull Creator<PhaseCenterOffset> CREATOR =
                new Creator<PhaseCenterOffset>() {
                    @Override
                    public PhaseCenterOffset createFromParcel(Parcel in) {
                        return new PhaseCenterOffset(
                                in.readDouble(),
                                in.readDouble(),
                                in.readDouble(),
                                in.readDouble(),
                                in.readDouble(),
                                in.readDouble()
                        );
                    }

                    @Override
                    public PhaseCenterOffset[] newArray(int size) {
                        return new PhaseCenterOffset[size];
                    }
                };

        /**
         * Returns the x-axis offset of the phase center from the origin of the Android sensor
         * coordinate system, in millimeters.
         */
        @FloatRange()
        public double getXOffsetMm() {
            return mOffsetXMm;
        }

        /**
         * Returns the 1-sigma uncertainty of the x-axis offset of the phase center from the origin
         * of the Android sensor coordinate system, in millimeters.
         */
        @FloatRange()
        public double getXOffsetUncertaintyMm() {
            return mOffsetXUncertaintyMm;
        }

        /**
         * Returns the y-axis offset of the phase center from the origin of the Android sensor
         * coordinate system, in millimeters.
         */
        @FloatRange()
        public double getYOffsetMm() {
            return mOffsetYMm;
        }

        /**
         * Returns the 1-sigma uncertainty of the y-axis offset of the phase center from the origin
         * of the Android sensor coordinate system, in millimeters.
         */
        @FloatRange()
        public double getYOffsetUncertaintyMm() {
            return mOffsetYUncertaintyMm;
        }

        /**
         * Returns the z-axis offset of the phase center from the origin of the Android sensor
         * coordinate system, in millimeters.
         */
        @FloatRange()
        public double getZOffsetMm() {
            return mOffsetZMm;
        }

        /**
         * Returns the 1-sigma uncertainty of the z-axis offset of the phase center from the origin
         * of the Android sensor coordinate system, in millimeters.
         */
        @FloatRange()
        public double getZOffsetUncertaintyMm() {
            return mOffsetZUncertaintyMm;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            dest.writeDouble(mOffsetXMm);
            dest.writeDouble(mOffsetXUncertaintyMm);
            dest.writeDouble(mOffsetYMm);
            dest.writeDouble(mOffsetYUncertaintyMm);
            dest.writeDouble(mOffsetZMm);
            dest.writeDouble(mOffsetZUncertaintyMm);
        }

        @Override
        public String toString() {
            return "PhaseCenterOffset{"
                    + "OffsetXMm=" + mOffsetXMm + " +/-" + mOffsetXUncertaintyMm
                    + ", OffsetYMm=" + mOffsetYMm + " +/-" + mOffsetYUncertaintyMm
                    + ", OffsetZMm=" + mOffsetZMm + " +/-" + mOffsetZUncertaintyMm
                    + '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof PhaseCenterOffset)) {
                return false;
            }
            PhaseCenterOffset that = (PhaseCenterOffset) o;
            return Double.compare(that.mOffsetXMm, mOffsetXMm) == 0
                    && Double.compare(that.mOffsetXUncertaintyMm, mOffsetXUncertaintyMm) == 0
                    && Double.compare(that.mOffsetYMm, mOffsetYMm) == 0
                    && Double.compare(that.mOffsetYUncertaintyMm, mOffsetYUncertaintyMm) == 0
                    && Double.compare(that.mOffsetZMm, mOffsetZMm) == 0
                    && Double.compare(that.mOffsetZUncertaintyMm, mOffsetZUncertaintyMm) == 0;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mOffsetXMm, mOffsetYMm, mOffsetZMm);
        }
    }

    /**
     * Represents corrections on a spherical mapping. Corrections are added to measurements to
     * obtain the corrected values.
     *
     * The corrections and associated (1-sigma) uncertainties are represented by respect 2D arrays.
     *
     * Each row (major indices) represents a fixed theta. The first row corresponds to a
     * theta angle of 0 degrees. The last row corresponds to a theta angle of (360 - deltaTheta)
     * degrees, where deltaTheta is the regular spacing between azimuthal angles, i.e., deltaTheta
     * = 360 / (number of rows).
     *
     * The columns (minor indices) represent fixed zenith angles, beginning at 0 degrees and ending
     * at 180 degrees. They are separated by deltaPhi, the regular spacing between zenith angles,
     * i.e., deltaPhi = 180 / (number of columns - 1).
     */
    public static final class SphericalCorrections implements Parcelable {

        private final int mNumRows;
        private final int mNumColumns;
        private final double[][] mCorrections;
        private final double[][] mCorrectionUncertainties;

        public SphericalCorrections(@NonNull double[][] corrections,
                @NonNull double[][] correctionUncertainties) {
            if (corrections.length != correctionUncertainties.length || corrections.length < 1) {
                throw new IllegalArgumentException("correction and uncertainty arrays must have "
                        + "the same (non-zero) dimensions");
            }

            mNumRows = corrections.length;
            mNumColumns = corrections[0].length;
            for (int i = 0; i < corrections.length; i++) {
                if (corrections[i].length != mNumColumns
                        || correctionUncertainties[i].length != mNumColumns || mNumColumns < 2) {
                    throw new IllegalArgumentException("correction and uncertainty arrays must all "
                            + " have the same (greater than 2) number of columns");
                }
            }

            mCorrections = corrections;
            mCorrectionUncertainties = correctionUncertainties;
        }

        private SphericalCorrections(Parcel in) {
            int numRows = in.readInt();
            int numColumns = in.readInt();

            double[][] corrections =
                    new double[numRows][numColumns];
            double[][] correctionUncertainties =
                    new double[numRows][numColumns];

            for (int row = 0; row < numRows; row++) {
                for (int col = 0; col < numColumns; col++) {
                    corrections[row][col] = in.readDouble();
                    correctionUncertainties[row][col] = in.readDouble();
                }
            }

            mNumRows = numRows;
            mNumColumns = numColumns;
            mCorrections = corrections;
            mCorrectionUncertainties = correctionUncertainties;
        }

        /**
         * Array representing corrections on a spherical mapping. Corrections are added to
         * measurements to obtain the corrected values.
         *
         * Each row (major indices) represents a fixed theta. The first row corresponds to a
         * theta angle of 0 degrees. The last row corresponds to a theta angle of (360 - deltaTheta)
         * degrees, where deltaTheta is the regular spacing between azimuthal angles, i.e.,
         * deltaTheta = 360 / (number of rows).
         *
         * The columns (minor indices) represent fixed zenith angles, beginning at 0 degrees and
         * ending at 180 degrees. They are separated by deltaPhi, the regular spacing between zenith
         * angles, i.e., deltaPhi = 180 / (number of columns - 1).
         */
        @NonNull
        public double[][] getCorrectionsArray() {
            return mCorrections;
        }

        /**
         * Array representing uncertainty on corrections on a spherical mapping.
         *
         * Each row (major indices) represents a fixed theta. The first row corresponds to a
         * theta angle of 0 degrees. The last row corresponds to a theta angle of (360 - deltaTheta)
         * degrees, where deltaTheta is the regular spacing between azimuthal angles, i.e.,
         * deltaTheta = 360 / (number of rows).
         *
         * The columns (minor indices) represent fixed zenith angles, beginning at 0 degrees and
         * ending at 180 degrees. They are separated by deltaPhi, the regular spacing between zenith
         * angles, i.e., deltaPhi = 180 / (number of columns - 1).
         */
        @NonNull
        public double[][] getCorrectionUncertaintiesArray() {
            return mCorrectionUncertainties;
        }

        /**
         * The fixed theta angle separation between successive rows.
         */
        @FloatRange(from = 0.0f, to = 360.0f)
        public double getDeltaTheta() {
            return 360.0D / mNumRows;
        }

        /**
         * The fixed phi angle separation between successive columns.
         */
        @FloatRange(from = 0.0f, to = 180.0f)
        public double getDeltaPhi() {
            return 180.0D / (mNumColumns - 1);
        }


        public static final @NonNull Creator<SphericalCorrections> CREATOR =
                new Creator<SphericalCorrections>() {
                    @Override
                    public SphericalCorrections createFromParcel(Parcel in) {
                        return new SphericalCorrections(in);
                    }

                    @Override
                    public SphericalCorrections[] newArray(int size) {
                        return new SphericalCorrections[size];
                    }
                };

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            dest.writeInt(mNumRows);
            dest.writeInt(mNumColumns);
            for (int row = 0; row < mNumRows; row++) {
                for (int col = 0; col < mNumColumns; col++) {
                    dest.writeDouble(mCorrections[row][col]);
                    dest.writeDouble(mCorrectionUncertainties[row][col]);
                }
            }
        }

        @Override
        public String toString() {
            return "SphericalCorrections{"
                    + "Corrections=" + Arrays.deepToString(mCorrections)
                    + ", CorrectionUncertainties=" + Arrays.deepToString(mCorrectionUncertainties)
                    + ", DeltaTheta=" + getDeltaTheta()
                    + ", DeltaPhi=" + getDeltaPhi()
                    + '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof SphericalCorrections)) {
                return false;
            }
            SphericalCorrections that = (SphericalCorrections) o;
            return mNumRows == that.mNumRows
                    && mNumColumns == that.mNumColumns
                    && Arrays.deepEquals(mCorrections, that.mCorrections)
                    && Arrays.deepEquals(mCorrectionUncertainties, that.mCorrectionUncertainties);
        }

        @Override
        public int hashCode() {
            int result = Arrays.deepHashCode(mCorrections);
            result = 31 * result + Arrays.deepHashCode(mCorrectionUncertainties);
            return result;
        }
    }

    private GnssAntennaInfo(
            double carrierFrequencyMHz,
            PhaseCenterOffset phaseCenterOffset,
            @Nullable SphericalCorrections phaseCenterVariationCorrections,
            @Nullable SphericalCorrections signalGainCorrectionDbi) {
        mCarrierFrequencyMHz = carrierFrequencyMHz;
        mPhaseCenterOffset = Objects.requireNonNull(phaseCenterOffset);
        mPhaseCenterVariationCorrections = phaseCenterVariationCorrections;
        mSignalGainCorrections = signalGainCorrectionDbi;
    }

    /**
     * Builder class for GnssAntennaInfo.
     */
    public static class Builder {
        private double mCarrierFrequencyMHz;
        private PhaseCenterOffset mPhaseCenterOffset;
        private @Nullable SphericalCorrections mPhaseCenterVariationCorrections;
        private @Nullable SphericalCorrections mSignalGainCorrections;

        /**
         * @deprecated Prefer {@link #Builder(double, PhaseCenterOffset)}.
         */
        @Deprecated
        public Builder() {
            this(0, new PhaseCenterOffset(0, 0, 0, 0, 0, 0));
        }

        public Builder(double carrierFrequencyMHz, @NonNull PhaseCenterOffset phaseCenterOffset) {
            mCarrierFrequencyMHz = carrierFrequencyMHz;
            mPhaseCenterOffset = Objects.requireNonNull(phaseCenterOffset);
        }

        public Builder(@NonNull GnssAntennaInfo antennaInfo) {
            mCarrierFrequencyMHz = antennaInfo.mCarrierFrequencyMHz;
            mPhaseCenterOffset = antennaInfo.mPhaseCenterOffset;
            mPhaseCenterVariationCorrections = antennaInfo.mPhaseCenterVariationCorrections;
            mSignalGainCorrections = antennaInfo.mSignalGainCorrections;
        }

        /**
         * Set antenna carrier frequency (MHz).
         *
         * @param carrierFrequencyMHz antenna carrier frequency (MHz)
         * @return Builder builder object
         */
        @NonNull
        public Builder setCarrierFrequencyMHz(@FloatRange(from = 0.0f) double carrierFrequencyMHz) {
            mCarrierFrequencyMHz = carrierFrequencyMHz;
            return this;
        }

        /**
         * Set antenna phase center offset.
         *
         * @param phaseCenterOffset phase center offset object
         * @return Builder builder object
         */
        @NonNull
        public Builder setPhaseCenterOffset(@NonNull PhaseCenterOffset phaseCenterOffset) {
            mPhaseCenterOffset = Objects.requireNonNull(phaseCenterOffset);
            return this;
        }

        /**
         * Set phase center variation corrections.
         *
         * @param phaseCenterVariationCorrections phase center variation corrections object
         * @return Builder builder object
         */
        @NonNull
        public Builder setPhaseCenterVariationCorrections(
                @Nullable SphericalCorrections phaseCenterVariationCorrections) {
            mPhaseCenterVariationCorrections = phaseCenterVariationCorrections;
            return this;
        }

        /**
         * Set signal gain corrections.
         *
         * @param signalGainCorrections signal gain corrections object
         * @return Builder builder object
         */
        @NonNull
        public Builder setSignalGainCorrections(
                @Nullable SphericalCorrections signalGainCorrections) {
            mSignalGainCorrections = signalGainCorrections;
            return this;
        }

        /**
         * Build GnssAntennaInfo object.
         *
         * @return instance of GnssAntennaInfo
         */
        @NonNull
        public GnssAntennaInfo build() {
            return new GnssAntennaInfo(mCarrierFrequencyMHz, mPhaseCenterOffset,
                    mPhaseCenterVariationCorrections, mSignalGainCorrections);
        }
    }

    /** Returns the center frequency of the corresponding carrier frequency band. */
    @FloatRange(from = 0.0f)
    public double getCarrierFrequencyMHz() {
        return mCarrierFrequencyMHz;
    }

    /**
     * Returns a {@link PhaseCenterOffset} object encapsulating the phase center offset and
     * corresponding uncertainties in millimeters.
     *
     * @return {@link PhaseCenterOffset}
     */
    @NonNull
    public PhaseCenterOffset getPhaseCenterOffset() {
        return mPhaseCenterOffset;
    }

    /**
     * Returns a {@link SphericalCorrections} object encapsulating the phase center variation
     * corrections and corresponding uncertainties in millimeters.
     *
     * @return phase center variation corrections as {@link SphericalCorrections}
     */
    @Nullable
    public SphericalCorrections getPhaseCenterVariationCorrections() {
        return mPhaseCenterVariationCorrections;
    }

    /**
     * Returns a {@link SphericalCorrections} object encapsulating the signal gain
     * corrections and corresponding uncertainties in dBi.
     *
     * @return signal gain corrections as {@link SphericalCorrections}
     */
    @Nullable
    public SphericalCorrections getSignalGainCorrections() {
        return mSignalGainCorrections;
    }

    public static final @NonNull Creator<GnssAntennaInfo> CREATOR = new Creator<GnssAntennaInfo>() {
        @Override
        public GnssAntennaInfo createFromParcel(Parcel in) {
            double carrierFrequencyMHz = in.readDouble();
            PhaseCenterOffset phaseCenterOffset =
                    in.readTypedObject(PhaseCenterOffset.CREATOR);
            SphericalCorrections phaseCenterVariationCorrections =
                    in.readTypedObject(SphericalCorrections.CREATOR);
            SphericalCorrections signalGainCorrections =
                    in.readTypedObject(SphericalCorrections.CREATOR);

            return new GnssAntennaInfo(
                    carrierFrequencyMHz,
                    phaseCenterOffset,
                    phaseCenterVariationCorrections,
                    signalGainCorrections);
        }

        @Override
        public GnssAntennaInfo[] newArray(int size) {
            return new GnssAntennaInfo[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(@NonNull Parcel parcel, int flags) {
        parcel.writeDouble(mCarrierFrequencyMHz);
        parcel.writeTypedObject(mPhaseCenterOffset, flags);
        parcel.writeTypedObject(mPhaseCenterVariationCorrections, flags);
        parcel.writeTypedObject(mSignalGainCorrections, flags);
    }

    @Override
    public String toString() {
        return "GnssAntennaInfo{"
                + "CarrierFrequencyMHz=" + mCarrierFrequencyMHz
                + ", PhaseCenterOffset=" + mPhaseCenterOffset
                + ", PhaseCenterVariationCorrections=" + mPhaseCenterVariationCorrections
                + ", SignalGainCorrections=" + mSignalGainCorrections
                + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof GnssAntennaInfo)) {
            return false;
        }
        GnssAntennaInfo that = (GnssAntennaInfo) o;
        return Double.compare(that.mCarrierFrequencyMHz, mCarrierFrequencyMHz) == 0
                && mPhaseCenterOffset.equals(that.mPhaseCenterOffset)
                && Objects.equals(mPhaseCenterVariationCorrections,
                    that.mPhaseCenterVariationCorrections)
                && Objects.equals(mSignalGainCorrections, that.mSignalGainCorrections);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mCarrierFrequencyMHz, mPhaseCenterOffset,
                mPhaseCenterVariationCorrections, mSignalGainCorrections);
    }
}
