package com.uva.rafael.tfg_goniometer.presenter;

import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;

import com.uva.rafael.tfg_goniometer.R;
import com.uva.rafael.tfg_goniometer.interfaces.PresenterFunctions;
import com.uva.rafael.tfg_goniometer.model.MainModel;
import com.uva.rafael.tfg_goniometer.view.MainActivity;
import com.uva.rafael.tfg_goniometer.view.fragments.MedicionFragment;
import com.uva.rafael.tfg_goniometer.view.fragments.PacientesFragment;

import java.text.DateFormat;
import java.util.Date;
import java.util.TimeZone;

/**
 * Este es el Presentador asociado al <tt>Fragment MedicionFragment</tt> de la aplicación. Se encarga
 * de llevar a cabo toda la lógica asociada a las acciones del usuario realizadas en la IU.
 * <p>
 * <p>En concreto, se encarga de realizar la "configuración inicial" del <tt>Fragment</tt>, que,
 * consiste en actualizar el item del <tt>NavigationView</tt> al segundo ítem del mismo, fijar la
 * orientación de la pantalla a "Landscape", para evitar que se pueda girar, fijar el <tt>Toolbar</tt>
 * como <tt>SupportActionBar</tt> y registrar el <tt>Listener</tt> del vector de rotación del
 * dispositivo.</p>
 * <p>
 * <p>Ademas de eso, se encarga de calcular el angulo de desplazamiento del dispositivo entre dos
 * posiciones distintas, en función de las lecturas obtenidas por el vector de rotación del mismo;
 * así como indicar a la Vista (<tt>MedicionFragment</tt>), que muestre ese angulo en tiempo real
 * mientras el usuario mueve el dispositivo</p>
 * <p>
 * <p>Por último, gestiona el comportamiento de la aplicación cuando el usuario pulsa sobre el
 * botón "Reiniciar", indicando a la Vista (<tt>MedicionFragment</tt>) que deje la IU tal y como estaba
 * cuando se creó el <tt>Fragment MedicionFragment</tt>.</p>
 * <p>
 * <p>Esta clase forma parte de la aplicación TFG-Goniometer, desarrollada para el Trabajo de
 * Fin de Grado - Grado en Ingeniería Informatica (Universidad de Valladolid)</p>
 *
 * @author Rafael Matamoros Luque
 * @version 1.0
 * @see MedicionFragment
 * @see MainModel
 * @see Toolbar
 * @see SensorEventListener
 * @see Sensor
 * @see SensorManager
 * @see AlertDialog
 */
