/*
 * Copyright (C) 2014 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.hardware.cts.helpers;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.cts.helpers.sensoroperations.SensorOperation;
import android.os.Environment;
import android.util.Log;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Class used to store stats related to {@link SensorOperation}s. Sensor stats may be linked
 * together so that they form a tree.
 */
public class SensorStats {
    private static final String TAG = "SensorStats";
    public static final String DELIMITER = "__";

    public static final String ERROR = "error";
    public static final String EVENT_FIFO_LENGTH = "event_fifo_length_observed";
    public static final String EVENT_GAP_COUNT_KEY = "event_gap_count";
    public static final String EVENT_GAP_POSITIONS_KEY = "event_gap_positions";
    public static final String EVENT_OUT_OF_ORDER_COUNT_KEY = "event_out_of_order_count";
    public static final String EVENT_OUT_OF_ORDER_POSITIONS_KEY = "event_out_of_order_positions";
    public static final String EVENT_TIME_SYNCHRONIZATION_COUNT_KEY =
            "event_time_synchronization_count";
    public static final String EVENT_TIME_SYNCHRONIZATION_POSITIONS_KEY =
            "event_time_synchronization_positions";
    public static final String EVENT_TIME_WRONG_CLOCKSOURCE_COUNT_KEY =
            "event_time_wrong_clocksource_count";
    public static final String EVENT_TIME_WRONG_CLOCKSOURCE_POSITIONS_KEY =
            "event_time_wrong_clocksource_positions";
    public static final String EVENT_COUNT_KEY = "event_count";
    public static final String EVENT_COUNT_EXPECTED_KEY = "event_count_expected";
    public static final String EVENT_NOT_SANITIZED_KEY = "event_not_sanitized";
    public static final String EVENT_LOG_FILENAME = "event_log_filename";
    public static final String WRONG_SENSOR_KEY = "wrong_sensor_observed";
    public static final String FREQUENCY_KEY = "frequency";
    public static final String JITTER_95_PERCENTILE_PERCENT_KEY = "jitter_95_percentile_percent";
    public static final String MEAN_KEY = "mean";
    public static final String STANDARD_DEVIATION_KEY = "standard_deviation";
    public static final String MAGNITUDE_KEY = "magnitude";
    public static final String DELAYED_BATCH_DELIVERY = "delayed_batch_delivery";
    public static final String INITIAL_MEAN_KEY = "initial_mean";
    public static final String LATER_MEAN_KEY = "later_mean";

    private final Map<String, Object> mValues = new HashMap<>();
    private final Map<String, SensorStats> mSensorStats = new HashMap<>();

    /**
     * Add a value.
     *
     * @param key the key.
     * @param value the value as an {@link Object}.
     */
    public synchronized void addValue(String key, Object value) {
        if (value == null) {
            return;
        }
        mValues.put(key, value);
    }

    /**
     * Add a nested {@link SensorStats}. This is useful for keeping track of stats in a
     * {@link SensorOperation} tree.
     *
     * @param key the key
     * @param stats the sub {@link SensorStats} object.
     */
    public synchronized void addSensorStats(String key, SensorStats stats) {
        if (stats == null) {
            return;
        }
        mSensorStats.put(key, stats);
    }

    /**
     * Get the keys from the values table. Will not get the keys from the nested
     * {@link SensorStats}.
     */
    public synchronized Set<String> getKeys() {
        return mValues.keySet();
    }

    /**
     * Get a value from the values table. Will not attempt to get values from nested
     * {@link SensorStats}.
     */
    public synchronized Object getValue(String key) {
        return mValues.get(key);
    }

    /**
     * Flattens the map and all sub {@link SensorStats} objects. Keys will be flattened using
     * {@value #DELIMITER}. For example, if a sub {@link SensorStats} is added with key
     * {@code "key1"} containing the key value pair {@code \("key2", "value"\)}, the flattened map
     * will contain the entry {@code \("key1__key2", "value"\)}.
     *
     * @return a {@link Map} containing all stats from the value and sub {@link SensorStats}.
     */
    public synchronized Map<String, Object> flatten() {
        final Map<String, Object> flattenedMap = new HashMap<>(mValues);
        for (Entry<String, SensorStats> statsEntry : mSensorStats.entrySet()) {
            for (Entry<String, Object> valueEntry : statsEntry.getValue().flatten().entrySet()) {
                String key = statsEntry.getKey() + DELIMITER + valueEntry.getKey();
                flattenedMap.put(key, valueEntry.getValue());
            }
        }
        return flattenedMap;
    }

    /**
     * Utility method to log the stats to the logcat.
     */
    public void log(String tag) {
        final Map<String, Object> flattened = flatten();
        for (String key : getSortedKeys(flattened)) {
            Object value = flattened.get(key);
            Log.v(tag, String.format("%s: %s", key, getValueString(value)));
        }
    }

    /* Checks if external storage is available for read and write */
    private boolean isExternalStorageWritable() {
        String state = Environment.getExternalStorageState();
        return Environment.MEDIA_MOUNTED.equals(state);
    }

    /**
     * Utility method to log the stats to a file. Will overwrite the file if it already exists.
     */
    public void logToFile(Context context, String fileName) throws IOException {
        if (!isExternalStorageWritable()) {
            Log.w(TAG,
                "External storage unavailable, skipping log to file: " + fileName);
            return;
        }

        try {
            // Only log to file if currently not an Instant App since Instant Apps do not have access to
            // external storage.
            if (!context.getPackageManager().isInstantApp()) {
                File statsDirectory = SensorCtsHelper.getSensorTestDataDirectory("stats/");
                File logFile = new File(statsDirectory, fileName);
                final Map<String, Object> flattened = flatten();
                FileWriter fileWriter = new FileWriter(logFile, false /* append */);
                try (BufferedWriter writer = new BufferedWriter(fileWriter)) {
                    for (String key : getSortedKeys(flattened)) {
                        Object value = flattened.get(key);
                        writer.write(String.format("%s: %s\n", key, getValueString(value)));
                    }
                }
            }
        } catch(IOException e) {
            Log.w(TAG, "Unable to write to file: " + fileName, e);
        }
    }

    /**
     * Provides a sanitized sensor name, that can be used in file names.
     * See {@link #logToFile(String)}.
     */
    public static String getSanitizedSensorName(Sensor sensor) throws SensorTestPlatformException {
        return SensorCtsHelper.sanitizeStringForFileName(sensor.getStringType());
    }

    private static List<String> getSortedKeys(Map<String, Object> flattenedStats) {
        List<String> keys = new ArrayList<>(flattenedStats.keySet());
        Collections.sort(keys);
        return keys;
    }

    private static String getValueString(Object value) {
        if (value == null) {
            return "";
        } else if (value instanceof boolean[]) {
            return Arrays.toString((boolean[]) value);
        } else if (value instanceof byte[]) {
            return Arrays.toString((byte[]) value);
        } else if (value instanceof char[]) {
            return Arrays.toString((char[]) value);
        } else if (value instanceof double[]) {
            return Arrays.toString((double[]) value);
        } else if (value instanceof float[]) {
            return Arrays.toString((float[]) value);
        } else if (value instanceof int[]) {
            return Arrays.toString((int[]) value);
        } else if (value instanceof long[]) {
            return Arrays.toString((long[]) value);
        } else if (value instanceof short[]) {
            return Arrays.toString((short[]) value);
        } else if (value instanceof Object[]) {
            return Arrays.toString((Object[]) value);
        } else {
            return value.toString();
        }
    }
}
