/*
 * Copyright (C) 2017 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.googlecode.android_scripting.facade;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;

import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcDefault;
import com.googlecode.android_scripting.rpc.RpcDeprecated;
import com.googlecode.android_scripting.rpc.RpcParameter;
import com.googlecode.android_scripting.rpc.RpcStartEvent;
import com.googlecode.android_scripting.rpc.RpcStopEvent;

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

/**
 * Exposes the SensorManager related functionality. <br>
 * <br>
 * <b>Guidance notes</b> <br>
 * For reasons of economy the sensors on smart phones are usually low cost and, therefore, low
 * accuracy (usually represented by 10 bit data). The floating point data values obtained from
 * sensor readings have up to 16 decimal places, the majority of which are noise. On many phones the
 * accelerometer is limited (by the phone manufacturer) to a maximum reading of 2g. The magnetometer
 * (which also provides orientation readings) is strongly affected by the presence of ferrous metals
 * and can give large errors in vehicles, on board ship etc.
 *
 * Following a startSensingTimed(A,B) api call sensor events are entered into the Event Queue (see
 * EventFacade). For the A parameter: 1 = All Sensors, 2 = Accelerometer, 3 = Magnetometer and 4 =
 * Light. The B parameter is the minimum delay between recordings in milliseconds. To avoid
 * duplicate readings the minimum delay should be 20 milliseconds. The light sensor will probably be
 * much slower (taking about 1 second to register a change in light level). Note that if the light
 * level is constant no sensor events will be registered by the light sensor.
 *
 * Following a startSensingThreshold(A,B,C) api call sensor events greater than a given threshold
 * are entered into the Event Queue. For the A parameter: 1 = Orientation, 2 = Accelerometer, 3 =
 * Magnetometer and 4 = Light. The B parameter is the integer value of the required threshold level.
 * For orientation sensing the integer threshold value is in milliradians. Since orientation events
 * can exceed the threshold value for long periods only crossing and return events are recorded. The
 * C parameter is the required axis (XYZ) of the sensor: 0 = No axis, 1 = X, 2 = Y, 3 = X+Y, 4 = Z,
 * 5= X+Z, 6 = Y+Z, 7 = X+Y+Z. For orientation X = azimuth, Y = pitch and Z = roll. <br>
 *
 * <br>
 * <b>Example (python)</b>
 *
 * <pre>
 * import android, time
 * droid = android.Android()
 * droid.startSensingTimed(1, 250)
 * time.sleep(1)
 * s1 = droid.readSensors().result
 * s2 = droid.sensorsGetAccuracy().result
 * s3 = droid.sensorsGetLight().result
 * s4 = droid.sensorsReadAccelerometer().result
 * s5 = droid.sensorsReadMagnetometer().result
 * s6 = droid.sensorsReadOrientation().result
 * droid.stopSensing()
 * </pre>
 *
 * Returns:<br>
 * s1 = {u'accuracy': 3, u'pitch': -0.47323511242866517, u'xmag': 1.75, u'azimuth':
 * -0.26701245009899138, u'zforce': 8.4718560000000007, u'yforce': 4.2495484000000001, u'time':
 * 1297160391.2820001, u'ymag': -8.9375, u'zmag': -41.0625, u'roll': -0.031366908922791481,
 * u'xforce': 0.23154590999999999}<br>
 * s2 = 3 (Highest accuracy)<br>
 * s3 = None ---(not available on many phones)<br>
 * s4 = [0.23154590999999999, 4.2495484000000001, 8.4718560000000007] ----(x, y, z accelerations)<br>
 * s5 = [1.75, -8.9375, -41.0625] -----(x, y, z magnetic readings)<br>
 * s6 = [-0.26701245009899138, -0.47323511242866517, -0.031366908922791481] ---(azimuth, pitch, roll
 * in radians)<br>
 *
 */
public class SensorManagerFacade extends RpcReceiver {
  private final EventFacade mEventFacade;
  private final SensorManager mSensorManager;

  private volatile Bundle mSensorReadings;

  private volatile Integer mAccuracy;
  private volatile Integer mSensorNumber;
  private volatile Integer mXAxis = 0;
  private volatile Integer mYAxis = 0;
  private volatile Integer mZAxis = 0;
  private volatile Integer mThreshing = 0;
  private volatile Integer mThreshOrientation = 0;
  private volatile Integer mXCrossed = 0;
  private volatile Integer mYCrossed = 0;
  private volatile Integer mZCrossed = 0;

