use crate::Error;

const POSIMM_CODE: u8 = 0b00000000;
const POSIMM_MASK: u8 = 0b01111111;
const POSIMM_LAST: u8 = POSIMM_CODE + POSIMM_MASK;

const NEGIMM_CODE: u8 = 0b11100000;
const NEGIMM_MASK: u8 = 0b00011111;
const NEGIMM_LAST: u8 = NEGIMM_CODE + NEGIMM_MASK;

const STRIMM_CODE: u8 = 0b10000000;
const STRIMM_MASK: u8 = 0b00011111;
const STRIMM_LAST: u8 = STRIMM_CODE + STRIMM_MASK;

const ARRIMM_CODE: u8 = 0b10100000;
const ARRIMM_MASK: u8 = 0b00001111;
const ARRIMM_LAST: u8 = ARRIMM_CODE + ARRIMM_MASK;

const MAPIMM_CODE: u8 = 0b10110000;
const MAPIMM_MASK: u8 = 0b00001111;
const MAPIMM_LAST: u8 = MAPIMM_CODE + MAPIMM_MASK;

const BITS_8: u8 = 0b00;
const BITS_16: u8 = 0b01;
const BITS_32: u8 = 0b10;
const BITS_64: u8 = 0b11;

const FLEX: u8 = 0b11;

const ARRAY_CODE: u8 = 0b11000000;
const ARRAY_CODE_U8: u8 = ARRAY_CODE + BITS_8;
const ARRAY_CODE_U16: u8 = ARRAY_CODE + BITS_16;
const ARRAY_CODE_U32: u8 = ARRAY_CODE + BITS_32;
const ARRAY_CODE_FLEX: u8 = ARRAY_CODE + FLEX;

const MAP_CODE: u8 = 0b11000100;
const MAP_CODE_U8: u8 = MAP_CODE + BITS_8;
const MAP_CODE_U16: u8 = MAP_CODE + BITS_16;
const MAP_CODE_U32: u8 = MAP_CODE + BITS_32;
const MAP_CODE_FLEX: u8 = MAP_CODE + FLEX;

const UNSIGNED_CODE: u8 = 0b11001000;
const UNSIGNED_CODE_U8: u8 = UNSIGNED_CODE + BITS_8;
const UNSIGNED_CODE_U16: u8 = UNSIGNED_CODE + BITS_16;
const UNSIGNED_CODE_U32: u8 = UNSIGNED_CODE + BITS_32;
const UNSIGNED_CODE_U64: u8 = UNSIGNED_CODE + BITS_64;

const SIGNED_CODE: u8 = 0b11001100;
const SIGNED_CODE_U8: u8 = SIGNED_CODE + BITS_8;
const SIGNED_CODE_U16: u8 = SIGNED_CODE + BITS_16;
const SIGNED_CODE_U32: u8 = SIGNED_CODE + BITS_32;
const SIGNED_CODE_U64: u8 = SIGNED_CODE + BITS_64;

const STR_CODE: u8 = 0b11010000;
const STR_CODE_U8: u8 = STR_CODE + BITS_8;
const STR_CODE_U16: u8 = STR_CODE + BITS_16;
const STR_CODE_U32: u8 = STR_CODE + BITS_32;

const BIN_CODE: u8 = 0b11010100;
const BIN_CODE_U8: u8 = BIN_CODE + BITS_8;
const BIN_CODE_U16: u8 = BIN_CODE + BITS_16;
const BIN_CODE_U32: u8 = BIN_CODE + BITS_32;

const FLOAT_CODE_32: u8 = 0b11011010;
const FLOAT_CODE_64: u8 = FLOAT_CODE_32 + 1;

const BOOL_CODE_0: u8 = 0b11011100;
const BOOL_CODE_1: u8 = BOOL_CODE_0 + 1;

const NULL_CODE: u8 = 0b11011110;
const TERM_CODE: u8 = 0b11011111;

const BOOL_MASK: u8 = 0b00000001;

/// Data marker for the encoded data.
///
/// The marker identifies the data type of the encoded data immediately following the marker.
/// The marker itself is encoded as a numerical 8-bit value and can be followed by at most 9 bytes of additional data.
/// Some marker values support immediate data, i.e. the data represented by the marker is encoded into the marker itself.
/// Other marker values encode immediate length values.
///
/// For more information on each marker variant, see their individual documentation.
pub enum Marker {
    /// Immediate positive value in the range [0, 128).
    ImmPos {
        /// The encoded positive value.
        value: u8,
    },

    /// Immediate negative value in the range [-32, 0).
    ImmNeg {
        /// The encoded negative value.
        value: i8,
    },

    /// Immediate string length, in the range [0, 32).
    ImmStr {
        /// The encoded string length.
        len: u8,
    },

    /// Immediate array length, in the range [0, 16).
    ImmArr {
        /// The encoded array length.
        len: u8,
    },

