/**
 * Copyright (c) 2017-present, Stanislav Doskalenko - [email protected]
 * All rights reserved.
 *
 * This source code is licensed under the MIT-style license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * Based on Asim Malik android source code, copyright (c) 2015
 *
 **/

package com.reactnative.googlefit;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.fitness.Fitness;
import com.google.android.gms.fitness.data.Bucket;
import com.google.android.gms.fitness.data.DataPoint;
import com.google.android.gms.fitness.data.DataSet;
import com.google.android.gms.fitness.data.DataType;
import com.google.android.gms.fitness.data.Field;
import com.google.android.gms.fitness.data.DataSource;
import com.google.android.gms.fitness.request.DataSourcesRequest;
import com.google.android.gms.fitness.request.DataReadRequest;
import com.google.android.gms.fitness.result.DataReadResult;
import com.google.android.gms.fitness.result.DataSourcesResult;
import com.google.android.gms.fitness.data.Device;

import java.text.DateFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class StepHistory {

    private ReactContext mReactContext;
    private GoogleFitManager googleFitManager;

    private static final String TAG = "RNGoogleFit";

    public StepHistory(ReactContext reactContext, GoogleFitManager googleFitManager){
        this.mReactContext = reactContext;
        this.googleFitManager = googleFitManager;
    }

    public void getUserInputSteps(long startTime, long endTime, final Callback successCallback) {

        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        dateFormat.setTimeZone(TimeZone.getDefault());

        Log.i(TAG, "Range Start: " + dateFormat.format(startTime));
        Log.i(TAG, "Range End: " + dateFormat.format(endTime));

        final DataReadRequest readRequest = new DataReadRequest.Builder()
            .read(DataType.TYPE_STEP_COUNT_DELTA)
            .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
            .build();

        DataReadResult dataReadResult =
            Fitness.HistoryApi.readData(googleFitManager.getGoogleApiClient(), readRequest).await(1, TimeUnit.MINUTES);

        DataSet stepData = dataReadResult.getDataSet(DataType.TYPE_STEP_COUNT_DELTA);
    
        int userInputSteps = 0;

        for (DataPoint dp : stepData.getDataPoints()) {
            for(Field field : dp.getDataType().getFields()) {
                if("user_input".equals(dp.getOriginalDataSource().getStreamName())){
                    int steps = dp.getValue(field).asInt();
                    userInputSteps += steps;
                }
            }
        }
      
        successCallback.invoke(userInputSteps);
    }

    public void aggregateDataByDate(long startTime, long endTime, final Callback successCallback) {

        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        dateFormat.setTimeZone(TimeZone.getDefault());

        Log.i(TAG, "Range Start: " + dateFormat.format(startTime));
        Log.i(TAG, "Range End: " + dateFormat.format(endTime));

        final WritableArray results = Arguments.createArray();

        List<DataSource> dataSources = new ArrayList<>();

        // GoogleFit Apps
        dataSources.add(
            new DataSource.Builder()
                .setAppPackageName("com.google.android.gms")
                .setDataType(DataType.TYPE_STEP_COUNT_DELTA)
                .setType(DataSource.TYPE_DERIVED)
                .setStreamName("estimated_steps")
                .build()
        );

        // GoogleFit Apps
        dataSources.add(
            new DataSource.Builder()
                .setAppPackageName("com.google.android.gms")
                .setDataType(DataType.TYPE_STEP_COUNT_DELTA)
                .setType(DataSource.TYPE_DERIVED)
                .setStreamName("merge_step_deltas")
                .build()
        );

        // Mi Fit
        dataSources.add(
            new DataSource.Builder()
                .setAppPackageName("com.xiaomi.hm.health")
                .setDataType(DataType.TYPE_STEP_COUNT_DELTA)
                .setType(DataSource.TYPE_RAW)
                .setStreamName("")
                .build()
        );

        /*
        DataSourcesRequest sourceRequest = new DataSourcesRequest.Builder()
                .setDataTypes(DataType.TYPE_STEP_COUNT_DELTA,
                    DataType.TYPE_STEP_COUNT_CUMULATIVE,
                    DataType.AGGREGATE_STEP_COUNT_DELTA
                    )
                //.setDataSourceTypes(DataSource.TYPE_DERIVED)
                .build();
        DataSourcesResult dataSourcesResult =
           Fitness.SensorsApi.findDataSources(googleFitManager.getGoogleApiClient(), sourceRequest).await(1, TimeUnit.MINUTES);

        dataSources.addAll( dataSourcesResult.getDataSources() );
        */

        final AtomicInteger dataSourcesToLoad = new AtomicInteger(dataSources.size());

        for (DataSource dataSource : dataSources) {
            final WritableMap source = Arguments.createMap();

            DataType type = dataSource.getDataType();
            Device device = dataSource.getDevice();

            Log.i(TAG, "DataSource:");

            Log.i(TAG, "  + StreamID  : " + dataSource.getStreamIdentifier());
            source.putString("id", dataSource.getStreamIdentifier());

            if (dataSource.getAppPackageName() != null) {
                source.putString("appPackage", dataSource.getAppPackageName());
            } else {
                source.putNull("appPackage");
            }

            if (dataSource.getName() != null) {
                source.putString("name", dataSource.getName());
            } else {
                source.putNull("name");
            }

            if (dataSource.getStreamName() != null) {
                source.putString("stream", dataSource.getStreamName());
            } else {
                source.putNull("stream");
            }

            Log.i(TAG, "  + Type      : " + type);
            source.putString("type", type.getName());

            Log.i(TAG, "  + Device    : " + device);
            if (device != null) {
                source.putString("deviceManufacturer", device.getManufacturer());
                source.putString("deviceModel", device.getModel());
                switch(device.getType()) {
                    case Device.TYPE_CHEST_STRAP:
                        source.putString("deviceType", "chestStrap"); break;
                }
            } else {
                source.putNull("deviceManufacturer");
                source.putNull("deviceModel");
                source.putNull("deviceType");
            }

            //if (!DataType.TYPE_STEP_COUNT_DELTA.equals(type)) continue;
            DataReadRequest readRequest;

            List<DataType> aggregateDataTypeList = DataType.getAggregatesForInput(type);
            if (aggregateDataTypeList.size() > 0) {
                DataType aggregateType = aggregateDataTypeList.get(0);
                Log.i(TAG, "  + Aggregate : " + aggregateType);

                //Check how many steps were walked and recorded in specified days
                readRequest = new DataReadRequest.Builder()
                        .aggregate(dataSource
                            //DataType.TYPE_STEP_COUNT_DELTA
                            ,
                            //DataType.AGGREGATE_STEP_COUNT_DELTA
                            aggregateType)
                        .bucketByTime(12, TimeUnit.HOURS) // Half-day resolution
                        .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
                        .build();
            } else {
                readRequest = new DataReadRequest.Builder()
                        .read(dataSource)
                        //.bucketByTime(12, TimeUnit.HOURS) // Half-day resolution
                        .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
                        .build();
            }

            PendingResult<DataReadResult> readPendingResult = Fitness.HistoryApi.readData(googleFitManager.getGoogleApiClient(), readRequest);
            readPendingResult.setResultCallback(new ResultCallback<DataReadResult>() {
                @Override
                public void onResult(@NonNull DataReadResult dataReadResult) {
                    WritableArray steps = Arguments.createArray();

                    //Used for aggregated data
                    if (dataReadResult.getBuckets().size() > 0) {
                        Log.i(TAG, "  +++ Number of buckets: " + dataReadResult.getBuckets().size());
                        for (Bucket bucket : dataReadResult.getBuckets()) {
                            List<DataSet> dataSets = bucket.getDataSets();
                            for (DataSet dataSet : dataSets) {
                                processDataSet(dataSet, steps);
                            }
                        }
                    }

                    //Used for non-aggregated data
                    if (dataReadResult.getDataSets().size() > 0) {
                        Log.i(TAG, "  +++ Number of returned DataSets: " + dataReadResult.getDataSets().size());
                        for (DataSet dataSet : dataReadResult.getDataSets()) {
                            processDataSet(dataSet, steps);
                        }
                    }

                    WritableMap map = Arguments.createMap();
                    map.putMap("source", source);
                    map.putArray("steps", steps);
                    results.pushMap(map);

                    if (dataSourcesToLoad.decrementAndGet() <= 0) {
                        successCallback.invoke(results);
                    }
                }
            }, 1, TimeUnit.MINUTES);
        }
    }

    //Will be deprecated in future releases
    public void displayLastWeeksData(long startTime, long endTime) {
        DateFormat dateFormat = DateFormat.getDateInstance();
        //Log.i(TAG, "Range Start: " + dateFormat.format(startTime));
        //Log.i(TAG, "Range End: " + dateFormat.format(endTime));

        //Check how many steps were walked and recorded in the last 7 days
        DataReadRequest readRequest = new DataReadRequest.Builder()
                .aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
                .bucketByTime(1, TimeUnit.DAYS)
                .setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
                .build();

        DataReadResult dataReadResult = Fitness.HistoryApi.readData(googleFitManager.getGoogleApiClient(), readRequest).await(1, TimeUnit.MINUTES);

        WritableArray map = Arguments.createArray();

        //Used for aggregated data
        if (dataReadResult.getBuckets().size() > 0) {
            Log.i(TAG, "Number of buckets: " + dataReadResult.getBuckets().size());
            for (Bucket bucket : dataReadResult.getBuckets()) {
                List<DataSet> dataSets = bucket.getDataSets();
                for (DataSet dataSet : dataSets) {
                    processDataSet(dataSet, map);
                }
            }
        }
        //Used for non-aggregated data
        else if (dataReadResult.getDataSets().size() > 0) {
            Log.i(TAG, "Number of returned DataSets: " + dataReadResult.getDataSets().size());
            for (DataSet dataSet : dataReadResult.getDataSets()) {
                processDataSet(dataSet, map);
            }
        }

        sendEvent(this.mReactContext, "StepHistoryChangedEvent", map);
    }

    private void processDataSet(DataSet dataSet, WritableArray map) {
        //Log.i(TAG, "Data returned for Data type: " + dataSet.getDataType().getName());

        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        dateFormat.setTimeZone(TimeZone.getDefault());

        WritableMap stepMap = Arguments.createMap();

        for (DataPoint dp : dataSet.getDataPoints()) {
            Log.i(TAG, "\tData point:");
            Log.i(TAG, "\t\tType : " + dp.getDataType().getName());
            Log.i(TAG, "\t\tStart: " + dateFormat.format(dp.getStartTime(TimeUnit.MILLISECONDS)));
            Log.i(TAG, "\t\tEnd  : " + dateFormat.format(dp.getEndTime(TimeUnit.MILLISECONDS)));

            for(Field field : dp.getDataType().getFields()) {
                Log.i(TAG, "\t\tField: " + field.getName() +
                        " Value: " + dp.getValue(field));

                stepMap.putDouble("startDate", dp.getStartTime(TimeUnit.MILLISECONDS));
                stepMap.putDouble("endDate", dp.getEndTime(TimeUnit.MILLISECONDS));
                stepMap.putDouble("steps", dp.getValue(field).asInt());
                map.pushMap(stepMap);
            }
        }
    }


    private void sendEvent(ReactContext reactContext,
                           String eventName,
                           @Nullable WritableArray params) {
        reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
    }

}