public class MedicionPresenter implements PresenterFunctions, SensorEventListener,
        PresenterFunctions.MedicionFunctions {

    // Referencias al fragmento (Vista) con el que esta asociado y al Modelo de la aplicación
    private MedicionFragment fragment;
    private final MainModel model;

    // Referencias a SensorManager y Sensor para obtener las lecturas del vector de rotación
    private SensorManager sensorManager;
    private Sensor rotationVector;

    /*
     * Referencias donde se almacenaran la lectura inicial del vector de rotación (en formato de
     * cuaternión) y la lectura del vector de rotación cada vez que se detecta un cambio en el
     * sensor (también en formato de cuaternión)
     */
    private final float[] initialQuaternion = new float[]{-1, -1, -1, -1};
    private final float[] quaternion = new float[4];

    /*
     * Como el vector de rotación obtiene información constantemente desde el momento en que se
     * registra,esta variable sirve para indicar cuando se debe mostrar esa información o cuando se
     * debe, simplemente, dejarla pasar
     */
    private boolean mostrarInformacion = false;
    private DateFormat dateTime;

    /**
     * Constructor principal de la clase
     *
     * @param fragment Fragmento (Vista) con la que mantiene una relación 1-a-1.
     * @param model    Modelo (único) de la aplicación.
     */
    public MedicionPresenter(Fragment fragment, MainModel model) {
        this.fragment = (MedicionFragment) fragment;
        this.model = model;
    }

    /**
     * Método que realiza las operaciones iniciales cuando se crea el
     * <tt>Fragment MedicionFragment</tt>.
     * <p>
     * Se encarga de marcar el segundo ítem del <tt>NavigationView</tt> (correspondiente a
     * <tt>MedicionFragment</tt>), fijar la orientación de la pantalla en "Landscape", de utilizar
     * el <tt>Toolbar</tt> recibido como <tt>SupportActionBar</tt>, y de registrar el
     * <tt>Listener</tt> del vector de rotación, para que empiece a obtener lecturas del dispositivo.
     * <p>
     * <p>Ademas de esto, se encarga de enviar a la Vista (<tt>MedicionFragment</tt>), el tamaño
     * que tiene que asignar al goniómetro y al botón "Reiniciar" en función del tamaño de la
     * pantalla del dispositivo.</p>
     *
     * @param toolbar <tt>Toolbar</tt> a emplear como <tt>SupportActionBar</tt>
     */
    @Override
    public void setUpInitialSettings(Toolbar toolbar) {
        // Marcar el segundo ítem del Navigationview
        ((MainActivity) fragment.getActivity()).setNavigationItem(1);

        // Obtener las medidas de la pantalla del dispositivo
        DisplayMetrics metrics = new DisplayMetrics();
        fragment.getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);

        // Sólo se necesita la medida de la anchura, dado que la pantalla va a estar en horizontal
        int widthPixels = metrics.widthPixels;

        // El goniómetro tendra un tamaño del 79% de la anchura de la pantalla
        fragment.setTamañoGoniometro((int) (0.79 * widthPixels));
        // Y el botón "Reiniciar" un 25% de la anchura de la pantalla
        fragment.setTamañoReiniciar((int) (0.25 * widthPixels));

        // Fijar la orientación de la pantalla en "Landscape"
        fragment.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

        // Utilizar el toolbar como SupportActionBar
        AppCompatActivity activity = ((AppCompatActivity) fragment.getActivity());

        activity.setSupportActionBar(toolbar);

        if (activity.getSupportActionBar() != null)
            activity.getSupportActionBar().setHomeAsUpIndicator(R.mipmap.ic_menu_white);

        activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        activity.getSupportActionBar().setTitle("");

        // Registrar el Listener del vector de rotación para empezar a obtener lecturas del mismo
        registrarListeners();
    }

    /*
     * Método que se encarga de obtener una referencia a SensorManager (SENSOR_SERVICE) y, poder así,
     * obtener una referencia al Sensor del vector de rotación (Sensor.TYPE_ROTATION_VECTOR).
     *
     * Por último, registra el Listener sobre este rotationVector para empezar a obtener lecturas
     * del mismo en el método onSensorChanged(SensorEvent sensorEvent)
     */
    private void registrarListeners() {
        sensorManager = (SensorManager) fragment.getActivity().getSystemService(Context.SENSOR_SERVICE);
        rotationVector = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);

        sensorManager.registerListener(this, rotationVector, SensorManager.SENSOR_DELAY_NORMAL);
    }

    /**
     * Evento <tt>ClickListener</tt> que se llama cuando el usuario ha pulsado sobre el
     * <tt>LinearLayout</tt> que contiene el goniómetro en el <tt>MedicionFragment</tt>.
     * <p>
     * Se encarga de comprobar si se dispone un lectura del vector de rotación como "origen" para
     * calcular el angulo de desplazamiento.
     * <p>
     * Si ya se dispone de ella, significa que el usuario ha pulsado sobre el goniómetro para terminar
     * la medición, por tanto, se deja de mostrar la variación del angulo en tiempo real, y se
     * obtiene la fecha y la hora en la que se ha realizado la medición para su, quizas, posterior
     * almacenamiento.
     * <p>
     * Si no se dispone de ella, significa que el usuario ha pulsado sobre el goniómetro para empezar
     * la medición, por tanto, se toma como origen de la misma la lectura que se esta obteniendo
     * del vector de rotación en esa posición, y se empieza a mostrar la variación del angulo en
     * tiempo real.
     */
    @Override
    public void onGoniometerClicked() {

        if (initialQuaternion[0] == -1) {
            /*
             * No se dispone de una posición "origen" de la medición. Se toma como origen de la
             * misma, la lectura del vector de rotación en ese momento
             */
            System.arraycopy(quaternion, 0, initialQuaternion, 0, quaternion.length);

            // Se empieza a mostrar el angulo de desplazamiento en tiempo real
            mostrarInformacion = true;
        } else {
            /*
             * Se dispone de una posición "origen" de la medición. Ya no es necesario seguir
             * mostrando la variación del angulo en tiempo real
             */
            mostrarInformacion = false;

            // Se obtiene la fecha y la hora en la que se ha realizado la medición
            dateTime = DateFormat.getDateTimeInstance();
            dateTime.setTimeZone(TimeZone.getDefault());
        }
    }

    /**
     * Evento <tt>ClickListener</tt> que se llama cuando el usuario ha pulsado sobre el botón
     * "ALMACENAR MEDICIÓN" en el <tt>MedicionFragment</tt>.
     * <p>
     * Se encarga de:
     * <p>
     * 1.- Comprobar que el usuario ha realizado una medición y no ha pulsado sobre el botón antes
     * de haberlo hecho.
     * 2.- Crea un dialogo que envía a la Vista para que se lo muestre al usuario, con el fin de
     * confirmar que quiere almacenar la medición.
     * 3.- Crea un nuevo <tt>Fragment PacientesFragment</tt> para que el usuario elija el usuario al
     * que quiere añadir esa medición.
     *
     * @param measurement Lectura obtenida del <tt>TextView</tt> con el angulo de desplazamiento
     *                    del dispositivo.
     */
    @Override
    public void onStoreMeasurementClicked(String measurement) {
        // Dialogo que se mostrara al usuario
        AlertDialog dialog;

        // 1. Instantiate an AlertDialog.Builder with its constructor
        AlertDialog.Builder builder = new AlertDialog.Builder(fragment.getActivity());

        // Comprobación de que se ha realizado una medición completamente
        if (!(measurement.equals(fragment.getText(R.string.inicio_medicion))) && !mostrarInformacion) {
            final double angulo = Double.parseDouble(measurement.split("º")[0]);

            // 2. Chain together various setter methods to set the dialog characteristics
            builder.setTitle(R.string.seleccionar_opcion);
            builder.setItems(R.array.storeMeasurementArray, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int which) {
                    if (which == 0) {
                        // Se ha pulsado sobre la opción "Paciente existente"

                        // A continuación, se mostrara el listado de pacientes en el sistema
                        PacientesFragment pacientesFragment = new PacientesFragment();

                        Bundle params = new Bundle();

                        params.putDouble(MainModel.LECTURA_GONIOMETRO, angulo);

                        // Se pasa también la fecha y hora en el Bundle al crear el Fragment
                        params.putString(MainModel.DATETIME, dateTime.format(new Date()));

                        // Se muestra el botón que permite almacenar una nueva medición en el paciente
                        model.setMostrarOpcionesAlmacenarMedicion(true);
                        // Se oculta la opción de borrar un paciente mientras se hace una medición
                        model.setMostrarOpcionesBorrarPaciente(false);

                        pacientesFragment.setArguments(params);

                        fragment.getFragmentManager()
                                .beginTransaction()
                                .replace(R.id.content_frame, pacientesFragment)
                                .addToBackStack("PacientesFragment")
                                .commit();
                    }
                    /*
                     * Si se ha pulsado sobre la opción "Cancelar" no hacer nada, simplemente
                     * cerrar el dialogo
                     */
                }
            });
        } else {
            // 2. Chain together various setter methods to set the dialog characteristics
            builder.setMessage(R.string.error_medicion);

            // 3. Add the buttons
            builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    // User clicked OK button

                    // No realizar ninguna acción. Simplemente cerrar el dialogo
                }
            });
        }
        // 4. Get the AlertDialog from create()
        dialog = builder.create();

        // Enviar el dialogo a la Vista para que se lo muestre al usuario
        fragment.displayResult(dialog);
    }

    /**
     * Called when there is a new sensor event.
     * <p>
     * Si el cambio es del vector de rotación, se almacena la lectura del mismo en la variable global
     * <tt>quaternion</tt> (previa obtención del cuaternión asociado a la lectura del vector de
     * rotación), y, por último, si hay que mostrar la variación del angulo en tiempo real, se llama
     * al método <tt>mostrarAngulo()</tt> para que obtenga el angulo de desplazamiento entre el
     * cuaternión inicial <tt>initialQuaternion</tt> y el cuaternión final <tt>quaternion</tt>, y
     * mande esa información a la vista para que se la muestre al usuario.
     *
     * @param sensorEvent the SensorEvent
     */
    @Override
    public void onSensorChanged(SensorEvent sensorEvent) {
        if (sensorEvent.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR)
            SensorManager.getQuaternionFromVector(quaternion, sensorEvent.values);

        if (mostrarInformacion)
            /*
             * Si hay que mostrar la variación del angulo en tiempo real, se calcula y se envía a
             * la Vista para que lo muestre
             */
            mostrarAngulo();
    }

    /**
     * Called when the accuracy of the registered rotationVector has changed.
     *
     * @param sensor   the Sensor
     * @param accuracy The new accuracy of this rotationVector, one of <tt>SensorManager.SENSOR_STATUS_*</tt>
     */
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // No se implementa nueva funcionalidad
    }

    /*
     * Método que devuelve un objeto de tipo Animation con el desplazamiento de la View de
     * MedicionFragment que representa el angulo de desplazamiento del dispositivo desde la
     * posición de inicio hasta la de fin.
     */
    private Animation rotarBarraDesplazamiento(float inicio, float fin) {
        // El objeto Animation a devolver
        Animation animation;

        if ((inicio != 0) && (fin != 0))
            // Crear un objeto Animation desde la posición de inicio a la de fin recibidas
            animation = new RotateAnimation(inicio, fin,
                    Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f);
        else
            // Reiniciar la barra de desplazamiento a la posición inicial
            animation = new RotateAnimation(0.0f, 0.0f,
                    Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f);

        // No repetir la animación
        animation.setRepeatCount(-1);

        return animation;
    }

    /**
     * Evento <tt>ClickListener</tt> que se llama cuando el usuario ha pulsado sobre el botón
     * "Reiniciar" en el <tt>MedicionFragment</tt>.
     * <p>
     * Se encarga de:
     * <p>
     * 1.- Comprobar si se esta mostrando la variación del angulo en tiempo real.
     * 2.- De ser así, dejar de mostrarla.
     * 3.- Reiniciar los valores que representan el origen de la medición.
     * 4.- Volver al estado inicial la lectura del goniómetro, así como la barra de desplazamiento
     * que representa la variación del angulo.
     *
     * @param lecturaGoniometro Lectura del goniómetro que se tiene en ese momento.
     */
    @Override
    public void onResetClicked(String lecturaGoniometro) {
        if (mostrarInformacion)
            // Se deja de mostrar la variación del angulo en tiempo real
            mostrarInformacion = false;

        // Reinicio de los valores "origen" de la medición
        for (int i = 0; i < 4; i++)
            initialQuaternion[i] = -1;

        /*
         * Se indica a la Vista, que tanto el valor numérico de la lectura del goniómetro, como
         * la barra de desplazamiento deben volver a sus valores iniciales
         */
        fragment.setLecturaGoniometroText(fragment.getText(R.string.inicio_medicion).toString());
        fragment.startAnimation(rotarBarraDesplazamiento(0, 0));
    }

    /*
     * Método que se encarga de realizar todos los calculos para obtener el angulo de desplazamiento
     * entre dos cuaterniones que representan la posición tridimensional del dispositivo al origen y
     * al final de la medición.
     *
     * Finalmente, este angulo obtenido (en grados) (redondeado previamente a 1 cifra decimal) es
     * enviado a la Vista junto con el objeto Animation para que muestre la variación del angulo en
     * tiempo real al usuario.
     */
    private void mostrarAngulo() {
        double angulo;
        float[] finalQuaternion = quaternion.clone();

        /*
         * El angulo de desplazamiento se calcula como el angulo entre dos cuaterniones. Este
         * calculo se obtiene mediante la siguiente fórmula:
         *
         * theta = acos(2*(<q1,q2>^2) - 1)
         *
         * donde <q1,q2> se define como el producto interno (escalar) entre dos cuaterniones, es
         * decir: <a1 + b1*i + c1*j + d1*k, a2 + b2*i + c2*j + d2*k> = a1*a2 + b1*b2 + c1*c2 + d1*d2
         *
         * Como este calculo se hace en radianes, finalmente el angulo obtenido es pasado a grados
         */
        angulo = Math.toDegrees
                (Math.acos(2 *
                        (Math.pow(
                                initialQuaternion[0] * finalQuaternion[0] +
                                        initialQuaternion[1] * finalQuaternion[1] +
                                        initialQuaternion[2] * finalQuaternion[2] +
                                        initialQuaternion[3] * finalQuaternion[3], 2)) - 1
                ));

        // Se redondea el angulo a 1 cifra decimal
        double anguloFinal = Math.round(angulo * 10.0) / 10.0;

        // La información es enviada a la Vista para que lo muestre al usuario
        fragment.setLecturaGoniometroText(anguloFinal + "º");
        fragment.startAnimation(rotarBarraDesplazamiento(360.0f, 360.0f - (float) anguloFinal));

    }

    /**
     * Called as part of the fragment lifecycle when your fragment is ready to start interacting
     * with the user.
     * <p>
     * Se encarga de volver a registrar el vector de rotación para poder obtener, de nuevo, lecturas
     * del mismo.
     */
    @Override
    public void onResume() {
        sensorManager.registerListener(this, rotationVector, SensorManager.SENSOR_DELAY_NORMAL);
    }

    /**
     * Called as part of the fragment lifecycle when a fragment is going into the background, but
     * has not (yet) been killed.
     * <p>
     * Se encarga de borrar el <tt>Listener</tt> del vector de rotación, para evitar así, que se
     * consuma la batería del dispositivo cuando la actividad esté en un segundo plano.
     */
    @Override
    public void onPause() {
        sensorManager.unregisterListener(this);
    }

    /**
     * Perform any final cleanup before an activity is destroyed.
     * <p>
     * Se encarga de liberar la referencia al <tt>Fragment</tt> con el que esta asociado.
     */
    @Override
    public void onDestroy() {
        fragment = null;
    }
}
