package com.example.fitbot.util.processing;

import android.util.Log;

import com.example.fitbot.exercise.Exercise;
import com.example.fitbot.exercise.ExerciseManager;
import com.example.fitbot.pepper.Pepper;
import com.example.fitbot.ui.activities.EndScreenActivity;
import com.example.fitbot.ui.activities.FitnessActivity;
import com.example.fitbot.ui.activities.MainActivity;
import com.example.fitbot.util.NavigationManager;
import com.example.fitbot.util.server.WebServer;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import org.jetbrains.annotations.NotNull;
import org.joml.Vector3f;

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

public class InputProcessor {

    private List<Vector3f>[] selfRotationVectorPaths;       // Relative path of the motion data
    //private Vector3f[][] selfRotationVectorPaths;       // Relative path of the motion data
    private Vector3f[][] targetRotationVectorPaths; // Target path of the motion data

    private final float sampleRate; // The sample rate of the motion sensor
    private float exerciseDurationInSeconds;
    private int repetitionsRemaining = 0;
    private int exercisesRemaining = 0;
    private float exerciseScore = 0.0F;

    private final FitnessActivity parentActivity;

    /**
     * The phrases that are said by the robot whenever the exercise starts.
     */
    private static final String[] STARTING_PHRASES = {
            "Veel success met de oefening!",
            "Je kan het!",
            "Veel plezier!"
    };

    /**
     * This field is used to determine if the motion data is being recorded.
     * If this is the case, instead of functioning normally, the element
     * will record the movement that the user makes, store it in the
     * `selfRotationVectorPaths` field and send it to the server.
     */
    private boolean recordingMovement = false;

    /**
     * Represents the duration of the recording in seconds.
     * This field only has effect when the `recordingMovement` field is set to true.
     */
    private float recordingDurationInSeconds = 0.0f;

    // How many seconds have passed since the start of the exercise.
    // The exercise starts whenever the `startListening` function is called.
    private double secondsPassed = 0.0D;
    private long lastTime;

    private static final String[] REQUIRED_SENSOR_JSON_PROPERTIES =
            {"rotationX", "rotationY", "rotationZ", "deviceId"};

    // The web server that listens for incoming motion data.
    private WebServer server;


    /**
     * Constructor for the motion processor.
     *
     * @param inputSampleRate The sample rate of the motion sensor.
     */
    public InputProcessor(float inputSampleRate, FitnessActivity parentActivity) {
        this.sampleRate = inputSampleRate;
        this.parentActivity = parentActivity;
    }

    /**
     * Function for setting the exercise to use.
     * This updates the user and target path and the
     * duration of the exercise.
     * <p>
     * This function is only initially used to select the starting exercise;
     * the exercises that follow are determined by a private method 'nextExercise'
     *
     * @param exercise The exercise to use the paths for.
     */
    public void useExercise(Exercise exercise) {
        if (this.recordingMovement)
            throw new IllegalStateException("Cannot change exercise while recording movement.");

        this.exercisesRemaining = 1;
        this.nextExercise(exercise);
        Pepper.say(STARTING_PHRASES[(int) Math.floor(Math.random() * STARTING_PHRASES.length)]);
    }

    /**
     * Moves on to the next exercise without changing the remaining exercises.
     *
     * @param exercise The exercise to move on to.
     */
    private void nextExercise(Exercise exercise) {
        if (this.exercisesRemaining-- <= 0) {
            NavigationManager.navigateToActivity(this.parentActivity, EndScreenActivity.class);
        }

        ExerciseManager.TOTAL_REPETITIONS_REQUIRED += ExerciseManager.DEFAULT_EXERCISE_REPETITIONS;
        ExerciseManager.TOTAL_EXERCISES_PREFORMED++;

        this.selfRotationVectorPaths[0] = new ArrayList<>();
        this.selfRotationVectorPaths[1] = new ArrayList<>();

        this.repetitionsRemaining = ExerciseManager.DEFAULT_EXERCISE_REPETITIONS;

        this.targetRotationVectorPaths = new Vector3f[2][exercise.rightPath.getAngleVectors().length];
        this.targetRotationVectorPaths[0] = exercise.leftPath.getAngleVectors();
        this.targetRotationVectorPaths[1] = exercise.rightPath.getAngleVectors();
        this.exerciseDurationInSeconds = exercise.exerciseTimeInSeconds;
        this.secondsPassed = 0.0D;
        this.lastTime = System.currentTimeMillis();
    }

