/*
 * Copyright (C) 2020 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.systemui.util.sensors;

import android.content.res.Resources;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.util.Assert;

import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;

class ThresholdSensorImpl implements ThresholdSensor {
    private static final String TAG = "ThresholdSensor";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final AsyncSensorManager mSensorManager;
    private final Sensor mSensor;
    private final float mThreshold;
    private boolean mRegistered;
    private boolean mPaused;
    private List<Listener> mListeners = new ArrayList<>();
    private Boolean mLastBelow;
    private String mTag;
    private final float mThresholdLatch;
    private int mSensorDelay;

    private SensorEventListener mSensorEventListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            boolean below = event.values[0] < mThreshold;
            boolean above = event.values[0] >= mThresholdLatch;
            logDebug("Sensor value: " + event.values[0]);
            onSensorEvent(below, above, event.timestamp);
        }

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

    private ThresholdSensorImpl(AsyncSensorManager sensorManager,
            Sensor sensor, float threshold, float thresholdLatch, int sensorDelay) {
        mSensorManager = sensorManager;
        mSensor = sensor;
        mThreshold = threshold;
        mThresholdLatch = thresholdLatch;
        mSensorDelay = sensorDelay;
    }

    @Override
    public void setTag(String tag) {
        mTag = tag;
    }

    @Override
    public void setDelay(int delay) {
        if (delay == mSensorDelay) {
            return;
        }

        mSensorDelay = delay;
        if (isLoaded()) {
            unregisterInternal();
            registerInternal();
        }
    }

    @Override
    public boolean isLoaded() {
        return mSensor != null;
    }

    @VisibleForTesting
    boolean isRegistered() {
        return mRegistered;
    }

    /**
     * Registers the listener with the sensor.
     *
     * Multiple listeners are not supported at this time.
     *
     * Returns true if the listener was successfully registered. False otherwise.
     */
    @Override
    public void register(Listener listener) {
        Assert.isMainThread();
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
        registerInternal();
    }

    @Override
    public void unregister(Listener listener) {
        Assert.isMainThread();
        mListeners.remove(listener);
        unregisterInternal();
    }

    /**
     * Unregister with the {@link SensorManager} without unsetting listeners on this object.
     */
    @Override
    public void pause() {
        Assert.isMainThread();
        mPaused = true;
        unregisterInternal();
    }

    /**
     * Register with the {@link SensorManager}. No-op if no listeners are registered on this object.
     */
    @Override
    public void resume() {
        Assert.isMainThread();
        mPaused = false;
        registerInternal();
    }

    private void alertListenersInternal(boolean below, long timestampNs) {
        List<Listener> listeners = new ArrayList<>(mListeners);
        listeners.forEach(listener ->
                listener.onThresholdCrossed(new ThresholdSensorEvent(below, timestampNs)));
    }

    private void registerInternal() {
        Assert.isMainThread();
        if (mRegistered || mPaused || mListeners.isEmpty()) {
            return;
        }
        logDebug("Registering sensor listener");
        mSensorManager.registerListener(mSensorEventListener, mSensor, mSensorDelay);
        mRegistered = true;
    }

    private void unregisterInternal() {
        Assert.isMainThread();
        if (!mRegistered) {
            return;
        }
        logDebug("Unregister sensor listener");
        mSensorManager.unregisterListener(mSensorEventListener);
        mRegistered = false;
        mLastBelow = null;  // Forget what we know.
    }

    /**
     * Call when the sensor reports a new value.
     *
     * Separate below-threshold and above-thresholds are specified. this allows latching behavior,
     * where a different threshold can be specified for triggering the sensor depending on if it's
     * going from above to below or below to above. To outside listeners of this class, the class
     * still appears entirely binary.
     */
    private void onSensorEvent(boolean belowThreshold, boolean aboveThreshold, long timestampNs) {
        Assert.isMainThread();
        if (!mRegistered) {
            return;
        }
        if (mLastBelow != null) {
            // If we last reported below and are not yet above, change nothing.
            if (mLastBelow && !aboveThreshold) {
                return;
            }
            // If we last reported above and are not yet below, change nothing.
            if (!mLastBelow && !belowThreshold) {
                return;
            }
        }
        mLastBelow = belowThreshold;
        logDebug("Alerting below: " + belowThreshold);
        alertListenersInternal(belowThreshold, timestampNs);
    }


    @Override
    public String toString() {
        return String.format("{isLoaded=%s, registered=%s, paused=%s, threshold=%s, sensor=%s}",
                isLoaded(), mRegistered, mPaused, mThreshold, mSensor);
    }

    private void logDebug(String msg) {
        if (DEBUG) {
            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
        }
    }

    static class Builder {
        private final Resources mResources;
        private final AsyncSensorManager mSensorManager;
        private int mSensorDelay = SensorManager.SENSOR_DELAY_NORMAL;;
        private float mThresholdValue;
        private float mThresholdLatchValue;
        private Sensor mSensor;
        private boolean mSensorSet;
        private boolean mThresholdSet;
        private boolean mThresholdLatchValueSet;

        @Inject
        Builder(@Main Resources resources, AsyncSensorManager sensorManager) {
            mResources = resources;
            mSensorManager = sensorManager;
        }


        Builder setSensorDelay(int sensorDelay) {
            mSensorDelay = sensorDelay;
            return this;
        }

        Builder setSensorResourceId(int sensorResourceId) {
            setSensorType(mResources.getString(sensorResourceId));
            return this;
        }

        Builder setThresholdResourceId(int thresholdResourceId) {
            try {
                setThresholdValue(mResources.getFloat(thresholdResourceId));
            } catch (Resources.NotFoundException e) {
                // no-op
            }
            return this;
        }

        Builder setThresholdLatchResourceId(int thresholdLatchResourceId) {
            try {
                setThresholdLatchValue(mResources.getFloat(thresholdLatchResourceId));
            } catch (Resources.NotFoundException e) {
                // no-op
            }
            return this;
        }

        Builder setSensorType(String sensorType) {
            Sensor sensor = findSensorByType(sensorType);
            if (sensor != null) {
                setSensor(sensor);
            }
            return this;
        }

        Builder setThresholdValue(float thresholdValue) {
            mThresholdValue = thresholdValue;
            mThresholdSet = true;
            if (!mThresholdLatchValueSet) {
                mThresholdLatchValue = mThresholdValue;
            }
            return this;
        }

        Builder setThresholdLatchValue(float thresholdLatchValue) {
            mThresholdLatchValue = thresholdLatchValue;
            mThresholdLatchValueSet = true;
            return this;
        }

        Builder setSensor(Sensor sensor) {
            mSensor = sensor;
            mSensorSet = true;
            return this;
        }

        /**
         * Creates a {@link ThresholdSensor} backed by a {@link ThresholdSensorImpl}.
         */
        public ThresholdSensor build() {
            if (!mSensorSet) {
                throw new IllegalStateException("A sensor was not successfully set.");
            }

            if (!mThresholdSet) {
                throw new IllegalStateException("A threshold was not successfully set.");
            }

            if (mThresholdValue > mThresholdLatchValue) {
                throw new IllegalStateException(
                        "Threshold must be less than or equal to Threshold Latch");
            }

            return new ThresholdSensorImpl(
                    mSensorManager, mSensor, mThresholdValue, mThresholdLatchValue, mSensorDelay);
        }

        private Sensor findSensorByType(String sensorType) {
            if (sensorType.isEmpty()) {
                return null;
            }

            List<Sensor> sensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL);
            Sensor sensor = null;
            for (Sensor s : sensorList) {
                if (sensorType.equals(s.getStringType())) {
                    sensor = s;
                    break;
                }
            }

            return sensor;
        }
    }
}
