/*  Copyright (C) 2021-2024 Daniel Dakhno

    This file is part of Gadgetbridge.

    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Gadgetbridge is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.vesc;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.vesc.VescCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;

public class VescDeviceSupport extends VescBaseDeviceSupport {
    BluetoothGattCharacteristic serialWriteCharacteristic, serialReadCharacteristic;

    public static final String COMMAND_SET_RPM = "nodomain.freeyourgadget.gadgetbridge.vesc.command.SET_RPM";
    public static final String COMMAND_SET_CURRENT = "nodomain.freeyourgadget.gadgetbridge.vesc.command.SET_CURRENT";
    public static final String COMMAND_SET_BREAK_CURRENT = "nodomain.freeyourgadget.gadgetbridge.vesc.command.SET_BREAK_CURRENT";
    public static final String COMMAND_GET_VALUES = "nodomain.freeyourgadget.gadgetbridge.vesc.command.GET_VALUES";
    public static final String EXTRA_RPM = "EXTRA_RPM";
    public static final String EXTRA_CURRENT = "EXTRA_CURRENT";
    public static final String EXTRA_VOLTAGE = "EXTRA_VOLTAGE";

    public static final String ACTION_GOT_VALUES = "nodomain.freeyourgadget.gadgetbridge.vesc.action.GOT_VALUES";

    private Logger logger = LoggerFactory.getLogger(getClass());

    private DeviceType deviceType;

    private ByteBuffer responseBuffer = ByteBuffer.allocate(100);

    public VescDeviceSupport(DeviceType type) {
        super();
        responseBuffer.order(ByteOrder.BIG_ENDIAN);

        deviceType = type;
        addSupportedService(UUID.fromString(VescCoordinator.UUID_SERVICE_SERIAL_NRF));
        addSupportedService(UUID.fromString(VescCoordinator.UUID_SERVICE_SERIAL_HM10));
    }

    @Override
    public void onFetchRecordedData(int dataTypes) {
        super.onFetchRecordedData(dataTypes);
        getValues();
    }

    @Override
    protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
        logger.debug("initializing device");

        builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));

        initBroadcast();


        this.serialWriteCharacteristic = getCharacteristic(UUID.fromString(VescCoordinator.UUID_CHARACTERISTIC_SERIAL_TX_NRF));
        this.serialReadCharacteristic = getCharacteristic(UUID.fromString(VescCoordinator.UUID_CHARACTERISTIC_SERIAL_RX_NRF));
        if(this.serialWriteCharacteristic == null || this.serialReadCharacteristic == null){
            this.serialWriteCharacteristic = getCharacteristic(UUID.fromString(VescCoordinator.UUID_CHARACTERISTIC_SERIAL_TX_HM10));
            this.serialReadCharacteristic = getCharacteristic(UUID.fromString(VescCoordinator.UUID_CHARACTERISTIC_SERIAL_RX_HM10));
        }

        builder.notify(this.serialReadCharacteristic, true);

        return builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
    }

    @Override
    public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorWrite(gatt, descriptor, status);
        getValues();

        return true;
    }

    @Override
    public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        handleRxCharacteristic(characteristic);

        return true;
    }

    private void handleRxCharacteristic(BluetoothGattCharacteristic characteristic) {
        if (characteristic != serialReadCharacteristic) return;

        responseBuffer.put(characteristic.getValue());
        short length = 0;
        int oldPosition = responseBuffer.position();
        responseBuffer.position(0);
        byte lengthType = responseBuffer.get();
        if (lengthType == 2) {
            length = responseBuffer.get();
        } else if (lengthType == 3) {
            length = responseBuffer.getShort();
        } else {
            return;
        }
        if (length == oldPosition - 5) {
            // whole message transmitted
            responseBuffer.position(oldPosition);
            handleResponseBuffer(responseBuffer);

            oldPosition = 0;
        }
        responseBuffer.position(oldPosition);
    }

    private void handleResponseBuffer(ByteBuffer responseBuffer) {
        int bufferLength = responseBuffer.position();
        int payloadStartPosition = responseBuffer.get(0);

        byte[] payload = new byte[bufferLength - 3 - payloadStartPosition];
        System.arraycopy(responseBuffer.array(), payloadStartPosition, payload, 0, payload.length);

        int actualCrc = CheckSums.getCRC16(payload, 0);
        int expectedCrc = responseBuffer.getShort(bufferLength - 3);

        byte responseType = payload[0];
        if (responseType == 0x04) {
            handleResponseValues(responseBuffer);
        }
    }

    private void handleResponseValues(ByteBuffer valueBuffer) {
        valueBuffer.position(3);
        float temp_mos = buffer_get_float16(valueBuffer, 1e1);
        float temp_motor = buffer_get_float16(valueBuffer, 1e1);
        float current_motor = buffer_get_float32(valueBuffer, 1e2);
        float current_in = buffer_get_float32(valueBuffer, 1e2);
        float id = buffer_get_float32(valueBuffer, 1e2);
        float iq = buffer_get_float32(valueBuffer, 1e2);
        float duty_now = buffer_get_float16(valueBuffer, 1e3);
        float rpm = buffer_get_float32(valueBuffer, 1e0);
        float v_in = buffer_get_float16(valueBuffer, 1e1);
        float amp_hours = buffer_get_float32(valueBuffer, 1e4);
        float amp_hours_charged = buffer_get_float32(valueBuffer, 1e4);
        float watt_hours = buffer_get_float32(valueBuffer, 1e4);
        float watt_hours_charged = buffer_get_float32(valueBuffer, 1e4);
        float tachometer = buffer_get_int32(valueBuffer);
        float tachometer_abs = buffer_get_int32(valueBuffer);

        handleBatteryVoltage(v_in);

        Intent intent = new Intent(ACTION_GOT_VALUES);
        intent.putExtra(EXTRA_VOLTAGE, v_in);
    }



    void handleBatteryVoltage(float voltage){
        SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
        float minimalVoltage = Float.parseFloat(prefs.getString(DeviceSettingsPreferenceConst.PREF_VESC_MINIMUM_VOLTAGE, "-1"));
        float maximalVoltage = Float.parseFloat(prefs.getString(DeviceSettingsPreferenceConst.PREF_VESC_MAXIMUM_VOLTAGE, "-1"));

        if(minimalVoltage == -1){
            return;
        }
        if(maximalVoltage == -1){
            return;
        }

        float voltageAboveMinimum = voltage - minimalVoltage;
        float voltageRange = maximalVoltage - minimalVoltage;
        float fullness = voltageAboveMinimum / voltageRange;

        int fullnessPercent = (int)(fullness * 100);
        fullnessPercent = Math.max(fullnessPercent, 0);
        fullnessPercent = Math.min(fullnessPercent, 100);

        getDevice().setBatteryLevel(fullnessPercent);
        getDevice().setBatteryVoltage(voltage);
        getDevice().sendDeviceUpdateIntent(getContext());
    }

    float buffer_get_float16(ByteBuffer buffer, double scale){
        return (float) (buffer.getShort() / scale);
    }

    float buffer_get_float32(ByteBuffer buffer, double scale){
        return (float) (buffer.getInt() / scale);
    }

    int buffer_get_int32(ByteBuffer buffer){
        return buffer.getInt();
    }

    @Override
    public void onTestNewFunction() {
        getValues();
        // getDecodedADC();
    }

    private void getDecodedADC() {
        buildAndQueryPacket(CommandType.COMM_GET_DECODED_ADC);
    }

    private void initBroadcast() {
        LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());

        IntentFilter filter = new IntentFilter();
        filter.addAction(COMMAND_SET_RPM);
        filter.addAction(COMMAND_SET_CURRENT);
        filter.addAction(COMMAND_SET_BREAK_CURRENT);
        filter.addAction(COMMAND_GET_VALUES);

        broadcastManager.registerReceiver(commandReceiver, filter);
    }

    @Override
    public void dispose() {
        super.dispose();
        LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(commandReceiver);
    }

    BroadcastReceiver commandReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(COMMAND_SET_RPM)) {
                VescDeviceSupport.this.setRPM(
                        intent.getIntExtra(EXTRA_RPM, 0)
                );
            } else if (intent.getAction().equals(COMMAND_SET_BREAK_CURRENT)) {
                VescDeviceSupport.this.setBreakCurrent(
                        intent.getIntExtra(EXTRA_CURRENT, 0)
                );
            } else if (intent.getAction().equals(COMMAND_SET_CURRENT)) {
                VescDeviceSupport.this.setCurrent(
                        intent.getIntExtra(EXTRA_CURRENT, 0)
                );
            } else if (intent.getAction().equals(COMMAND_GET_VALUES)) {
                VescDeviceSupport.this.getValues();
            }
        }
    };

    public void setCurrent(int currentMillisAmperes) {
        buildAndQueryPacket(CommandType.COMM_SET_CURRENT, currentMillisAmperes);
    }

    public void setBreakCurrent(int breakCurrentMillisAmperes) {
        buildAndQueryPacket(CommandType.COMM_SET_CURRENT_BRAKE, breakCurrentMillisAmperes);
    }

    public void getValues() {
        buildAndQueryPacket(CommandType.COMM_GET_VALUES);
    }

    public void setRPM(int rpm) {
        buildAndQueryPacket(CommandType.COMM_SET_RPM, rpm);
    }

    public void buildAndQueryPacket(CommandType commandType, Object... args) {
        byte[] data = buildPacket(commandType, args);
        queryPacket(data);
    }

    public void queryPacket(byte[] data) {
        new TransactionBuilder("write serial packet")
                .write(this.serialWriteCharacteristic, data)
                .queue(getQueue());
    }

    public byte[] buildPacket(CommandType commandType, Object... args) {
        int dataLength = 0;
        for (Object arg : args) {
            if (arg instanceof Integer) dataLength += 4;
            else if (arg instanceof Short) dataLength += 2;
        }
        ByteBuffer buffer = ByteBuffer.allocate(dataLength);

        for (Object arg : args) {
            if (arg instanceof Integer) buffer.putInt((Integer) arg);
            if (arg instanceof Short) buffer.putShort((Short) arg);
        }

        return buildPacket(commandType, buffer.array());
    }

    public byte[] buildPacket(CommandType commandType, byte[] data) {
        return buildPacket(commandType.getCommandByte(), data);
    }

    private byte[] buildPacket(byte commandByte, byte[] data) {
        byte[] contents = new byte[data.length + 1];
        contents[0] = commandByte;
        System.arraycopy(data, 0, contents, 1, data.length);
        return buildPacket(contents);
    }

    private byte[] buildPacket(byte[] contents) {
        int dataLength = contents.length;
        ByteBuffer buffer = ByteBuffer.allocate(dataLength + (dataLength < 256 ? 5 : 6));
        if (dataLength < 256) {
            buffer.put((byte) 0x02);
            buffer.put((byte) dataLength);
        } else {
            buffer.put((byte) 0x03);
            buffer.putShort((short) dataLength);
        }
        buffer.put(contents);
        buffer.putShort((short) CheckSums.getCRC16(contents, 0));
        buffer.put((byte) 0x03);

        return buffer.array();
    }
}
