package tw.moonjuice.light_sensor_capture;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.view.WindowManager;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.HashMap;
import java.util.Map;

import io.flutter.Log;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity implements EventChannel.StreamHandler, SensorEventListener, MethodChannel.MethodCallHandler {
    private Sensor mLightSensor;
    private SensorManager mSensorManager;
    private EventChannel.EventSink mEventSink;
    private AmbientLightRingBuffer mAmbientLightRingBuffer;
    private long mAmbientLightHorizonLong = 4000;
    private int mRecentLightSamples = 0;
    private float mLastObservedLux;
    private long mLastObservedLuxTime;
    private Handler mHandler;
    private long mAmbientLightHorizonShort = 2000;
    private float mSlowAmbientLux;
    private float mFastAmbientLux;
    private long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        mHandler = new AutomaticBrightnessHandler();
    }

    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        EventChannel eventChannel = new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "tw.moonjuice.light_sensor.stream");
        eventChannel.setStreamHandler(this);
        MethodChannel methodChannel = new MethodChannel(flutterEngine.getDartExecutor(), "tw.moonjuice.light_sensor.method");
        methodChannel.setMethodCallHandler(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mSensorManager.registerListener(this, mLightSensor, SensorManager.SENSOR_DELAY_FASTEST);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        mAmbientLightRingBuffer = new AmbientLightRingBuffer(250, 4000, new RealClock(false));
    }

    @Override
    protected void onStop() {
        mSensorManager.unregisterListener(this);
        super.onStop();
    }

    @Override
    public void onListen(Object arguments, EventChannel.EventSink events) {
        mEventSink = events;
    }

    @Override
    public void onCancel(Object arguments) {
        mEventSink = null;
    }

    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        final long time = SystemClock.uptimeMillis();
        final float lux = sensorEvent.values[0];
        Message msg = mHandler.obtainMessage();
        Bundle data = new Bundle();
        data.putFloat("lux", lux);
        data.putLong("time", time);
        msg.setData(data);
        msg.sendToTarget();
    }

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

    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        switch (call.method) {
            case "getExternalDownloadsPath":
                result.success(Environment.getExternalStoragePublicDirectory(
                        Environment.DIRECTORY_DOWNLOADS).getAbsolutePath());
                break;
            case "start":
                Toast.makeText(getContext(), "start", Toast.LENGTH_SHORT).show();
                result.success("success");
                break;
            default:
                Toast.makeText(getContext(), "stop", Toast.LENGTH_SHORT).show();
                result.success("success");
                break;
        }
    }
    /**
     * A ring buffer of ambient light measurements sorted by time.
     *
     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
     * from oldest to newest.
     */
    private static final class AmbientLightRingBuffer {
        // Proportional extra capacity of the buffer beyond the expected number of light samples
        // in the horizon
        private static final float BUFFER_SLACK = 1.5f;
        private float[] mRingLux;
        private long[] mRingTime;
        private int mCapacity;

        // The first valid element and the next open slot.
        // Note that if mCount is zero then there are no valid elements.
        private int mStart;
        private int mEnd;
        private int mCount;
        Clock mClock;

        public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon, Clock clock) {
            if (lightSensorRate <= 0) {
                throw new IllegalArgumentException("lightSensorRate must be above 0");
            }
            mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate);
            mRingLux = new float[mCapacity];
            mRingTime = new long[mCapacity];
            mClock = clock;
        }

        public float getLux(int index) {
            return mRingLux[offsetOf(index)];
        }

        public float[] getAllLuxValues() {
            float[] values = new float[mCount];
            if (mCount == 0) {
                return values;
            }

            if (mStart < mEnd) {
                System.arraycopy(mRingLux, mStart, values, 0, mCount);
            } else {
                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
            }

            return values;
        }

        public long getTime(int index) {
            return mRingTime[offsetOf(index)];
        }

        public long[] getAllTimestamps() {
            long[] values = new long[mCount];
            if (mCount == 0) {
                return values;
            }

            if (mStart < mEnd) {
                System.arraycopy(mRingTime, mStart, values, 0, mCount);
            } else {
                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
            }

            return values;
        }

        public void push(long time, float lux) {
            int next = mEnd;
            if (mCount == mCapacity) {
                int newSize = mCapacity * 2;

                float[] newRingLux = new float[newSize];
                long[] newRingTime = new long[newSize];
                int length = mCapacity - mStart;
                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
                if (mStart != 0) {
                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
                }
                mRingLux = newRingLux;
                mRingTime = newRingTime;

                next = mCapacity;
                mCapacity = newSize;
                mStart = 0;
            }
            mRingTime[next] = time;
            mRingLux[next] = lux;
            mEnd = next + 1;
            if (mEnd == mCapacity) {
                mEnd = 0;
            }
            mCount++;
        }

        public void prune(long horizon) {
            if (mCount == 0) {
                return;
            }

            while (mCount > 1) {
                int next = mStart + 1;
                if (next >= mCapacity) {
                    next -= mCapacity;
                }
                if (mRingTime[next] > horizon) {
                    // Some light sensors only produce data upon a change in the ambient light
                    // levels, so we need to consider the previous measurement as the ambient light
                    // level for all points in time up until we receive a new measurement. Thus, we
                    // always want to keep the youngest element that would be removed from the
                    // buffer and just set its measurement time to the horizon time since at that
                    // point it is the ambient light level, and to remove it would be to drop a
                    // valid data point within our horizon.
                    break;
                }
                mStart = next;
                mCount -= 1;
            }

            if (mRingTime[mStart] < horizon) {
                mRingTime[mStart] = horizon;
            }
        }

        public int size() {
            return mCount;
        }

        public void clear() {
            mStart = 0;
            mEnd = 0;
            mCount = 0;
        }

        @Override
        public String toString() {
            StringBuilder buf = new StringBuilder();
            buf.append('[');
            for (int i = 0; i < mCount; i++) {
                final long next = i + 1 < mCount ? getTime(i + 1)
                        : mClock.getSensorEventScaleTime();
                if (i != 0) {
                    buf.append(", ");
                }
                buf.append(getLux(i));
                buf.append(" / ");
                buf.append(next - getTime(i));
                buf.append("ms");
            }
            buf.append(']');
            return buf.toString();
        }

        private int offsetOf(int index) {
            if (index >= mCount || index < 0) {
                throw new ArrayIndexOutOfBoundsException(index);
            }
            index += mStart;
            if (index >= mCapacity) {
                index -= mCapacity;
            }
            return index;
        }
    }
    private static class RealClock implements Clock {
        private final boolean mOffloadControlsDozeBrightness;

        RealClock(boolean offloadControlsDozeBrightness) {
            mOffloadControlsDozeBrightness = offloadControlsDozeBrightness;
        }

        @Override
        public long uptimeMillis() {
            return SystemClock.uptimeMillis();
        }

        public long getSensorEventScaleTime() {
            return (mOffloadControlsDozeBrightness)
                    ? SystemClock.elapsedRealtime() : uptimeMillis();
        }
    }
    interface Clock {
        /**
         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
         */
        long uptimeMillis();

        /**
         * Gets the time on either the elapsedTime or the uptime scale, depending on how we
         * processing the events from the sensor
         */
        long getSensorEventScaleTime();
    }

    private class AutomaticBrightnessHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            switch (msg.what) {
                default:
                    final long time = msg.getData().getLong("time");
                    final float lux = msg.getData().getFloat("lux");
                    handleLightSensorEvent(time, lux);
                    break;
            }
        }
    }

    private void handleLightSensorEvent(long time, float lux) {
        mRecentLightSamples++;
        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
        mAmbientLightRingBuffer.push(time, lux);
        // Remember this sample value.
        mLastObservedLux = lux;
        mLastObservedLuxTime = time;
        //updateAmbientLux(time);
        mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort);
        mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong);