    /**
     * Method that is called whenever the user performs a good repetition.
     */
    public void onAdequateRepetition() {
        ExerciseManager.TOTAL_REPETITIONS_PERFORMED++;
        // TODO: Play sound
    }

    /**
     * Method that is called whenever the user performs a bad repetition.
     */
    public void onInadequateRepetition() {

    }

    /**
     * Function for setting whether the motion data
     * should be recorded or not.
     *
     * @param recording Whether the motion data should be recorded.
     * @param duration  For how long the motion data should be recorded.
     *                  This only has an effect if `recording` is true.
     */
    public void setRecording(boolean recording, float duration) {
        this.recordingMovement = recording;
        this.recordingDurationInSeconds = duration;
        if (recording) {
            this.secondsPassed = 0.0D;
            this.lastTime = System.currentTimeMillis();
            this.selfRotationVectorPaths[0] = new ArrayList<>();
            this.selfRotationVectorPaths[1] = new ArrayList<>();
        }
    }

    /**
     * Function for checking if the exercise or recording has finished.
     * This function will return true if the execution of the exercise has finished or
     * if the recording has finished, depending on the state of the `recordingMovement` field.
     *
     * @return Whether the exercise or recording has finished.
     */
    public boolean hasFinished() {
        return this.recordingMovement ?
                (this.secondsPassed >= this.recordingDurationInSeconds) :
                (this.secondsPassed >= this.exerciseDurationInSeconds);
    }

    /**
     * Function for starting the listening process
     * of the motion sensor. This function will create
     * a new WebSocket server and start listening for
     * incoming connections.
     */
    public void startListening() {
        // Create socket server
        this.server = WebServer.createServer();

        Log.i("MotionProcessor", "Listening for incoming connections.");

        // Check if the socket
        if (server != null) {
            // Update event handler to match our functionality.
            server.setEventHandler(this::parsePacket);
            this.secondsPassed = 0.0d;
            this.lastTime = System.currentTimeMillis();
        }
    }

    /**
     * Function for stopping the listening process
     * of the motion sensor. This function will stop
     * the WebSocket server.
     */
    public void stopListening() {
        if (server != null) {
            server.stop();
            server = null;
        }
    }

    /**
     * Function for parsing arbitrary packet data.
     *
     * @param data The data to parse.
     */
    public void parsePacket(@NotNull String data) {

        try {

            Log.i("MotionProcessor", "Time passed: " + this.secondsPassed + "s");
            if (this.recordingMovement)
                Log.i("MotionProcessor", this.secondsPassed + " / " + this.recordingDurationInSeconds);
            Log.i("MotionProcessor", "Received packet data: " + data);

            JsonElement json = JsonParser.parseString(data);

            if (!json.isJsonObject())
                return;

            JsonObject object = json.getAsJsonObject();

            // Ensure all properties are present in the received JSON object
            for (String s : REQUIRED_SENSOR_JSON_PROPERTIES) {
                if (!object.has(s))
                    return;
            }

            // Parse the data
            Vector3f rotation = new Vector3f(
                    object.get("rotationX").getAsFloat(),
                    object.get("rotationY").getAsFloat(),
                    object.get("rotationZ").getAsFloat());
            int deviceId = object.get("deviceId").getAsInt();

            // Parse the retrieved data
            parseRotationVector(rotation, deviceId);
        } catch (Exception e) {
            Log.i("MotionProcessor", "Failed to parse packet data.");
        }
    }

    /**
     * Function for adding motion data to the processor.
     *
     * @param rotation The rotation vector of the motion data.
     * @param deviceId The device ID of the motion data.
     */
    public void parseRotationVector(Vector3f rotation, int deviceId) {
        if (deviceId >= 0 && deviceId < selfRotationVectorPaths.length) {

            // Re-calculate time for index calculation
            secondsPassed = (System.currentTimeMillis() - lastTime) / 1000.0d;


            // Supposed index of the current rotation vector in the `rotationVectorPaths` array
            this.selfRotationVectorPaths[deviceId].add(rotation);

            if (this.recordingMovement && this.secondsPassed >= this.recordingDurationInSeconds) {
                // Do something with the recorded data.
                this.recordingMovement = false;
                // Convert recorded data from `selfRotationVectorPaths` to string, and
                // publish to database, or do something else with it.

                String converted = convertRecordedDataToString();

                // Do something with it
                Log.i("MotionProcessor", "Converted data: ");
                Log.i("MotionProcessor", converted);
            }

            // Do something else with the vector
            // TODO: Implement !!

            Log.i("MotionProcessor", "Rotation vector: " + rotation.toString() + " from device: " + deviceId);

            // Whenever the exercise has finished and it's not recording,
            // attempt to move to the next exercise.
            // If this fails, navigate back to the main activity.
            if (this.hasFinished() && !this.recordingMovement) {
                this.parentActivity.fetchExerciseAsync(this::nextExercise, (nil) -> {
                    Log.i("MotionProcessor", "Failed to fetch exercise data.");
                    NavigationManager.navigateToActivity(this.parentActivity, MainActivity.class);
                });
            }

        }
    }