    /// Immediate map length, in the range [0, 16).
    ImmMap {
        /// The encoded map length.
        len: u8,
    },

    /// The next 8 bits after the marker encode the length of an array, and the array data follows.
    A8,

    /// The next 16 bits after the marker encode the length of an array, and the array data follows.
    A16,

    /// The next 32 bits after the marker encode the length of an array, and the array data follows.
    A32,

    /// Immediately following the marker, a flexible array is encoded.
    /// The flexible array must be terminated by encoding a [`Term`](Marker::Term) marker after the last element of the array.
    AFlex,

    /// The next 8 bits after the marker encode the length of a map, and the map data follows.
    M8,

    /// The next 16 bits after the marker encode the length of a map, and the map data follows.
    M16,

    /// The next 32 bits after the marker encode the length of a map, and the map data follows.
    M32,

    /// Immediately following the marker, a flexible map is encoded.
    /// The flexible map must be terminated by encoding a [`Term`](Marker::Term) marker after the last element of the map.
    MFlex,

    /// The next 8 bits after the marker encode a `u8` value.
    U8,

    /// The next 16 bits after the marker encode a `u16` value.
    U16,

    /// The next 32 bits after the marker encode a `u32` value.
    U32,

    /// The next 64 bits after the marker encode a `u64` value.
    U64,

    /// The next 8 bits after the marker encode a `i8` value.
    I8,

    /// The next 16 bits after the marker encode a `i16` value.
    I16,

    /// The next 32 bits after the marker encode a `i32` value.
    I32,

    /// The next 64 bits after the marker encode a `i64` value.
    I64,

    /// The next 8 bits after the marker encode the length of a string, and the string data follows.
    S8,

    /// The next 16 bits after the marker encode the length of a string, and the string data follows.
    S16,

    /// The next 32 bits after the marker encode the length of a string, and the string data follows.
    S32,

    /// The next 8 bits after the marker encode the length of a byte array, and the binary data follows.
    B8,

    /// The next 16 bits after the marker encode the length of a byte array, and the binary data follows.
    B16,

    /// The next 32 bits after the marker encode the length of a byte array, and the binary data follows.
    B32,

    /// The next 32 bits after the marker encode a `f32` value.
    F32,

    /// The next 64 bits after the marker encode a `f64` value.
    F64,

    /// The marker encodes a boolean value.
    Bool {
        /// The encoded boolean value.
        value: bool,
    },

    /// The marker encodes a null or unit value, i.e. `()`.
    Null,

    /// The marker encodes a flexible container terminator.
    Term,

    /// Reserved, do not use.
    Reserved,
}

impl Marker {
    fn code(&self) -> u8 {
        match self {
            Marker::ImmPos { .. } => POSIMM_CODE,
            Marker::ImmNeg { .. } => NEGIMM_CODE,
            Marker::ImmStr { .. } => STRIMM_CODE,
            Marker::ImmArr { .. } => ARRIMM_CODE,
            Marker::ImmMap { .. } => MAPIMM_CODE,

            Marker::A8 => ARRAY_CODE_U8,
            Marker::A16 => ARRAY_CODE_U16,
            Marker::A32 => ARRAY_CODE_U32,
            Marker::AFlex => ARRAY_CODE_FLEX,

            Marker::M8 => MAP_CODE_U8,
            Marker::M16 => MAP_CODE_U16,
            Marker::M32 => MAP_CODE_U32,
            Marker::MFlex => MAP_CODE_FLEX,

            Marker::U8 => UNSIGNED_CODE_U8,
            Marker::U16 => UNSIGNED_CODE_U16,
            Marker::U32 => UNSIGNED_CODE_U32,
            Marker::U64 => UNSIGNED_CODE_U64,

            Marker::I8 => SIGNED_CODE_U8,
            Marker::I16 => SIGNED_CODE_U16,
            Marker::I32 => SIGNED_CODE_U32,
            Marker::I64 => SIGNED_CODE_U64,

            Marker::S8 => STR_CODE_U8,
            Marker::S16 => STR_CODE_U16,
            Marker::S32 => STR_CODE_U32,

            Marker::B8 => BIN_CODE_U8,
            Marker::B16 => BIN_CODE_U16,
            Marker::B32 => BIN_CODE_U32,

            Marker::F32 => FLOAT_CODE_32,
            Marker::F64 => FLOAT_CODE_64,

            Marker::Bool { .. } => BOOL_CODE_0,

            Marker::Null => NULL_CODE,
            Marker::Term => TERM_CODE,

            Marker::Reserved => panic!(),
        }
    }

    fn mask(&self) -> u8 {
        match self {
            Marker::ImmPos { .. } => POSIMM_MASK,
            Marker::ImmNeg { .. } => NEGIMM_MASK,
            Marker::ImmStr { .. } => STRIMM_MASK,
            Marker::ImmArr { .. } => ARRIMM_MASK,
            Marker::ImmMap { .. } => MAPIMM_MASK,

            Marker::Bool { .. } => BOOL_MASK,

            Marker::Reserved => panic!(),

            _ => 0,
        }
    }