//        Log.i("moon", "mFastAmbientLux: " + mFastAmbientLux);
//        Log.i("moon", "mSlowAmbientLux: " + mSlowAmbientLux);
        if (mEventSink != null) {
            final Map data = new HashMap();
            data.put("lux", lux);
            data.put("lux_fast", mFastAmbientLux);
            data.put("lux_slow", mSlowAmbientLux);
            data.put("timestamp", time);
            mEventSink.success(data);
        }
    }

    private float calculateAmbientLux(long now, long horizon) {
        final int N = mAmbientLightRingBuffer.size();
        if (N == 0) {
//            Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
            return -1;
        }

        // Find the first measurement that is just outside of the horizon.
        int endIndex = 0;
        final long horizonStartTime = now - horizon;
        for (int i = 0; i < N-1; i++) {
            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
                endIndex++;
            } else {
                break;
            }
        }
//        if (mLoggingEnabled) {
//            Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
//                    mAmbientLightRingBuffer.getTime(endIndex) + ", " +
//                    mAmbientLightRingBuffer.getLux(endIndex) + ")");
//        }
        float sum = 0;
        float totalWeight = 0;
        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
        for (int i = N - 1; i >= endIndex; i--) {
            long eventTime = mAmbientLightRingBuffer.getTime(i);
            if (i == endIndex && eventTime < horizonStartTime) {
                // If we're at the final value, make sure we only consider the part of the sample
                // within our desired horizon.
                eventTime = horizonStartTime;
            }
            final long startTime = eventTime - now;
            float weight = calculateWeight(startTime, endTime);
            float lux = mAmbientLightRingBuffer.getLux(i);
//            if (mLoggingEnabled) {
//                Log.i("moon", "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
//                        "lux=" + lux + ", " +
//                        "weight=" + weight);
//            }
            totalWeight += weight;
            sum += lux * weight;
            endTime = startTime;
        }
//        if (mLoggingEnabled) {
//            Log.i("moon", "calculateAmbientLux: " +
//                    "totalWeight=" + totalWeight + ", " +
//                    "newAmbientLux=" + (sum / totalWeight));
//        }
        return sum / totalWeight;
    }

    private float calculateWeight(long startDelta, long endDelta) {
        return weightIntegral(endDelta) - weightIntegral(startDelta);
    }
    private float weightIntegral(long x) {
        return x * (x * 0.5f + 4000);
    }
}
