/*
 * Copyright (C) 2016 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 com.android.internal.location.gnssmetrics;

import android.location.GnssStatus;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.connectivity.GpsBatteryStats;
import android.server.location.ServerLocationProtoEnums;
import android.text.format.DateUtils;
import android.util.Base64;
import android.util.Log;
import android.util.StatsLog;
import android.util.TimeUtils;

import com.android.internal.app.IBatteryStats;
import com.android.internal.location.nano.GnssLogsProto.GnssLog;
import com.android.internal.location.nano.GnssLogsProto.PowerMetrics;

import java.util.Arrays;

/**
 * GnssMetrics: Is used for logging GNSS metrics
 *
 * @hide
 */
public class GnssMetrics {

  private static final String TAG = GnssMetrics.class.getSimpleName();

  /* Constant which indicates GPS signal quality is as yet unknown */
  public static final int GPS_SIGNAL_QUALITY_UNKNOWN =
          ServerLocationProtoEnums.GPS_SIGNAL_QUALITY_UNKNOWN; // -1

  /* Constant which indicates GPS signal quality is poor */
  public static final int GPS_SIGNAL_QUALITY_POOR =
      ServerLocationProtoEnums.GPS_SIGNAL_QUALITY_POOR; // 0

  /* Constant which indicates GPS signal quality is good */
  public static final int GPS_SIGNAL_QUALITY_GOOD =
      ServerLocationProtoEnums.GPS_SIGNAL_QUALITY_GOOD; // 1

  /* Number of GPS signal quality levels */
  public static final int NUM_GPS_SIGNAL_QUALITY_LEVELS = GPS_SIGNAL_QUALITY_GOOD + 1;

  /** Default time between location fixes (in millisecs) */
  private static final int DEFAULT_TIME_BETWEEN_FIXES_MILLISECS = 1000;

  /* The time since boot when logging started */
  private String logStartInElapsedRealTime;

  /* GNSS power metrics */
  private GnssPowerMetrics mGnssPowerMetrics;

    /**
     * A boolean array indicating whether the constellation types have been used in fix.
     */
    private boolean[] mConstellationTypes;

  /** Constructor */
  public GnssMetrics(IBatteryStats stats) {
    mGnssPowerMetrics = new GnssPowerMetrics(stats);
    locationFailureStatistics = new Statistics();
    timeToFirstFixSecStatistics = new Statistics();
    positionAccuracyMeterStatistics = new Statistics();
    topFourAverageCn0Statistics = new Statistics();
    reset();
  }

  /**
   * Logs the status of a location report received from the HAL
   *
   * @param isSuccessful
   */
  public void logReceivedLocationStatus(boolean isSuccessful) {
    if (!isSuccessful) {
      locationFailureStatistics.addItem(1.0);
      return;
    }
    locationFailureStatistics.addItem(0.0);
    return;
  }

  /**
   * Logs missed reports
   *
   * @param desiredTimeBetweenFixesMilliSeconds
   * @param actualTimeBetweenFixesMilliSeconds
   */
  public void logMissedReports(int desiredTimeBetweenFixesMilliSeconds,
      int actualTimeBetweenFixesMilliSeconds) {
    int numReportMissed = (actualTimeBetweenFixesMilliSeconds /
        Math.max(DEFAULT_TIME_BETWEEN_FIXES_MILLISECS, desiredTimeBetweenFixesMilliSeconds)) - 1;
    if (numReportMissed > 0) {
      for (int i = 0; i < numReportMissed; i++) {
        locationFailureStatistics.addItem(1.0);
      }
    }
    return;
  }

  /**
   * Logs time to first fix
   *
   * @param timeToFirstFixMilliSeconds
   */
  public void logTimeToFirstFixMilliSecs(int timeToFirstFixMilliSeconds) {
    timeToFirstFixSecStatistics.addItem((double) (timeToFirstFixMilliSeconds/1000));
    return;
  }

  /**
   * Logs position accuracy
   *
   * @param positionAccuracyMeters
   */
  public void logPositionAccuracyMeters(float positionAccuracyMeters) {
    positionAccuracyMeterStatistics.addItem((double) positionAccuracyMeters);
    return;
  }

