/*  Copyright (C) 2020-2024 Yukai Li

    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.devices.lefun.commands;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants;

/**
 * Base class for Lefun Bluetooth commands and responses
 */
public abstract class BaseCommand {
    // Common constants
    /**
     * Common get operation type
     */
    public static final byte OP_GET = 0;
    /**
     * Common set operation type
     */
    public static final byte OP_SET = 1;

    /**
     * Calculates command checksum
     *
     * @param data   the data to generate checksum from
     * @param offset the offset in data to start calculating from
     * @param length the number of bytes to include in calculation
     * @return the computed checksum
     */
    public static byte calculateChecksum(byte[] data, int offset, int length) {
        int checksum = 0;
        for (int i = offset; i < offset + length; ++i) {
            byte b = data[i];
            for (int j = 0; j < 8; ++j) {
                if (((b ^ checksum) & 1) == 0) {
                    checksum >>= 1;
                } else {
                    checksum = (checksum ^ 0x18) >> 1 | 0x80;
                }
                b >>= 1;
            }
        }
        return (byte) checksum;
    }

    /**
     * When implemented in a subclass, parses the response from a device
     *
     * @param id     the command ID
     * @param params the params buffer
     */
    abstract protected void deserializeParams(byte id, ByteBuffer params);

    /**
     * When implemented in a subclass, provides the arguments to send in the command
     *
     * @param params the params buffer to write to
     * @return the command ID
     */
    abstract protected byte serializeParams(ByteBuffer params);

    /**
     * Deserialize a response from the device
     *
     * @param response the response data to deserialize
     */
    public void deserialize(byte[] response) {
        if (response.length < LefunConstants.CMD_HEADER_LENGTH || response.length < response[1])
            throw new IllegalArgumentException("Response is too short");
        if (calculateChecksum(response, 0, response[1] - 1) != response[response[1] - 1])
            throw new IllegalArgumentException("Incorrect message checksum");
        ByteBuffer buffer = ByteBuffer.wrap(response, LefunConstants.CMD_HEADER_LENGTH - 1,
                response[1] - LefunConstants.CMD_HEADER_LENGTH);
        buffer.order(ByteOrder.BIG_ENDIAN);
        deserializeParams(response[2], buffer);
    }

    /**
     * Serializes a command to send to the device
     *
     * @return the data to send to the device
     */
    public byte[] serialize() {
        ByteBuffer buffer = ByteBuffer.allocate(LefunConstants.CMD_MAX_LENGTH - LefunConstants.CMD_HEADER_LENGTH);
        buffer.order(ByteOrder.BIG_ENDIAN);
        byte id = serializeParams(buffer);
        return makeCommand(id, buffer);
    }

    /**
     * Builds a command given ID and parameters buffer.
     *
     * @param id     the command ID
     * @param params the parameters buffer
     * @return the assembled command buffer
     */
    protected byte[] makeCommand(byte id, ByteBuffer params) {
        if (params.position() > LefunConstants.CMD_MAX_LENGTH - LefunConstants.CMD_HEADER_LENGTH)
            throw new IllegalArgumentException("params is too long to fit");

        int paramsLength = params.position();
        byte[] request = new byte[paramsLength + LefunConstants.CMD_HEADER_LENGTH];
        request[0] = LefunConstants.CMD_REQUEST_ID;
        request[1] = (byte) request.length;
        request[2] = id;
        params.flip();
        params.get(request, LefunConstants.CMD_HEADER_LENGTH - 1, paramsLength);
        request[request.length - 1] = calculateChecksum(request, 0, request.length - 1);
        return request;
    }

    /**
     * Throws a standard parameters length exception
     */
    protected void throwUnexpectedLength() {
        throw new IllegalArgumentException("Unexpected parameters length");
    }

    /**
     * Checks for valid command ID and throws if wrong ID provided
     *
     * @param id         command ID from device
     * @param expectedId expected command ID
     */
    protected void validateId(byte id, byte expectedId) {
        if (id != expectedId)
            throw new IllegalArgumentException("Wrong command ID");
    }

    /**
     * Checks for valid command ID and command length
     *
     * @param id             command ID from device
     * @param params         params buffer from device
     * @param expectedId     expected command ID
     * @param expectedLength expected params length
     */
    protected void validateIdAndLength(byte id, ByteBuffer params, byte expectedId, int expectedLength) {
        validateId(id, expectedId);
        if (params.limit() - params.position() != expectedLength)
            throwUnexpectedLength();
    }

    /**
     * Gets whether a bit is set
     *
     * @param value the value to check against
     * @param mask  the bitmask
     * @return whether the bits indicated by the bitmask are set
     */
    protected boolean getBit(int value, int mask) {
        return (value & mask) != 0;
    }

    /**
     * Sets a bit in a value
     *
     * @param value the value to modify
     * @param mask  the bitmask
     * @param set   whether to set or clear the bits
     * @return the modified value
     */
    protected int setBit(int value, int mask, boolean set) {
        if (set) {
            return value | mask;
        } else {
            return value & ~mask;
        }
    }

    /**
     * Sets a bit in a value
     *
     * @param value the value to modify
     * @param mask  the bitmask
     * @param set   whether to set or clear the bits
     * @return the modified value
     */
    protected short setBit(short value, int mask, boolean set) {
        if (set) {
            return (short) (value | mask);
        } else {
            return (short) (value & ~mask);
        }
    }

    /**
     * Sets a bit in a value
     *
     * @param value the value to modify
     * @param mask  the bitmask
     * @param set   whether to set or clear the bits
     * @return the modified value
     */
    protected byte setBit(byte value, int mask, boolean set) {
        if (set) {
            return (byte) (value | mask);
        } else {
            return (byte) (value & ~mask);
        }
    }

    /**
     * Find index of first bit that is set
     *
     * @param value the value to look at
     * @return the index of the lowest set bit, starting at 0 for least significant bit; -1 if no bits set
     */
    protected int getLowestSetBitIndex(int value) {
        if (value == 0) return -1;

        int i = 0;
        while ((value & 1) == 0) {
            ++i;
            value >>= 1;
        }
        return i;
    }
}
