/*
 * Copyright 2015 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.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;

import java.util.Arrays;
import java.util.UUID;

public class HeartRateServiceFragment extends ServiceFragment {
  private static final String TAG = HeartRateServiceFragment.class.getCanonicalName();
  private static final int MIN_UINT = 0;
  private static final int MAX_UINT8 = (int) Math.pow(2, 8) - 1;
  private static final int MAX_UINT16 = (int) Math.pow(2, 16) - 1;
  /**
   * See <a href="https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.heart_rate.xml">
   * Heart Rate Service</a>
   */
  private static final UUID HEART_RATE_SERVICE_UUID = UUID
      .fromString("0000180D-0000-1000-8000-00805f9b34fb");

  /**
   * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml">
   * Heart Rate Measurement</a>
   */
  private static final UUID HEART_RATE_MEASUREMENT_UUID = UUID
      .fromString("00002A37-0000-1000-8000-00805f9b34fb");
  private static final int HEART_RATE_MEASUREMENT_VALUE_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT8;
  private static final int INITIAL_HEART_RATE_MEASUREMENT_VALUE = 60;
  private static final int EXPENDED_ENERGY_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT16;
  private static final int INITIAL_EXPENDED_ENERGY = 0;
  private static final String HEART_RATE_MEASUREMENT_DESCRIPTION = "Used to send a heart rate " +
      "measurement";

  /**
   * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.body_sensor_location.xml">
   * Body Sensor Location</a>
   */
  private static final UUID BODY_SENSOR_LOCATION_UUID = UUID
      .fromString("00002A38-0000-1000-8000-00805f9b34fb");
  private static final int LOCATION_OTHER = 0;

  /**
   * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_control_point.xml">
   * Heart Rate Control Point</a>
   */
  private static final UUID HEART_RATE_CONTROL_POINT_UUID = UUID
      .fromString("00002A39-0000-1000-8000-00805f9b34fb");

  private BluetoothGattService mHeartRateService;
  private BluetoothGattCharacteristic mHeartRateMeasurementCharacteristic;
  private BluetoothGattCharacteristic mBodySensorLocationCharacteristic;
  private BluetoothGattCharacteristic mHeartRateControlPoint;

  private ServiceFragmentDelegate mDelegate;

  private EditText mEditTextHeartRateMeasurement;
  private final OnEditorActionListener mOnEditorActionListenerHeartRateMeasurement = new OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        String newHeartRateMeasurementValueString = textView.getText().toString();
        if (isValidCharacteristicValue(newHeartRateMeasurementValueString,
            HEART_RATE_MEASUREMENT_VALUE_FORMAT)) {
          int newHeartRateMeasurementValue = Integer.parseInt(newHeartRateMeasurementValueString);
          mHeartRateMeasurementCharacteristic.setValue(newHeartRateMeasurementValue,
              HEART_RATE_MEASUREMENT_VALUE_FORMAT,
              /* offset */ 1);
        } else {
          Toast.makeText(getActivity(), R.string.heartRateMeasurementValueInvalid,
              Toast.LENGTH_SHORT).show();
        }
      }
      return false;
    }
  };

  private final OnEditorActionListener mOnEditorActionListenerEnergyExpended = new OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        String newEnergyExpendedString = textView.getText().toString();
        if (isValidCharacteristicValue(newEnergyExpendedString,
            EXPENDED_ENERGY_FORMAT)) {
          int newEnergyExpended = Integer.parseInt(newEnergyExpendedString);
          mHeartRateMeasurementCharacteristic.setValue(newEnergyExpended,
              EXPENDED_ENERGY_FORMAT,
              /* offset */ 2);
        } else {
          Toast.makeText(getActivity(), R.string.energyExpendedInvalid,
              Toast.LENGTH_SHORT).show();
        }
      }
      return false;
    }
  };
  private EditText mEditTextEnergyExpended;
  private Spinner mSpinnerBodySensorLocation;

  private final OnItemSelectedListener mLocationSpinnerOnItemSelectedListener =
      new OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
          setBodySensorLocationValue(position);
        }

        @Override
        public void onNothingSelected(AdapterView<?> parent) {
        }
      };

  private final OnClickListener mNotifyButtonListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
      mDelegate.sendNotificationToDevices(mHeartRateMeasurementCharacteristic);
    }
  };

  public HeartRateServiceFragment() {
    mHeartRateMeasurementCharacteristic =
        new BluetoothGattCharacteristic(HEART_RATE_MEASUREMENT_UUID,
            BluetoothGattCharacteristic.PROPERTY_NOTIFY,
            /* No permissions */ 0);

    mHeartRateMeasurementCharacteristic.addDescriptor(
        Peripheral.getClientCharacteristicConfigurationDescriptor());

    mHeartRateMeasurementCharacteristic.addDescriptor(
        Peripheral.getCharacteristicUserDescriptionDescriptor(HEART_RATE_MEASUREMENT_DESCRIPTION));

    mBodySensorLocationCharacteristic =
        new BluetoothGattCharacteristic(BODY_SENSOR_LOCATION_UUID,
            BluetoothGattCharacteristic.PROPERTY_READ,
            BluetoothGattCharacteristic.PERMISSION_READ);

    mHeartRateControlPoint =
        new BluetoothGattCharacteristic(HEART_RATE_CONTROL_POINT_UUID,
            BluetoothGattCharacteristic.PROPERTY_WRITE,
            BluetoothGattCharacteristic.PERMISSION_WRITE);

    mHeartRateService = new BluetoothGattService(HEART_RATE_SERVICE_UUID,
        BluetoothGattService.SERVICE_TYPE_PRIMARY);
    mHeartRateService.addCharacteristic(mHeartRateMeasurementCharacteristic);
    mHeartRateService.addCharacteristic(mBodySensorLocationCharacteristic);
    mHeartRateService.addCharacteristic(mHeartRateControlPoint);
  }


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

    View view = inflater.inflate(R.layout.fragment_heart_rate, container, false);
    mSpinnerBodySensorLocation = (Spinner) view.findViewById(R.id.spinner_bodySensorLocation);
    mSpinnerBodySensorLocation.setOnItemSelectedListener(mLocationSpinnerOnItemSelectedListener);
    mEditTextHeartRateMeasurement = (EditText) view
        .findViewById(R.id.editText_heartRateMeasurementValue);
    mEditTextHeartRateMeasurement
        .setOnEditorActionListener(mOnEditorActionListenerHeartRateMeasurement);
    mEditTextEnergyExpended = (EditText) view
        .findViewById(R.id.editText_energyExpended);
    mEditTextEnergyExpended
        .setOnEditorActionListener(mOnEditorActionListenerEnergyExpended);
    Button notifyButton = (Button) view.findViewById(R.id.button_heartRateMeasurementNotify);
    notifyButton.setOnClickListener(mNotifyButtonListener);

    setHeartRateMeasurementValue(INITIAL_HEART_RATE_MEASUREMENT_VALUE,
        INITIAL_EXPENDED_ENERGY);
    setBodySensorLocationValue(LOCATION_OTHER);
    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 BluetoothGattService getBluetoothGattService() {
    return mHeartRateService;
  }

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

  private void setHeartRateMeasurementValue(int heartRateMeasurementValue, int expendedEnergy) {

    Log.d(TAG, Arrays.toString(mHeartRateMeasurementCharacteristic.getValue()));
    /* Set the org.bluetooth.characteristic.heart_rate_measurement
     * characteristic to a byte array of size 4 so
     * we can use setValue(value, format, offset);
     *
     * Flags (8bit) + Heart Rate Measurement Value (uint8) + Energy Expended (uint16) = 4 bytes
     *
     * Flags = 1 << 3:
     *   Heart Rate Format (0) -> UINT8
     *   Sensor Contact Status (00) -> Not Supported
     *   Energy Expended (1) -> Field Present
     *   RR-Interval (0) -> Field not pressent
     *   Unused (000)
     */
    mHeartRateMeasurementCharacteristic.setValue(new byte[]{0b00001000, 0, 0, 0});
    // Characteristic Value: [flags, 0, 0, 0]
    mHeartRateMeasurementCharacteristic.setValue(heartRateMeasurementValue,
        HEART_RATE_MEASUREMENT_VALUE_FORMAT,
        /* offset */ 1);
    // Characteristic Value: [flags, heart rate value, 0, 0]
    mEditTextHeartRateMeasurement.setText(Integer.toString(heartRateMeasurementValue));
    mHeartRateMeasurementCharacteristic.setValue(expendedEnergy,
        EXPENDED_ENERGY_FORMAT,
        /* offset */ 2);
    // Characteristic Value: [flags, heart rate value, energy expended (LSB), energy expended (MSB)]
    mEditTextEnergyExpended.setText(Integer.toString(expendedEnergy));
  }
  private void setBodySensorLocationValue(int location) {
    mBodySensorLocationCharacteristic.setValue(new byte[]{(byte) location});
    mSpinnerBodySensorLocation.setSelection(location);
  }

  private boolean isValidCharacteristicValue(String s, int format) {
    try {
      int value = Integer.parseInt(s);
      if (format == BluetoothGattCharacteristic.FORMAT_UINT8) {
        return (value >= MIN_UINT) && (value <= MAX_UINT8);
      } else if (format == BluetoothGattCharacteristic.FORMAT_UINT16) {
        return (value >= MIN_UINT) && (value <= MAX_UINT16);
      } else {
        throw new IllegalArgumentException(format + " is not a valid argument");
      }
    } catch (NumberFormatException e) {
      return false;
    }
  }

  @Override
  public int writeCharacteristic(BluetoothGattCharacteristic characteristic, int offset, byte[] value) {
    if (offset != 0) {
      return BluetoothGatt.GATT_INVALID_OFFSET;
    }
    // Heart Rate control point is a 8bit characteristic
    if (value.length != 1) {
      return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
    }
    if ((value[0] & 1) == 1) {
      getActivity().runOnUiThread(new Runnable() {
        @Override
        public void run() {
          mHeartRateMeasurementCharacteristic.setValue(INITIAL_EXPENDED_ENERGY,
              EXPENDED_ENERGY_FORMAT, /* offset */ 2);
          mEditTextEnergyExpended.setText(Integer.toString(INITIAL_EXPENDED_ENERGY));
        }
      });
    }
    return BluetoothGatt.GATT_SUCCESS;
  }

  @Override
  public void notificationsEnabled(BluetoothGattCharacteristic characteristic, boolean indicate) {
    if (characteristic.getUuid() != HEART_RATE_MEASUREMENT_UUID) {
      return;
    }
    if (indicate) {
      return;
    }
    getActivity().runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Toast.makeText(getActivity(), R.string.notificationsEnabled, Toast.LENGTH_SHORT)
            .show();
      }
    });
  }

  @Override
  public void notificationsDisabled(BluetoothGattCharacteristic characteristic) {
    if (characteristic.getUuid() != HEART_RATE_MEASUREMENT_UUID) {
      return;
    }
    getActivity().runOnUiThread(new Runnable() {
      @Override
      public void run() {
        Toast.makeText(getActivity(), R.string.notificationsNotEnabled, Toast.LENGTH_SHORT)
            .show();
      }
    });
  }
}