  /*
  * Logs CN0 when at least 4 SVs are available
  *
  */
  public void logCn0(float[] cn0s, int numSv) {
    if (numSv == 0 || cn0s == null || cn0s.length == 0 || cn0s.length < numSv) {
      if (numSv == 0) {
         mGnssPowerMetrics.reportSignalQuality(null, 0);
      }
      return;
    }
    float[] cn0Array = Arrays.copyOf(cn0s, numSv);
    Arrays.sort(cn0Array);
    mGnssPowerMetrics.reportSignalQuality(cn0Array, numSv);
    if (numSv < 4) {
      return;
    }
    if (cn0Array[numSv - 4] > 0.0) {
      double top4AvgCn0 = 0.0;
      for (int i = numSv - 4; i < numSv; i++) {
        top4AvgCn0 += (double) cn0Array[i];
      }
      top4AvgCn0 /= 4;
      topFourAverageCn0Statistics.addItem(top4AvgCn0);
    }
    return;
  }


    /**
     * Logs that a constellation type has been observed.
     */
    public void logConstellationType(int constellationType) {
        if (constellationType >= mConstellationTypes.length) {
            Log.e(TAG, "Constellation type " + constellationType + " is not valid.");
            return;
        }
        mConstellationTypes[constellationType] = true;
    }

  /**
   * Dumps GNSS metrics as a proto string
   * @return
   */
  public String dumpGnssMetricsAsProtoString() {
    GnssLog msg = new GnssLog();
    if (locationFailureStatistics.getCount() > 0) {
      msg.numLocationReportProcessed = locationFailureStatistics.getCount();
      msg.percentageLocationFailure = (int) (100.0 * locationFailureStatistics.getMean());
    }
    if (timeToFirstFixSecStatistics.getCount() > 0) {
      msg.numTimeToFirstFixProcessed = timeToFirstFixSecStatistics.getCount();
      msg.meanTimeToFirstFixSecs = (int) timeToFirstFixSecStatistics.getMean();
      msg.standardDeviationTimeToFirstFixSecs
          = (int) timeToFirstFixSecStatistics.getStandardDeviation();
    }
    if (positionAccuracyMeterStatistics.getCount() > 0) {
      msg.numPositionAccuracyProcessed = positionAccuracyMeterStatistics.getCount();
      msg.meanPositionAccuracyMeters = (int) positionAccuracyMeterStatistics.getMean();
      msg.standardDeviationPositionAccuracyMeters
          = (int) positionAccuracyMeterStatistics.getStandardDeviation();
    }
    if (topFourAverageCn0Statistics.getCount() > 0) {
      msg.numTopFourAverageCn0Processed = topFourAverageCn0Statistics.getCount();
      msg.meanTopFourAverageCn0DbHz = topFourAverageCn0Statistics.getMean();
      msg.standardDeviationTopFourAverageCn0DbHz
          = topFourAverageCn0Statistics.getStandardDeviation();
    }
    msg.powerMetrics = mGnssPowerMetrics.buildProto();
    msg.hardwareRevision = SystemProperties.get("ro.boot.revision", "");
    String s = Base64.encodeToString(GnssLog.toByteArray(msg), Base64.DEFAULT);
    reset();
    return s;
  }