    /**
     * Function for converting the recorded data to a string.
     * This function will convert the recorded data to a string
     * that can be sent to a database or other storage.
     *
     * @return The converted string.
     */
    private String convertRecordedDataToString() {
        int[] intBits = new int[3];
        char[] vectorChars = new char[12]; // 4 bytes per scalar, 12 chars per vector
        JsonArray jsonArray = new JsonArray();
        /*
         * Convert to JSON array in the following format:
         * [
         *  { "deviceId": number, "data": [ [x, y, z], [x, y, z], ... ] },
         * ]
         */

        // Iterate over all devices. In the current instance, it's 2.
        for (int deviceId = 0; deviceId < selfRotationVectorPaths.length; deviceId++) {
            JsonObject jsonDeviceObject = new JsonObject();
            jsonDeviceObject.addProperty("deviceId", deviceId);

            // Data array
            JsonArray jsonDeviceDataArray = new JsonArray();

            for (Vector3f vector : selfRotationVectorPaths[deviceId]) {
                JsonArray jsonScalarArray = new JsonArray();
                jsonScalarArray.add(vector.x);
                jsonScalarArray.add(vector.y);
                jsonScalarArray.add(vector.z);
                jsonDeviceDataArray.add(jsonScalarArray);
            }
            jsonDeviceObject.add("data", jsonDeviceDataArray);

            jsonArray.add(jsonDeviceObject);
        }
        return jsonArray.toString();
    }

    /**
     * Method for getting the current progress of the exercise.
     * The return value will range between 0.0 and 1.0.
     *
     * @return The current progress of the exercise.
     */
    public double getCurrentProgress() {
        return secondsPassed / exerciseDurationInSeconds;
    }

    /**
     * Function for getting the combined (average) error value of both sensors.
     public double getCombinedError()
     {

     }*/

    /**
     * Function for getting the error offsets of the user's path compared to the
     * target path at a given point in time.
     *
     * @param sensorId The sensor ID to get the error offsets from.
     * @param time     The time to get the error offsets from.
     *                 This value must be >= 0 && <= exerciseTime, otherwise
     *                 the error will be 0 by default.
     * @return A list of error offsets of the motion data compared to the reference path.
     */
    public double getError(int sensorId, float time) {

        /*// Ensure the sensor ID is within the bounds of the array
        if (sensorId < 0 || sensorId >= selfRotationVectorPaths.length)
            return 0.0d;

        // Index of the current rotation vector
        int targetIndex = (int) ((this.exerciseDurationInSeconds / this.targetRotationVectorPaths[sensorId].length) * time);
        int selfIndex = (int) (this.selfRotationVectorPaths[sensorId].length / this.sampleRate * time);

        // Ensure the indexes are within the bounds of the array
        if (
                targetIndex >= 0 && targetIndex <= this.targetRotationVectorPaths[sensorId].length - 1 &&
                selfIndex >= 0 && selfIndex <= this.selfRotationVectorPaths[sensorId].length - 1 &&
                this.selfRotationVectorPaths[sensorId][selfIndex] != null &&
                this.targetRotationVectorPaths[sensorId][targetIndex] != null
        ) {
            return this.selfRotationVectorPaths[sensorId][selfIndex].distance(this.targetRotationVectorPaths[sensorId][targetIndex]);
        }*/
        return 0.0d;
    }

    /**
     * Method for getting the average error of the motion data
     * compared to the reference path.
     *
     * @param sensorId The sensor ID to get the error offsets from.
     * @return The average error of the motion data compared to the reference path.
     */
    public double getAverageError(int sensorId) {
        double error = 0;
        for (int i = 0; i < this.exerciseDurationInSeconds; i++) {
            error += getError(sensorId, i);
        }
        return error / this.exerciseDurationInSeconds;
    }

    public float secondsPassed() {
        return (float) secondsPassed;
    }

    public boolean isRecording() {
        return this.recordingMovement;
    }
}
