/*
 * Bluegiga’s Bluetooth Smart Android SW for Bluegiga BLE modules
 * Contact: [email protected].
 *
 * This is free software distributed under the terms of the MIT license reproduced below.
 *
 * Copyright (c) 2013, Bluegiga Technologies
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files ("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:
 *
 * THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF 
 * ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A  PARTICULAR PURPOSE.
 */
package com.siliconlabs.bledemo.bluetoothdatamodel.parsing;

import androidx.core.util.Pair;
import android.text.TextUtils;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;

// Converters - converts value between different numeral system
public class Converters {

    private static final char[] hexArray = "0123456789ABCDEF".toCharArray();

    // Gets value in hexadecimal system
    public static String getHexValue(byte[] value) {
        if (value == null) {
            return "";
        }

        char[] hexChars = new char[value.length * 3];
        int v;
        for (int j = 0; j < value.length; j++) {
            v = value[j] & 0xFF;
            hexChars[j * 3] = hexArray[v >>> 4];
            hexChars[j * 3 + 1] = hexArray[v & 0x0F];
            hexChars[j * 3 + 2] = ' ';
        }
        return new String(hexChars);
    }

    // Gets value in hexadecimal system for single byte
    public static String getHexValue(byte b) {
        char[] hexChars = new char[2];
        int v;
        v = b & 0xFF;
        hexChars[0] = hexArray[v >>> 4];
        hexChars[1] = hexArray[v & 0x0F];
        return new String(hexChars);
    }

    // Gets value in ascii system
    public static String getAsciiValue(byte[] value) {
        if (value == null) {
            return "";
        }

        StringBuilder builder = new StringBuilder();
        for(int i=0; i<value.length; i++) {
            if(32 <= value[i] && value[i] <= 126) builder.append((char)value[i]);
            else builder.append("\uFFFD");
        }

        return builder.toString();
    }

    // Gets value in decimal system
    public static String getDecimalValue(byte[] value) {
        if (value == null) {
            return "";
        }

        StringBuilder result = new StringBuilder();
        for (byte b : value) {
            result.append((int) b & 0xff).append(" ");
        }
        return result.toString();
    }

    // Gets value in decimal system for single byte
    public static String getDecimalValue(byte b) {
        String result = "";
        result += ((int) b & 0xff);

        return result;
    }

    // Gets value in decimal system for single byte
    public static String getDecimalValueFromTwosComplement(byte b) {
        // the first bit of the byte in twos complement
        String result = "" + b;

        if ((b & 0xa0) > 0) {
            int val = b;
            val = ~val & 0xff;
            val = val + 0x01;

            // the sign of the value
            int sign = (b >>> 7) & 0x01;
            sign = sign > 0 ? -1 : 1;

            result = "" + (sign * val);
        }
        return result;
    }

    public static String getDecimalValueFromTwosComplement(String binaryString) {
        // default to hex value

        if (binaryString.length() > 64) {
            String binAsHex = (new BigInteger(binaryString, 2)).toString(16);
            return "0x" + binAsHex;
        }

        // prepend the sign up to 64 bits
        String result = "";
        StringBuilder stringPrependExtendSign = new StringBuilder(binaryString);
        for (int i = 0; i < 64 - binaryString.length(); i++) {
            stringPrependExtendSign.insert(0, binaryString.substring(0, 1));
        }

        // flip the bits (needed for negative numbers)
        StringBuilder flippedBits = new StringBuilder();
        for (int i = 0; i < 64; i++) {
            if (binaryString.subSequence(0, 1).equals("1")) {
                // flip bits if negative twos complement negative
                if (stringPrependExtendSign.substring(i, i + 1).equals("1")) {
                    flippedBits.append(0);
                } else {
                    flippedBits.append(1);
                }
            }
        }

        // if prepended sign extension is negative, add one to flipped bits and make long neg.
        if (binaryString.subSequence(0, 1).equals("1")) {
            // finish twos complement calculation if negative twos complement number
            long flippedBitsAsLong = Long.parseLong(flippedBits.toString(), 2);
            flippedBitsAsLong += 1;
            flippedBitsAsLong = -1 * flippedBitsAsLong;
            result = "" + flippedBitsAsLong;
        } else {
            result = "" + Long.parseLong(stringPrependExtendSign.toString(), 2);
        }

        return result;
    }

    public static Pair<byte[], Boolean> convertStringTo(String input, String format) {
        if (TextUtils.isEmpty(input)) {
            return new Pair<>(input.getBytes(), true);
        }

        byte[] returnVal;

        // note that java uses big endian
        switch (format) {
            case "utf8s":
                returnVal = convertToUTF8(input);
                break;
            case "utf16s":
                returnVal = convertToUTF16(input);
                break;
            case "uint8":
                return convertToUint8(input);
            case "uint16":
                return convertToUint16(input);
            case "uint24":
                return convertToUint24(input);
            case "uint32":
                return convertToUint32(input);
            case "uint40":
                return convertToUint40(input);
            case "uint48":
                return convertToUint48(input);
            case "sint8":
                return convertToSint8(input);
            case "sint16":
                return convertToSint16(input);
            case "sint24":
                return convertToSint24(input);
            case "sint32":
                return convertToSint32(input);
            case "sint40":
                return convertToSint40(input);
            case "sint48":
                return convertToSint48(input);
            case "float32":
                return convertToFloat32(input);
            case "float64":
                return convertToFloat64(input);
            default:
                return new Pair<>(input.getBytes(), true);
        }

        return new Pair<>(returnVal, true);
    }

