/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * 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 io.github.webbluetoothcg.bletestperipheral;

import android.app.Activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

public class HealthThermometerServiceFragment extends ServiceFragment {
  /**
   * See <a href="https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.health_thermometer.xml">
   * Health Thermometer Service</a>
   * This service exposes two characteristics with descriptors:
   *   - Measurement Interval Characteristic:
   *       - Listen to notifications to from which you can subscribe to notifications
   *     - CCCD Descriptor:
   *       - Read/Write to get/set notifications.
   *     - User Description Descriptor:
   *       - Read/Write to get/set the description of the Characteristic.
   *   - Temperature Measurement Characteristic:
   *       - Read value to get the current interval of the temperature measurement timer.
   *       - Write value resets the temperature measurement timer with the new value. This timer
   *         is responsible for triggering value changed events every "Measurement Interval" value.
   *     - CCCD Descriptor:
   *       - Read/Write to get/set notifications.
   *     - User Description Descriptor:
   *       - Read/Write to get/set the description of the Characteristic.
   */
  private static final UUID HEALTH_THERMOMETER_SERVICE_UUID = UUID
      .fromString("00001809-0000-1000-8000-00805f9b34fb");

  /**
   * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.temperature_measurement.xml">
   * Temperature Measurement</a>
   */
  private static final UUID TEMPERATURE_MEASUREMENT_UUID = UUID
      .fromString("00002A1C-0000-1000-8000-00805f9b34fb");
  private static final int TEMPERATURE_MEASUREMENT_VALUE_FORMAT = BluetoothGattCharacteristic.FORMAT_FLOAT;
  private static final float INITIAL_TEMPERATURE_MEASUREMENT_VALUE = 37.0f;
  private static final int EXPONENT_MASK = 0x7f800000;
  private static final int EXPONENT_SHIFT = 23;
  private static final int MANTISSA_MASK = 0x007fffff;
  private static final int MANTISSA_SHIFT = 0;
  private static final String TEMPERATURE_MEASUREMENT_DESCRIPTION = "This characteristic is used " +
      "to send a temperature measurement.";

  /**
   * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.measurement_interval.xml">
   * Measurement Interval</a>
   */
  private static final UUID MEASUREMENT_INTERVAL_UUID = UUID
      .fromString("00002A21-0000-1000-8000-00805f9b34fb");
  private static final int MEASUREMENT_INTERVAL_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT16;
  private static final int INITIAL_MEASUREMENT_INTERVAL = 1;
  private static final int MIN_MEASUREMENT_INTERVAL = 1;
  private static final int MAX_MEASUREMENT_INTERVAL = (int) Math.pow(2, 16) - 1;
  private static final String MEASUREMENT_INTERVAL_DESCRIPTION = "This characteristic is used " +
      "to enable and control the interval between consecutive temperature measurements.";

  private BluetoothGattService mHealthThermometerService;
  private BluetoothGattCharacteristic mTemperatureMeasurementCharacteristic;
  private BluetoothGattCharacteristic mMeasurementIntervalCharacteristic;
  private BluetoothGattDescriptor mMeasurementIntervalCCCDescriptor;

  private ServiceFragmentDelegate mDelegate;

  private Timer mTimer;

