package net.heartshield.sensors;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import java.util.List;

/**
 * Accelerometer time series signal capture.
 *
 * Events are delivered to the main thread, but data handling occurs in a separate thread.
 *
 * @author David Madl (git@abanbytes.eu)
 * @date 2016-03-21
 */
public class AccelerationSensor implements ISensor, SensorEventListener {
    private SensorManager mSensorManager;
    private Sensor mSensor;
    private AccelerationHandler mBackgroundHandlerThread;
    private Handler mBackgroundHandler;
    private State mState;
    private final Object mStateLock = new Object();

    private long mPrevTimestamp; // in nanoseconds
    private long mMinDataEventTime; // in nanoseconds
    private int mPrevOffset;

    private Handler mHandler;
    private Double mFps;
    private SensorData mData;
    private ISensor.Listener mListener = null;

    private static final int NDIM = 3; // x,y,z channels

    /**
     * @param context Android Context used to get the sensor service
     * @param maxDataEventRate maximum rate of onData() events emitted per second
     */
    public AccelerationSensor(Context context, double maxDataEventRate) {
        mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        mFps = 1e6 / (double) mSensor.getMinDelay();
        mMinDataEventTime = (long) (1e9 / maxDataEventRate);

        mBackgroundHandlerThread = new AccelerationHandler();
        //mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper());
        mBackgroundHandlerThread.start();

        mHandler = new Handler(Looper.getMainLooper());
        mData = new SensorData(NDIM);
        mState = State.STOPPED;
    }

    /**
     * Strange that this is needed. No Looper is found if we try this:
     *
     * <pre>
     * // WRONG:
     * mBackgroundHandlerThread = new HandlerThread("AccelerationSensor");
     * mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper());
     * </pre>
     */
    private class AccelerationHandler extends Thread {
        @Override
        public void run() {
            Looper.prepare();
            mBackgroundHandler = new Handler();
            Looper.loop();
        }
    }

    private void emitOnState(final ISensor.State newState) {
        State oldState;
        synchronized(mStateLock) {
            oldState = mState;
            mState = newState;
        }
        if(mListener == null)
            return;

        if(!newState.equals(oldState)) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mListener.onState(newState);
                }
            });
        }
    }

    private long mRefTimestamp;
    private long mStartTimeNanos;

    @Override
    public void onSensorChanged(SensorEvent event) {
        State state;
        synchronized(mStateLock) {
            // ignore possible spurious callbacks still in the queue
            if (mState == State.STOPPED)
                return;
            state = mState;
        }

        if(state == State.STARTING) {
            mRefTimestamp = event.timestamp;
            mStartTimeNanos = System.nanoTime();
            emitOnState(State.RUNNING);
        }

        mData.addSample(new double[]{
                // convert to nanoTime as best as we can
                ((double) (event.timestamp - mRefTimestamp + mStartTimeNanos)) / 1e9,
                event.values[0],
                event.values[1],
                event.values[2]
        });

        if((event.timestamp - mPrevTimestamp) > mMinDataEventTime) {
            final int offset = mPrevOffset;
            if(mListener != null) {
                final int length = mData.length() - offset;
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mListener.onData(offset, length);
                    }
                });
            }
            mPrevTimestamp = event.timestamp;
            mPrevOffset = mData.length();
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {}

    /////

    /**
     * Nominal number of sample values per second.
     * Only valid after the first call to {@link ISensor#start}
     */
    @Override
    public double getFps() { return mFps; }

    /** start measurement, resetting sensor data */
    @Override
    public void start() {
        synchronized(mStateLock) {
            if(mState != State.STOPPED && mState != State.FAILED)
                throw new IllegalStateException("start() called in invalid state: " + mState.toString());
            mState = State.STARTING;
        }

        mData.clear();
        mPrevTimestamp = 0;
        mPrevOffset = 0;

        mRefTimestamp = 0;
        mStartTimeNanos = 0;

        final int maxReportLatencyUs = (int) (mMinDataEventTime / 1000); // hopefully save some CPU

        mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_FASTEST, maxReportLatencyUs, mBackgroundHandler);
    }

    /** stop measurement, keeping sensor data */
    @Override
    public void stop() {
        mSensorManager.unregisterListener(this);
        emitOnState(State.STOPPED);
    }

    /**
     * Time series of each dimension: e.g. {{t1, t2, ...}, {x1, x2, ...}}
     * Deep copies are returned.
     *
     * @return sensor data recorded since {@link ISensor#start} was called
     */
    @Override
    public List<double[]> getSamples() { return mData.getSamples(); }

    /** @return number of samples */
    @Override
    public int length() { return mData.length(); }

    /** events are delivered to the main thread, but pixel handling occurs in a separate thread */
    @Override
    public void setListener(ISensor.Listener listener) { mListener = listener; }

    /** obtain details after FAILED state */
    @Override
    public Exception getError() { return null; }
}