  private volatile Float mThreshold;
  private volatile Float mXForce;
  private volatile Float mYForce;
  private volatile Float mZForce;

  private volatile Float mXMag;
  private volatile Float mYMag;
  private volatile Float mZMag;

  private volatile Float mLight;

  private volatile Double mAzimuth;
  private volatile Double mPitch;
  private volatile Double mRoll;

  private volatile Long mLastTime;
  private volatile Long mDelayTime;

  private SensorEventListener mSensorListener;

  public SensorManagerFacade(FacadeManager manager) {
    super(manager);
    mEventFacade = manager.getReceiver(EventFacade.class);
    mSensorManager = (SensorManager) manager.getService().getSystemService(Context.SENSOR_SERVICE);
  }

  @Rpc(description = "Starts recording sensor data to be available for polling.")
  @RpcStartEvent("sensors")
  public void startSensingTimed(
      @RpcParameter(name = "sensorNumber", description = "1 = All, 2 = Accelerometer, 3 = Magnetometer and 4 = Light") Integer sensorNumber,
      @RpcParameter(name = "delayTime", description = "Minimum time between readings in milliseconds") Integer delayTime) {
    mSensorNumber = sensorNumber;
    if (delayTime < 20) {
      delayTime = 20;
    }
    mDelayTime = (long) (delayTime);
    mLastTime = System.currentTimeMillis();
    if (mSensorListener == null) {
      mSensorListener = new SensorValuesCollector();
      mSensorReadings = new Bundle();
      switch (mSensorNumber) {
      case 1:
        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ALL)) {
          mSensorManager.registerListener(mSensorListener, sensor,
              SensorManager.SENSOR_DELAY_FASTEST);
        }
        break;
      case 2:
        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER)) {
          mSensorManager.registerListener(mSensorListener, sensor,
              SensorManager.SENSOR_DELAY_FASTEST);
        }
        break;
      case 3:
        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_MAGNETIC_FIELD)) {
          mSensorManager.registerListener(mSensorListener, sensor,
              SensorManager.SENSOR_DELAY_FASTEST);
        }
        break;
      case 4:
        for (Sensor sensor : mSensorManager.getSensorList(Sensor.TYPE_LIGHT)) {
          mSensorManager.registerListener(mSensorListener, sensor,
              SensorManager.SENSOR_DELAY_FASTEST);
        }
      }
    }
  }

  @Rpc(description = "Records to the Event Queue sensor data exceeding a chosen threshold.")
  @RpcStartEvent("threshold")
  public void startSensingThreshold(

      @RpcParameter(name = "sensorNumber", description = "1 = Orientation, 2 = Accelerometer, 3 = Magnetometer and 4 = Light") Integer sensorNumber,
      @RpcParameter(name = "threshold", description = "Threshold level for chosen sensor (integer)") Integer threshold,
      @RpcParameter(name = "axis", description = "0 = No axis, 1 = X, 2 = Y, 3 = X+Y, 4 = Z, 5= X+Z, 6 = Y+Z, 7 = X+Y+Z") Integer axis) {
    mSensorNumber = sensorNumber;
    mXAxis = axis & 1;
    mYAxis = axis & 2;
    mZAxis = axis & 4;
    if (mSensorNumber == 1) {
      mThreshing = 0;
      mThreshOrientation = 1;
      mThreshold = ((float) threshold) / ((float) 1000);
    } else {
      mThreshing = 1;
      mThreshold = (float) threshold;
    }
    startSensingTimed(mSensorNumber, 20);
  }

  @Rpc(description = "Returns the most recently recorded sensor data.")
  public Bundle readSensors() {
    if (mSensorReadings == null) {
      return null;
    }
    synchronized (mSensorReadings) {
      return new Bundle(mSensorReadings);
    }
  }

  @Rpc(description = "Stops collecting sensor data.")
  @RpcStopEvent("sensors")
  public void stopSensing() {
    mSensorManager.unregisterListener(mSensorListener);
    mSensorListener = null;
    mSensorReadings = null;
    mThreshing = 0;
    mThreshOrientation = 0;
  }

  @Rpc(description = "Returns the most recently received accuracy value.")
  public Integer sensorsGetAccuracy() {
    return mAccuracy;
  }

  @Rpc(description = "Returns the most recently received light value.")
  public Float sensorsGetLight() {
    return mLight;
  }

  @Rpc(description = "Returns the most recently received accelerometer values.", returns = "a List of Floats [(acceleration on the) X axis, Y axis, Z axis].")
  public List<Float> sensorsReadAccelerometer() {
    synchronized (mSensorReadings) {
      return Arrays.asList(mXForce, mYForce, mZForce);
    }
  }

  @Rpc(description = "Returns the most recently received magnetic field values.", returns = "a List of Floats [(magnetic field value for) X axis, Y axis, Z axis].")
  public List<Float> sensorsReadMagnetometer() {
    synchronized (mSensorReadings) {
      return Arrays.asList(mXMag, mYMag, mZMag);
    }
  }

  @Rpc(description = "Returns the most recently received orientation values.", returns = "a List of Doubles [azimuth, pitch, roll].")
  public List<Double> sensorsReadOrientation() {
    synchronized (mSensorReadings) {
      return Arrays.asList(mAzimuth, mPitch, mRoll);
    }
  }

  @Rpc(description = "Starts recording sensor data to be available for polling.")
  @RpcDeprecated(value = "startSensingTimed or startSensingThreshhold", release = "4")
  public void startSensing(
      @RpcParameter(name = "sampleSize", description = "number of samples for calculating average readings") @RpcDefault("5") Integer sampleSize) {
    if (mSensorListener == null) {
      startSensingTimed(1, 220);
    }
  }

  @Override
  public void shutdown() {
    stopSensing();
  }

  private class SensorValuesCollector implements SensorEventListener {
    private final static int MATRIX_SIZE = 9;

    private final RollingAverage mmAzimuth;
    private final RollingAverage mmPitch;
    private final RollingAverage mmRoll;

    private float[] mmGeomagneticValues;
    private float[] mmGravityValues;
    private float[] mmR;
    private float[] mmOrientation;

    public SensorValuesCollector() {
      mmAzimuth = new RollingAverage();
      mmPitch = new RollingAverage();
      mmRoll = new RollingAverage();
    }

    private void postEvent() {
      mSensorReadings.putDouble("time", System.currentTimeMillis() / 1000.0);
      mEventFacade.postEvent("sensors", mSensorReadings.clone());
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
      if (mSensorReadings == null) {
        return;
      }
      synchronized (mSensorReadings) {
        mSensorReadings.putInt("accuracy", accuracy);
        mAccuracy = accuracy;

      }
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
      if (mSensorReadings == null) {
        return;
      }
      synchronized (mSensorReadings) {
        switch (event.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER:
          mXForce = event.values[0];
          mYForce = event.values[1];
          mZForce = event.values[2];
          if (mThreshing == 0) {
            mSensorReadings.putFloat("xforce", mXForce);
            mSensorReadings.putFloat("yforce", mYForce);
            mSensorReadings.putFloat("zforce", mZForce);
            if ((mSensorNumber == 2) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
              mLastTime = System.currentTimeMillis();
              postEvent();
            }
          }
          if ((mThreshing == 1) && (mSensorNumber == 2)) {
            if ((Math.abs(mXForce) > mThreshold) && (mXAxis == 1)) {
              mSensorReadings.putFloat("xforce", mXForce);
              postEvent();
            }

            if ((Math.abs(mYForce) > mThreshold) && (mYAxis == 2)) {
              mSensorReadings.putFloat("yforce", mYForce);
              postEvent();
            }

            if ((Math.abs(mZForce) > mThreshold) && (mZAxis == 4)) {
              mSensorReadings.putFloat("zforce", mZForce);
              postEvent();
            }
          }

          mmGravityValues = event.values.clone();
          break;
        case Sensor.TYPE_MAGNETIC_FIELD:
          mXMag = event.values[0];
          mYMag = event.values[1];
          mZMag = event.values[2];
          if (mThreshing == 0) {
            mSensorReadings.putFloat("xMag", mXMag);
            mSensorReadings.putFloat("yMag", mYMag);
            mSensorReadings.putFloat("zMag", mZMag);
            if ((mSensorNumber == 3) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
              mLastTime = System.currentTimeMillis();
              postEvent();
            }
          }
          if ((mThreshing == 1) && (mSensorNumber == 3)) {
            if ((Math.abs(mXMag) > mThreshold) && (mXAxis == 1)) {
              mSensorReadings.putFloat("xforce", mXMag);
              postEvent();
            }
            if ((Math.abs(mYMag) > mThreshold) && (mYAxis == 2)) {
              mSensorReadings.putFloat("yforce", mYMag);
              postEvent();
            }
            if ((Math.abs(mZMag) > mThreshold) && (mZAxis == 4)) {
              mSensorReadings.putFloat("zforce", mZMag);
              postEvent();
            }
          }
          mmGeomagneticValues = event.values.clone();
          break;
        case Sensor.TYPE_LIGHT:
          mLight = event.values[0];
          if (mThreshing == 0) {
            mSensorReadings.putFloat("light", mLight);
            if ((mSensorNumber == 4) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
              mLastTime = System.currentTimeMillis();
              postEvent();
            }
          }
          if ((mThreshing == 1) && (mSensorNumber == 4)) {
            if (mLight > mThreshold) {
              mSensorReadings.putFloat("light", mLight);
              postEvent();
            }
          }
          break;

        }
        if (mSensorNumber == 1) {
          if (mmGeomagneticValues != null && mmGravityValues != null) {
            if (mmR == null) {
              mmR = new float[MATRIX_SIZE];
            }
            if (SensorManager.getRotationMatrix(mmR, null, mmGravityValues, mmGeomagneticValues)) {
              if (mmOrientation == null) {
                mmOrientation = new float[3];
              }
              SensorManager.getOrientation(mmR, mmOrientation);
              mmAzimuth.add(mmOrientation[0]);
              mmPitch.add(mmOrientation[1]);
              mmRoll.add(mmOrientation[2]);

              mAzimuth = mmAzimuth.get();
              mPitch = mmPitch.get();
              mRoll = mmRoll.get();
              if (mThreshOrientation == 0) {
                mSensorReadings.putDouble("azimuth", mAzimuth);
                mSensorReadings.putDouble("pitch", mPitch);
                mSensorReadings.putDouble("roll", mRoll);
                if ((mSensorNumber == 1) && (System.currentTimeMillis() > (mDelayTime + mLastTime))) {
                  mLastTime = System.currentTimeMillis();
                  postEvent();
                }
              }
              if ((mThreshOrientation == 1) && (mSensorNumber == 1)) {
                if ((mXAxis == 1) && (mXCrossed == 0)) {
                  if (Math.abs(mAzimuth) > ((double) mThreshold)) {
                    mSensorReadings.putDouble("azimuth", mAzimuth);
                    postEvent();
                    mXCrossed = 1;
                  }
                }
                if ((mXAxis == 1) && (mXCrossed == 1)) {
                  if (Math.abs(mAzimuth) < ((double) mThreshold)) {
                    mSensorReadings.putDouble("azimuth", mAzimuth);
                    postEvent();
                    mXCrossed = 0;
                  }
                }
                if ((mYAxis == 2) && (mYCrossed == 0)) {
                  if (Math.abs(mPitch) > ((double) mThreshold)) {
                    mSensorReadings.putDouble("pitch", mPitch);
                    postEvent();
                    mYCrossed = 1;
                  }
                }
                if ((mYAxis == 2) && (mYCrossed == 1)) {
                  if (Math.abs(mPitch) < ((double) mThreshold)) {
                    mSensorReadings.putDouble("pitch", mPitch);
                    postEvent();
                    mYCrossed = 0;
                  }
                }
                if ((mZAxis == 4) && (mZCrossed == 0)) {
                  if (Math.abs(mRoll) > ((double) mThreshold)) {
                    mSensorReadings.putDouble("roll", mRoll);
                    postEvent();
                    mZCrossed = 1;
                  }
                }
                if ((mZAxis == 4) && (mZCrossed == 1)) {
                  if (Math.abs(mRoll) < ((double) mThreshold)) {
                    mSensorReadings.putDouble("roll", mRoll);
                    postEvent();
                    mZCrossed = 0;
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  static class RollingAverage {
    private final int mmSampleSize;
    private final double mmData[];
    private int mmIndex = 0;
    private boolean mmFilled = false;
    private double mmSum = 0.0;

    public RollingAverage() {
      mmSampleSize = 5;
      mmData = new double[mmSampleSize];
    }

    public void add(double value) {
      mmSum -= mmData[mmIndex];
      mmData[mmIndex] = value;
      mmSum += mmData[mmIndex];
      ++mmIndex;
      mmIndex %= mmSampleSize;
      mmFilled = (!mmFilled) ? mmIndex == 0 : mmFilled;
    }

    public double get() throws IllegalStateException {
      if (!mmFilled && mmIndex == 0) {
        throw new IllegalStateException("No values to average.");
      }
      return (mmFilled) ? (mmSum / mmSampleSize) : (mmSum / mmIndex);
    }
  }
}