  private EditText mEditTextTemperatureMeasurement;
  private final OnEditorActionListener mOnEditorActionListenerTemperatureMeasurement = new OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        String newTemperatureMeasurementValueString = textView.getText().toString();
        if (isValidTemperatureMeasurementValue(newTemperatureMeasurementValueString)) {
          float newTemperatureMeasurementValue = Float.valueOf(newTemperatureMeasurementValueString);
          setTemperatureMeasurementValue(newTemperatureMeasurementValue);
        } else {
          Toast.makeText(getActivity(), R.string.temperatureMeasurementValueInvalid,
              Toast.LENGTH_SHORT).show();
        }
      }
      return false;
    }
  };

  private final OnEditorActionListener mOnEditorActionListenerMeasurementInterval = new OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        int newMeasurementInterval = Integer.parseInt(textView.getText().toString());
        if (isValidMeasurementIntervalValue(newMeasurementInterval)) {
          mMeasurementIntervalCharacteristic.setValue(newMeasurementInterval,
              MEASUREMENT_INTERVAL_FORMAT,
              /* offset */ 0);
          resetTimer(newMeasurementInterval);
        } else {
          Toast.makeText(getActivity(), R.string.measurementIntervalInvalid,
              Toast.LENGTH_SHORT).show();
        }
      }
      return false;
    }
  };
  private EditText mEditTextMeasurementInterval;

  private TextView mTextViewNotifications;

  public HealthThermometerServiceFragment() {
    mTemperatureMeasurementCharacteristic =
        new BluetoothGattCharacteristic(TEMPERATURE_MEASUREMENT_UUID,
            BluetoothGattCharacteristic.PROPERTY_INDICATE,
            /* No permissions */ 0);

    mTemperatureMeasurementCharacteristic.addDescriptor(
        Peripheral.getClientCharacteristicConfigurationDescriptor());

    mTemperatureMeasurementCharacteristic.addDescriptor(
        Peripheral.getCharacteristicUserDescriptionDescriptor(TEMPERATURE_MEASUREMENT_DESCRIPTION));

    mMeasurementIntervalCharacteristic =
        new BluetoothGattCharacteristic(
            MEASUREMENT_INTERVAL_UUID,
            (BluetoothGattCharacteristic.PROPERTY_READ |
                BluetoothGattCharacteristic.PROPERTY_WRITE |
                BluetoothGattCharacteristic.PROPERTY_INDICATE),
            (BluetoothGattCharacteristic.PERMISSION_READ |
                BluetoothGattCharacteristic.PERMISSION_WRITE));

    mMeasurementIntervalCCCDescriptor = Peripheral.getClientCharacteristicConfigurationDescriptor();
    mMeasurementIntervalCharacteristic.addDescriptor(mMeasurementIntervalCCCDescriptor);

    mMeasurementIntervalCharacteristic.addDescriptor(
        Peripheral.getCharacteristicUserDescriptionDescriptor(MEASUREMENT_INTERVAL_DESCRIPTION));

    mHealthThermometerService = new BluetoothGattService(HEALTH_THERMOMETER_SERVICE_UUID,
        BluetoothGattService.SERVICE_TYPE_PRIMARY);
    mHealthThermometerService.addCharacteristic(mTemperatureMeasurementCharacteristic);
    mHealthThermometerService.addCharacteristic(mMeasurementIntervalCharacteristic);
  }


  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
      Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment_health_thermometer, container, false);
    mEditTextTemperatureMeasurement = (EditText) view
        .findViewById(R.id.editText_temperatureMeasurementValue);
    mEditTextTemperatureMeasurement
        .setOnEditorActionListener(mOnEditorActionListenerTemperatureMeasurement);
    mEditTextMeasurementInterval = (EditText) view
        .findViewById(R.id.editText_measurementIntervalValue);
    mEditTextMeasurementInterval
        .setOnEditorActionListener(mOnEditorActionListenerMeasurementInterval);

    mEditTextTemperatureMeasurement.setText(Float.toString(INITIAL_TEMPERATURE_MEASUREMENT_VALUE));
    setTemperatureMeasurementValue(INITIAL_TEMPERATURE_MEASUREMENT_VALUE);

    mMeasurementIntervalCharacteristic.setValue(INITIAL_MEASUREMENT_INTERVAL,
        MEASUREMENT_INTERVAL_FORMAT,
        /* offset */ 0);
    mEditTextMeasurementInterval.setText(Integer.toString(INITIAL_MEASUREMENT_INTERVAL));

    mTextViewNotifications = (TextView) view.findViewById(R.id.textView_notifications);
    mTextViewNotifications.setText(R.string.notificationsNotEnabled);

    return view;
  }

  @Override
  public void onAttach(Activity activity) {
    super.onAttach(activity);
    try {
      mDelegate = (ServiceFragmentDelegate) activity;
    } catch (ClassCastException e) {
      throw new ClassCastException(activity.toString()
          + " must implement ServiceFragmentDelegate");
    }
  }

  @Override
  public void onDetach() {
    super.onDetach();
    mDelegate = null;
  }

  @Override
  public void onStop() {
    super.onStop();
    cancelTimer();
  }

  @Override
  public BluetoothGattService getBluetoothGattService() {
    return mHealthThermometerService;
  }

  @Override
  public ParcelUuid getServiceUUID() {
    return new ParcelUuid(HEALTH_THERMOMETER_SERVICE_UUID);
  }

  private void setTemperatureMeasurementValue(float temperatureMeasurementValue) {

    /* Set the org.bluetooth.characteristic.temperature_measurement
     * characteristic to a byte array of size 5 so
     * we can use setValue(value, format, offset);
     *
     * Flags (8bit) + Temperature Measurement Value (float) = 5 bytes
     *
     * Flags:
     *   Temperature Units Flag (0) -> Celsius
     *   Time Stamp Flag (0) -> Time Stamp field not present
     *   Temperature Type Flag (0) -> Temperature Type field not present
     *   Unused (00000)
     */
    mTemperatureMeasurementCharacteristic.setValue(new byte[]{0b00000000, 0, 0, 0, 0});
    // Characteristic Value: [flags, 0, 0, 0, 0]

    int bits = Float.floatToIntBits(temperatureMeasurementValue);
    int exponent = (bits & EXPONENT_MASK) >>> EXPONENT_SHIFT;
    int mantissa = (bits & MANTISSA_MASK) >>> MANTISSA_SHIFT;

    mTemperatureMeasurementCharacteristic.setValue(mantissa, exponent,
        TEMPERATURE_MEASUREMENT_VALUE_FORMAT,
        /* offset */ 1);
    // Characteristic Value: [flags, temperature measurement value]
  }

  private void setTemperatureMeasurementTimerInterval(int measurementIntervalValueSeconds) {
    mTimer = new Timer();
    mTimer.scheduleAtFixedRate(new TimerTask() {
      @Override
      public void run() {
        getActivity().runOnUiThread(new Runnable() {
          @Override
          public void run() {
            mDelegate.sendNotificationToDevices(mTemperatureMeasurementCharacteristic);
          }
        });
      }
    }, 0 /* delay */, measurementIntervalValueSeconds * 1000);
  }

  private void cancelTimer() {
    if (mTimer != null) {
      mTimer.cancel();
    }
  }

  private void resetTimer(int measurementIntervalValue) {
    cancelTimer();
    setTemperatureMeasurementTimerInterval(measurementIntervalValue);
  }

  private boolean isValidTemperatureMeasurementValue(String s) {
    try {
      float value = Float.valueOf(s);
      return true;
    } catch (NumberFormatException e) {
      return false;
    }
  }

  private boolean isValidMeasurementIntervalValue(int value) {
    return (value >= MIN_MEASUREMENT_INTERVAL) && (value <= MAX_MEASUREMENT_INTERVAL);
  }

  @Override
  public int writeCharacteristic(BluetoothGattCharacteristic characteristic, int offset, byte[] value) {
    if (offset != 0) {
      return BluetoothGatt.GATT_INVALID_OFFSET;
    }
    // Measurement Interval is a 16bit characteristic
    if (value.length != 2) {
      return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
    }
    ByteBuffer byteBuffer = ByteBuffer.wrap(value);
    byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
    final int newMeasurementIntervalValue = byteBuffer.getShort();
    if (!isValidMeasurementIntervalValue(newMeasurementIntervalValue)) {
      return BluetoothGatt.GATT_FAILURE;
    }
    getActivity().runOnUiThread(new Runnable() {
      @Override
      public void run() {
        mMeasurementIntervalCharacteristic.setValue(newMeasurementIntervalValue,
            MEASUREMENT_INTERVAL_FORMAT,
            /* offset */ 0);
        if (mMeasurementIntervalCCCDescriptor.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) {
          resetTimer(newMeasurementIntervalValue);
          mTextViewNotifications.setText(R.string.notificationsEnabled);
        }
      }
    });
    return BluetoothGatt.GATT_SUCCESS;
  }

  @Override
  public void notificationsDisabled(BluetoothGattCharacteristic characteristic) {
    if (characteristic.getUuid() != TEMPERATURE_MEASUREMENT_UUID) {
      return;
    }
    cancelTimer();
    getActivity().runOnUiThread(new Runnable() {
      @Override
      public void run() {
        mTextViewNotifications.setText(R.string.notificationsNotEnabled);
      }
    });
  }

  @Override
  public void notificationsEnabled(BluetoothGattCharacteristic characteristic, boolean indicate) {
    if (characteristic.getUuid() != TEMPERATURE_MEASUREMENT_UUID) {
      return;
    }
    if (!indicate) {
      return;
    }
    getActivity().runOnUiThread(new Runnable() {
      @Override
      public void run() {
        int newMeasurementInterval = Integer.parseInt(mEditTextMeasurementInterval.getText()
            .toString());
        if (isValidMeasurementIntervalValue(newMeasurementInterval)) {
          mMeasurementIntervalCharacteristic.setValue(newMeasurementInterval,
              MEASUREMENT_INTERVAL_FORMAT,
            /* offset */ 0);
          resetTimer(newMeasurementInterval);
          mTextViewNotifications.setText(R.string.notificationsEnabled);
        }
      }
    });
  }

}