    public static Pair<byte[], Boolean> convertToFloat32(String input) {
        try {
            float floatVal = Float.parseFloat(input);

            int intBits = Float.floatToIntBits(floatVal);
            byte[] returnVal = new byte[4];
            returnVal[0] = (byte) (intBits & 0xff);
            returnVal[1] = (byte) ((intBits >>> 8) & 0xff);
            returnVal[2] = (byte) ((intBits >>> 16) & 0xff);
            returnVal[3] = (byte) ((intBits >>> 24) & 0xff);

            return new Pair<>(returnVal, true);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToFloat64(String input) {
        try {
            double floatVal = Double.parseDouble(input);
            long longBits = Double.doubleToLongBits(floatVal);
            byte[] returnVal = new byte[8];
            returnVal[0] = (byte) (longBits & 0xff);
            returnVal[1] = (byte) ((longBits >>> 8) & 0xff);
            returnVal[2] = (byte) ((longBits >>> 16) & 0xff);
            returnVal[3] = (byte) ((longBits >>> 24) & 0xff);
            returnVal[4] = (byte) ((longBits >>> 32) & 0xff);
            returnVal[5] = (byte) ((longBits >>> 40) & 0xff);
            returnVal[6] = (byte) ((longBits >>> 48) & 0xff);
            returnVal[7] = (byte) ((longBits >>> 56) & 0xff);

            return new Pair<>(returnVal, true);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }



    public static Pair<byte[], Boolean> convertToSint8(String input) {
        try {
            int val = Integer.parseInt(input);

            byte[] returnVal = new byte[1];
            returnVal[0] = (byte) (val & 0xFF);

            Boolean inRage = isValueInRange(-128, 127, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToSint16(String input) {
        try {
            int val = Integer.parseInt(input);

            byte[] returnVal = new byte[2];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);

            Boolean inRage = isValueInRange(-32768, 32767, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToSint24(String input) {
        try {
            int val = Integer.parseInt(input);

            byte[] returnVal = new byte[3];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);

            Boolean inRage = isValueInRange(-8388608, 8388607, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToSint32(String input) {
        try {
            long val = Long.parseLong(input);

            byte[] returnVal = new byte[4];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);
            returnVal[3] = (byte) ((val >>> 24) & 0xFF);

            Boolean inRage = isValueInRange(-2147483648L, 2147483647L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToSint40(String input) {
        try {
            long val = Long.parseLong(input);

            byte[] returnVal = new byte[5];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);
            returnVal[3] = (byte) ((val >>> 24) & 0xFF);
            returnVal[4] = (byte) ((val >>> 32) & 0xFF);

            Boolean inRage = isValueInRange(-140737488355328L, 140737488355327L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToSint48(String input) {
        try {
            long val = Long.parseLong(input);

            byte[] returnVal = new byte[6];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);
            returnVal[3] = (byte) ((val >>> 24) & 0xFF);
            returnVal[4] = (byte) ((val >>> 32) & 0xFF);
            returnVal[5] = (byte) ((val >>> 40) & 0xFF);

            Boolean inRage = isValueInRange(-140737488355328L, 140737488355327L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToUint8(String input) {
        try {
            int val = Integer.parseInt(input);

            byte[] returnVal = new byte[1];
            returnVal[0] = (byte) (val & 0xFF);

            Boolean inRage = isValueInRange(0, 255, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToUint16(String input) {
        try {
            int val = Integer.parseInt(input);

            byte[] returnVal = new byte[2];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);

            Boolean inRage = isValueInRange(0, 65535, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToUint24(String input) {
        try {
            int val = Integer.parseInt(input);

            byte[] returnVal = new byte[3];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);

            Boolean inRage = isValueInRange(0, 16777215L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToUint32(String input) {
        try {
            long val = Long.parseLong(input);

            byte[] returnVal = new byte[4];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);
            returnVal[3] = (byte) ((val >>> 24) & 0xFF);

            Boolean inRage = isValueInRange(0, 4294967295L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToUint40(String input) {
        try {
            long val = Long.parseLong(input);

            byte[] returnVal = new byte[5];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);
            returnVal[3] = (byte) ((val >>> 24) & 0xFF);
            returnVal[4] = (byte) ((val >>> 32) & 0xFF);

            Boolean inRage = isValueInRange(0, 281474976710655L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static Pair<byte[], Boolean> convertToUint48(String input) {
        try {
            long val = Long.parseLong(input);

            byte[] returnVal = new byte[6];
            returnVal[0] = (byte) (val & 0xFF);
            returnVal[1] = (byte) ((val >>> 8) & 0xFF);
            returnVal[2] = (byte) ((val >>> 16) & 0xFF);
            returnVal[3] = (byte) ((val >>> 24) & 0xFF);
            returnVal[4] = (byte) ((val >>> 32) & 0xFF);
            returnVal[5] = (byte) ((val >>> 40) & 0xFF);

            Boolean inRage = isValueInRange(0, 281474976710655L, val);

            return new Pair<>(returnVal, inRage);
        } catch (Exception e) {
            e.printStackTrace();
            return new Pair<>(input.getBytes(), false);
        }
    }

    public static byte[] convertToUTF8(String input) {
        byte[] returnVal = null;
        returnVal = input.getBytes(StandardCharsets.UTF_8);
        return returnVal;
    }

    public static byte[] convertToUTF16(String input) {
        byte[] returnVal = null;
        returnVal = input.getBytes(StandardCharsets.UTF_16);
        return returnVal;
    }

    private static boolean isValueInRange(int min, int max, int val) {
        return max > min ? val >= min && val <= max : val >= max && val <= min;
    }

    private static boolean isValueInRange(long min, long max, long val) {
        return max > min ? val >= min && val <= max : val >= max && val <= min;
    }
}