    /// Given a marker, return the numerical encoding.
    pub fn encoding(&self) -> u8 {
        let code = self.code();
        let mask = self.mask();

        let imm = match self {
            Marker::ImmPos { value } => *value,
            Marker::ImmNeg { value } => *value as u8,
            Marker::ImmStr { len } | Marker::ImmArr { len } | Marker::ImmMap { len } => *len,
            Marker::Bool { value } => *value as u8,

            _ => 0,
        };

        code | (imm & mask)
    }

    /// Return the number of additional bytes following the marker for a complete read.
    ///
    /// This method only returns the number of bytes required by the marker.
    /// It does not count the data bytes following the marker, for example in the case of length markers.
    pub fn argument_bytes(&self) -> usize {
        match self {
            Marker::A8 | Marker::M8 | Marker::U8 | Marker::I8 | Marker::S8 | Marker::B8 => 1,
            Marker::A16 | Marker::M16 | Marker::U16 | Marker::I16 | Marker::S16 | Marker::B16 => 2,

            Marker::A32
            | Marker::M32
            | Marker::U32
            | Marker::I32
            | Marker::S32
            | Marker::B32
            | Marker::F32 => 4,

            Marker::U64 | Marker::I64 | Marker::F64 => 8,

            Marker::Reserved => panic!(),

            _ => 0,
        }
    }

    /// Try to decode a numerical value into a valid marker.
    ///
    /// # Errors
    /// This method returns [`BadMarker`](Error::BadMarker) if an attempt is made to decode reserved marker values.
    pub fn decode(value: u8) -> Result<Self, Error> {
        let marker = match value {
            POSIMM_CODE..=POSIMM_LAST => Marker::ImmPos { value },
            NEGIMM_CODE..=NEGIMM_LAST => Marker::ImmNeg { value: value as i8 },
            STRIMM_CODE..=STRIMM_LAST => Marker::ImmStr {
                len: value & STRIMM_MASK,
            },
            ARRIMM_CODE..=ARRIMM_LAST => Marker::ImmArr {
                len: value & ARRIMM_MASK,
            },
            MAPIMM_CODE..=MAPIMM_LAST => Marker::ImmMap {
                len: value & MAPIMM_MASK,
            },

            ARRAY_CODE_U8 => Marker::A8,
            ARRAY_CODE_U16 => Marker::A16,
            ARRAY_CODE_U32 => Marker::A32,
            ARRAY_CODE_FLEX => Marker::AFlex,

            MAP_CODE_U8 => Marker::M8,
            MAP_CODE_U16 => Marker::M16,
            MAP_CODE_U32 => Marker::M32,
            MAP_CODE_FLEX => Marker::MFlex,

            UNSIGNED_CODE_U8 => Marker::U8,
            UNSIGNED_CODE_U16 => Marker::U16,
            UNSIGNED_CODE_U32 => Marker::U32,
            UNSIGNED_CODE_U64 => Marker::U64,

            SIGNED_CODE_U8 => Marker::I8,
            SIGNED_CODE_U16 => Marker::I16,
            SIGNED_CODE_U32 => Marker::I32,
            SIGNED_CODE_U64 => Marker::I64,

            STR_CODE_U8 => Marker::S8,
            STR_CODE_U16 => Marker::S16,
            STR_CODE_U32 => Marker::S32,

            BIN_CODE_U8 => Marker::B8,
            BIN_CODE_U16 => Marker::B16,
            BIN_CODE_U32 => Marker::B32,

            FLOAT_CODE_32 => Marker::F32,
            FLOAT_CODE_64 => Marker::F64,

            BOOL_CODE_0 => Marker::Bool { value: false },
            BOOL_CODE_1 => Marker::Bool { value: true },

            NULL_CODE => Marker::Null,
            TERM_CODE => Marker::Term,

            _ => return Err(Error::BadMarker),
        };

        Ok(marker)
    }

    /// Return `true` if the marker represents a string data type.
    pub fn is_str(&self) -> bool {
        match self {
            Marker::ImmStr { .. } | Marker::S8 | Marker::S16 | Marker::S32 => true,
            _ => false,
        }
    }

    /// Return `true` if the marker represents a map data type, including flexible maps.
    pub fn is_map(&self) -> bool {
        match self {
            Marker::ImmMap { .. } | Marker::M8 | Marker::M16 | Marker::M32 | Marker::MFlex => true,
            _ => false,
        }
    }

    /// Return `true` if the marker represents an array data type, including flexible arrays.
    pub fn is_array(&self) -> bool {
        match self {
            Marker::ImmArr { .. } | Marker::A8 | Marker::A16 | Marker::A32 | Marker::AFlex => true,
            _ => false,
        }
    }
}