  /**
   * Dumps GNSS Metrics as text
   *
   * @return GNSS Metrics
   */
  public String dumpGnssMetricsAsText() {
    StringBuilder s = new StringBuilder();
    s.append("GNSS_KPI_START").append('\n');
    s.append("  KPI logging start time: ").append(logStartInElapsedRealTime).append("\n");
    s.append("  KPI logging end time: ");
    TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s);
    s.append("\n");
    s.append("  Number of location reports: ").append(
        locationFailureStatistics.getCount()).append("\n");
    if (locationFailureStatistics.getCount() > 0) {
      s.append("  Percentage location failure: ").append(
          100.0 * locationFailureStatistics.getMean()).append("\n");
    }
    s.append("  Number of TTFF reports: ").append(
        timeToFirstFixSecStatistics.getCount()).append("\n");
    if (timeToFirstFixSecStatistics.getCount() > 0) {
      s.append("  TTFF mean (sec): ").append(timeToFirstFixSecStatistics.getMean()).append("\n");
      s.append("  TTFF standard deviation (sec): ").append(
          timeToFirstFixSecStatistics.getStandardDeviation()).append("\n");
    }
    s.append("  Number of position accuracy reports: ").append(
        positionAccuracyMeterStatistics.getCount()).append("\n");
    if (positionAccuracyMeterStatistics.getCount() > 0) {
      s.append("  Position accuracy mean (m): ").append(
          positionAccuracyMeterStatistics.getMean()).append("\n");
      s.append("  Position accuracy standard deviation (m): ").append(
          positionAccuracyMeterStatistics.getStandardDeviation()).append("\n");
    }
    s.append("  Number of CN0 reports: ").append(
        topFourAverageCn0Statistics.getCount()).append("\n");
    if (topFourAverageCn0Statistics.getCount() > 0) {
      s.append("  Top 4 Avg CN0 mean (dB-Hz): ").append(
          topFourAverageCn0Statistics.getMean()).append("\n");
      s.append("  Top 4 Avg CN0 standard deviation (dB-Hz): ").append(
          topFourAverageCn0Statistics.getStandardDeviation()).append("\n");
    }
        s.append("  Used-in-fix constellation types: ");
        for (int i = 0; i < mConstellationTypes.length; i++) {
            if (mConstellationTypes[i]) {
                s.append(GnssStatus.constellationTypeToString(i)).append(" ");
            }
        }
        s.append("\n");
    s.append("GNSS_KPI_END").append("\n");
    GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats();
    if (stats != null) {
      s.append("Power Metrics").append("\n");
      s.append("  Time on battery (min): "
          + stats.getLoggingDurationMs() / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
      long[] t = stats.getTimeInGpsSignalQualityLevel();
      if (t != null && t.length == NUM_GPS_SIGNAL_QUALITY_LEVELS) {
        s.append("  Amount of time (while on battery) Top 4 Avg CN0 > " +
            Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) +
            " dB-Hz (min): ").append(t[1] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
        s.append("  Amount of time (while on battery) Top 4 Avg CN0 <= " +
            Double.toString(GnssPowerMetrics.POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) +
            " dB-Hz (min): ").append(t[0] / ((double) DateUtils.MINUTE_IN_MILLIS)).append("\n");
      }
      s.append("  Energy consumed while on battery (mAh): ").append(
          stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS)).append("\n");
    }
    s.append("Hardware Version: " + SystemProperties.get("ro.boot.revision", "")).append("\n");
    return s.toString();
  }

   /** Class for storing statistics */
  private class Statistics {

    /** Resets statistics */
    public void reset() {
      count = 0;
      sum = 0.0;
      sumSquare = 0.0;
    }

    /** Adds an item */
    public void addItem(double item) {
      count++;
      sum += item;
      sumSquare += item * item;
    }

    /** Returns number of items added */
    public int getCount() {
      return count;
    }

    /** Returns mean */
    public double getMean() {
      return sum/count;
    }

    /** Returns standard deviation */
    public double getStandardDeviation() {
      double m = sum/count;
      m = m * m;
      double v = sumSquare/count;
      if (v > m) {
        return Math.sqrt(v - m);
      }
      return 0;
    }

    private int count;
    private double sum;
    private double sumSquare;
  }

  /** Location failure statistics */
  private Statistics locationFailureStatistics;

  /** Time to first fix statistics */
  private Statistics timeToFirstFixSecStatistics;

  /** Position accuracy statistics */
  private Statistics positionAccuracyMeterStatistics;

  /** Top 4 average CN0 statistics */
  private Statistics topFourAverageCn0Statistics;

  /**
   * Resets GNSS metrics
   */
  private void reset() {
    StringBuilder s = new StringBuilder();
    TimeUtils.formatDuration(SystemClock.elapsedRealtimeNanos() / 1000000L, s);
    logStartInElapsedRealTime = s.toString();
    locationFailureStatistics.reset();
    timeToFirstFixSecStatistics.reset();
    positionAccuracyMeterStatistics.reset();
    topFourAverageCn0Statistics.reset();
        resetConstellationTypes();
    return;
  }

    /** Resets {@link #mConstellationTypes} as an all-false boolean array. */
    public void resetConstellationTypes() {
        mConstellationTypes = new boolean[GnssStatus.CONSTELLATION_COUNT];
    }

  /* Class for handling GNSS power related metrics */
  private class GnssPowerMetrics {

    /* Threshold for Top Four Average CN0 below which GNSS signal quality is declared poor */
    public static final double POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ = 20.0;

    /* Minimum change in Top Four Average CN0 needed to trigger a report */
    private static final double REPORTING_THRESHOLD_DB_HZ = 1.0;

    /* BatteryStats API */
    private final IBatteryStats mBatteryStats;

    /* Last reported Top Four Average CN0 */
    private double mLastAverageCn0;

    /* Last reported signal quality bin (based on Top Four Average CN0) */
    private int mLastSignalLevel;

    public GnssPowerMetrics(IBatteryStats stats) {
      mBatteryStats = stats;
      // Used to initialize the variable to a very small value (unachievable in practice) so that
      // the first CNO report will trigger an update to BatteryStats
      mLastAverageCn0 = -100.0;
      mLastSignalLevel = GPS_SIGNAL_QUALITY_UNKNOWN;
    }

    /**
     * Builds power metrics proto buf. This is included in the gnss proto buf.
     * @return PowerMetrics
     */
    public PowerMetrics buildProto() {
      PowerMetrics p = new PowerMetrics();
      GpsBatteryStats stats = mGnssPowerMetrics.getGpsBatteryStats();
      if (stats != null) {
        p.loggingDurationMs = stats.getLoggingDurationMs();
        p.energyConsumedMah = stats.getEnergyConsumedMaMs() / ((double) DateUtils.HOUR_IN_MILLIS);
        long[] t = stats.getTimeInGpsSignalQualityLevel();
        p.timeInSignalQualityLevelMs = new long[t.length];
        for (int i = 0; i < t.length; i++) {
          p.timeInSignalQualityLevelMs[i] = t[i];
        }
      }
      return p;
    }

    /**
     * Returns the GPS power stats
     * @return GpsBatteryStats
     */
    public GpsBatteryStats getGpsBatteryStats() {
      try {
        return mBatteryStats.getGpsBatteryStats();
      } catch (Exception e) {
        Log.w(TAG, "Exception", e);
        return null;
      }
    }

    /**
     * Reports signal quality to BatteryStats. Signal quality is based on Top four average CN0. If
     * the number of SVs seen is less than 4, then signal quality is the average CN0.
     * Changes are reported only if the average CN0 changes by more than REPORTING_THRESHOLD_DB_HZ.
     */
    public void reportSignalQuality(float[] ascendingCN0Array, int numSv) {
      double avgCn0 = 0.0;
      if (numSv > 0) {
        for (int i = Math.max(0, numSv - 4); i < numSv; i++) {
          avgCn0 += (double) ascendingCN0Array[i];
        }
        avgCn0 /= Math.min(numSv, 4);
      }
      if (Math.abs(avgCn0 - mLastAverageCn0) < REPORTING_THRESHOLD_DB_HZ) {
        return;
      }
      int signalLevel = getSignalLevel(avgCn0);
      if (signalLevel != mLastSignalLevel) {
        StatsLog.write(StatsLog.GPS_SIGNAL_QUALITY_CHANGED, signalLevel);
        mLastSignalLevel = signalLevel;
      }
      try {
        mBatteryStats.noteGpsSignalQuality(signalLevel);
        mLastAverageCn0 = avgCn0;
      } catch (Exception e) {
        Log.w(TAG, "Exception", e);
      }
      return;
    }

    /**
     * Obtains signal level based on CN0
     */
    private int getSignalLevel(double cn0) {
      if (cn0 > POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) {
        return GnssMetrics.GPS_SIGNAL_QUALITY_GOOD;
      }
      return GnssMetrics.GPS_SIGNAL_QUALITY_POOR;
    }
  }
}
