/*
 * MIT License
 *
 * Copyright (c) 2017 Inova IT
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package si.inova.neatle.operation;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.support.annotation.RestrictTo;

import java.nio.charset.Charset;
import java.util.UUID;

/**
 * A result of a single command.
 */
public class CommandResult {

    /**
     * Characteristic value format type uint8
     */
    public static final int FORMAT_UINT8 = 0x11;

    /**
     * Characteristic value format type uint16
     */
    public static final int FORMAT_UINT16 = 0x12;

    /**
     * Characteristic value format type uint32
     */
    public static final int FORMAT_UINT32 = 0x14;

    /**
     * Characteristic value format type sint8
     */
    public static final int FORMAT_SINT8 = 0x21;

    /**
     * Characteristic value format type sint16
     */
    public static final int FORMAT_SINT16 = 0x22;

    /**
     * Characteristic value format type sint32
     */
    public static final int FORMAT_SINT32 = 0x24;

    private final UUID uuid;
    private final byte[] data;
    private final int status;
    private final long timestamp;

    CommandResult(UUID uuid, byte[] data, int status, long timestamp) {
        this.uuid = uuid;
        this.data = data;
        this.status = status;
        this.timestamp = timestamp;
    }

    /**
     * Returns the raw response of a command, in bytes.
     *
     * @return the response in bytes
     */
    public byte[] getValue() {
        return data;
    }

    /**
     * Returns the string representation of the command response (in UTF8 encoding). If the data
     * received is not a string, it throws an {@link IllegalStateException}.
     *
     * @return the string representation of the command response.
     * @throws IllegalStateException if the command data is not a UTF8 string
     */
    public String getValueAsString() {
        if (data == null) {
            return null;
        } else if (data.length == 0) {
            return "";
        } else {
            return new String(data, Charset.forName("UTF8"));
        }
    }

    /**
     * Returns the value of the command result represented as an int32 (int). It will thro a
     * {@link java.nio.BufferUnderflowException} if the data has less than 4 bytes (e.g. is not an int).
     *
     * @return the value of the data as an int.
     */
    public int getValueAsInt() {
        if (data.length > 4) {
            throw new IllegalStateException("Data has more than 4 bytes and cannot be converted to an integer");
        }

        int ret = 0;
        switch (data.length) {
            case 4:
                ret |= (data[3] & 0xFF) << ((data.length - 4) * 8);
            case 3:
                ret |= (data[2] & 0xFF) << ((data.length - 3) * 8);
            case 2:
                ret |= (data[1] & 0xFF) << ((data.length - 2) * 8);
            case 1:
                ret |= (data[0] & 0xFF) << ((data.length - 1) * 8);
        }

        return ret;
    }

    /**
     * Return the stored value of this characteristic.
     *
     * <p>The formatType parameter determines how the characteristic value
     * is to be interpreted. For example, settting formatType to
     * {@link BluetoothGattCharacteristic#FORMAT_UINT16} specifies that the first two bytes of the
     * characteristic value at the given offset are interpreted to generate the
     * return value.
     *
     * @param formatType The format type used to interpret the characteristic
     *                   value.
     * @param offset     Offset at which the integer value can be found.
     * @return Cached value of the characteristic or null of offset exceeds
     * value size.
     */
    public Integer getFormattedIntValue(int formatType, int offset) {
        if ((offset + formatType & 0xF) > data.length) return null;

        switch (formatType) {
            case FORMAT_UINT8:
                return unsignedByteToInt(data[offset]);
            case FORMAT_UINT16:
                return unsignedBytesToInt(data[offset], data[offset + 1]);
            case FORMAT_UINT32:
                return unsignedBytesToInt(data[offset], data[offset + 1],
                        data[offset + 2], data[offset + 3]);
            case FORMAT_SINT8:
                return unsignedToSigned(unsignedByteToInt(data[offset]), 8);
            case FORMAT_SINT16:
                return unsignedToSigned(unsignedBytesToInt(data[offset],
                        data[offset + 1]), 16);
            case FORMAT_SINT32:
                return unsignedToSigned(unsignedBytesToInt(data[offset],
                        data[offset + 1], data[offset + 2], data[offset + 3]), 32);
        }

        return null;
    }

    /**
     * Returns the UUID of the characteristic this data was read from.
     *
     * @return the uuid
     */
    public UUID getUUID() {
        return uuid;
    }

    /**
     * Returns the status of the command execution. For instance, {@link BluetoothGatt#GATT_SUCCESS}
     * if the command was successful.
     *
     * @return the status of the command execution.
     */
    public int getStatus() {
        return status;
    }

    /**
     * Returns the timestamp of this command.
     *
     * @return the timestamp
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * Checks if this command was successful.
     *
     * @return true if the command was succesful, false otherwise
     */
    public boolean wasSuccessful() {
        return status == BluetoothGatt.GATT_SUCCESS;
    }

    /**
     * Convert signed bytes to a 16-bit unsigned int.
     */
    private int unsignedBytesToInt(byte b0, byte b1) {
        return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8));
    }

    /**
     * Convert a signed byte to an unsigned int.
     */
    private int unsignedByteToInt(byte b) {
        return b & 0xFF;
    }

    /**
     * Convert an unsigned integer value to a two's-complement encoded
     * signed value.
     */
    private int unsignedToSigned(int unsigned, int size) {
        if ((unsigned & (1 << size - 1)) != 0) {
            unsigned = -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
        }
        return unsigned;
    }

    /**
     * Convert signed bytes to a 32-bit unsigned int.
     */
    private int unsignedBytesToInt(byte b0, byte b1, byte b2, byte b3) {
        return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8))
                + (unsignedByteToInt(b2) << 16) + (unsignedByteToInt(b3) << 24);
    }

    @Override
    public String toString() {
        return "CommandResult[status: " + status + ", uuid:" + uuid + ", data:" + (data == null ? "null" : data.length) + "]";
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static CommandResult createCharacteristicRead(BluetoothGattCharacteristic characteristic, int status) {
        long when = System.currentTimeMillis();
        return new CommandResult(characteristic.getUuid(), characteristic.getValue(), status, when);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static CommandResult createErrorResult(UUID characteristicUUID, int error) {
        long when = System.currentTimeMillis();
        return new CommandResult(characteristicUUID, null, error, when);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static CommandResult createEmptySuccess(UUID characteristicUUID) {
        long when = System.currentTimeMillis();
        return new CommandResult(characteristicUUID, null, BluetoothGatt.GATT_SUCCESS, when);
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static CommandResult createCharacteristicChanged(BluetoothGattCharacteristic characteristic) {
        long when = System.currentTimeMillis();
        return new CommandResult(characteristic.getUuid(), characteristic.getValue(), BluetoothGatt.GATT_SUCCESS, when);
    }
}