// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use byteorder::{LittleEndian, WriteBytesExt};

use crate::bitwidth::BitWidth;
use crate::bitwidth::BitWidth::*;
use crate::flexbuffer_type::FlexBufferType;
use crate::flexbuffer_type::FlexBufferType::*;

/// Internal representation of FlexBuffer Types and Data before writing.
/// These get placed on the builder's stack and are eventually commited.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Value {
    // Inline types
    Null,
    Int(i64),
    UInt(u64),
    Float(f64),
    Bool(bool),
    /// Null termintated, c_string. Only used with `Map`s.
    Key(usize),
    /// The other ~20 or so types.
    Reference {
        address: usize,
        child_width: BitWidth,
        fxb_type: FlexBufferType,
    },
}

macro_rules! new_typed_vector {
    ($name: ident, $v2: ident, $v3: ident, $v4: ident, $vn: ident) => {
        /// Returns a typed vector, fixed length if possible.
        /// Address and child width are zero initialized and must be set.
        pub fn $name(n: usize) -> Value {
            let address = 0;
            let child_width = W8;
            match n {
                2 => Value::Reference { address, child_width, fxb_type: $v2 },
                3 => Value::Reference { address, child_width, fxb_type: $v3 },
                4 => Value::Reference { address, child_width, fxb_type: $v4 },
                _ => Value::Reference { address, child_width, fxb_type: $vn },
            }
        }
    };
}

impl Value {
    pub fn new_vector() -> Self {
        Value::Reference { address: 0, child_width: W8, fxb_type: Vector }
    }
    pub fn new_map() -> Self {
        Value::Reference { address: 0, child_width: W8, fxb_type: Map }
    }
    new_typed_vector!(new_int_vector, VectorInt2, VectorInt3, VectorInt4, VectorInt);
    new_typed_vector!(new_uint_vector, VectorUInt2, VectorUInt3, VectorUInt4, VectorUInt);
    new_typed_vector!(new_float_vector, VectorFloat2, VectorFloat3, VectorFloat4, VectorFloat);
    pub fn fxb_type(&self) -> FlexBufferType {
        match *self {
            Value::Null => Null,
            Value::Int(_) => Int,
            Value::UInt(_) => UInt,
            Value::Float(_) => Float,
            Value::Bool(_) => Bool,
            Value::Key(_) => Key,
            Value::Reference { fxb_type, .. } => fxb_type,
        }
    }
    pub fn is_fixed_length_vector(&self) -> bool {
        self.fxb_type().is_fixed_length_vector()
    }
    pub fn is_inline(&self) -> bool {
        self.fxb_type().is_inline()
    }
    pub fn is_reference(&self) -> bool {
        !self.is_inline()
    }
    pub fn is_key(&self) -> bool {
        if let Value::Key(_) = self {
            true
        } else {
            false
        }
    }
    pub fn is_typed_vector_or_map(&self) -> bool {
        if let Value::Reference { fxb_type, .. } = self {
            fxb_type.is_heterogenous()
        } else {
            false
        }
    }
    pub fn prefix_length(&self) -> usize {
        if self.is_fixed_length_vector() || self.is_inline() {
            return 0;
        }
        if let Value::Reference { fxb_type, .. } = self {
            if *fxb_type == Map {
                return 3;
            }
        }
        1
    }
    pub fn set_fxb_type_or_panic(&mut self, new_type: FlexBufferType) {
        if let Value::Reference { fxb_type, .. } = self {
            *fxb_type = new_type;
        } else {
            panic!("`set_fxb_type_or_panic` called on {:?}", self)
        }
    }
    pub fn set_child_width_or_panic(&mut self, new_width: BitWidth) {
        if let Value::Reference { child_width, .. } = self {
            *child_width = new_width;
        } else {
            panic!("`set_child_width_or_panic` called on {:?}", self);
        }
    }
    pub fn get_address(&self) -> Option<usize> {
        if let Value::Reference { address, .. } | Value::Key(address) = self {
            Some(*address)
        } else {
            None
        }
    }
    pub fn set_address_or_panic(&mut self, new_address: usize) {
        if let Value::Reference { address, .. } | Value::Key(address) = self {
            *address = new_address;
        } else {
            panic!("`set_address_or_panic` called on {:?}", self);
        }
    }
    /// For inline types - the width of the value to be stored.
    /// For reference types, the width of the referred.
    /// Note Key types always refer to 8 bit data.
    pub fn width_or_child_width(&self) -> BitWidth {
        match *self {
            Value::Int(x) => x.into(),
            Value::UInt(x) => x.into(),
            Value::Float(x) => x.into(),
            Value::Key(_) | Value::Bool(_) | Value::Null => W8,
            Value::Reference { child_width, .. } => child_width,
        }
    }
    pub fn relative_address(self, written_at: usize) -> Option<Value> {
        self.get_address().map(|address| {
            let offset = written_at
                .checked_sub(address)
                .expect("Error: References may only refer backwards in buffer.");
            Value::UInt(offset as u64)
        })
    }
    /// Computes the minimum required width of `value` when stored in a vector
    /// starting at `vector_start` at index `idx` (this index includes the prefix).
    /// `Value::Reference{..}` variants require location information because
    /// offsets are relative.
    pub fn width_in_vector(self, vector_start: usize, idx: usize) -> BitWidth {
        match self {
            Value::Bool(_) => W8,
            Value::Null => W8,
            Value::Int(x) => x.into(),
            Value::UInt(x) => x.into(),
            Value::Float(x) => x.into(),
            _ => {
                debug_assert!(self.is_reference());
                for &width in BitWidth::iter() {
                    let bytes = width as usize + 1;
                    let alignment = (bytes - vector_start % bytes) % bytes;
                    let written_at = vector_start + alignment + idx * bytes;
                    // This match must always succeed.
                    if let Some(Value::UInt(offset)) = self.relative_address(written_at) {
                        if BitWidth::from(offset) == width {
                            return width;
                        }
                    }
                }
                unreachable!()
            }
        }
    }
    pub fn packed_type(self, parent_width: BitWidth) -> u8 {
        let width = if self.is_inline() {
            std::cmp::max(parent_width, self.width_or_child_width())
        } else {
            self.width_or_child_width()
        };
        (self.fxb_type() as u8) << 2 | width as u8
    }
}

pub fn find_vector_type<'a, T>(mut values: T) -> Value
where
    T: std::iter::Iterator<Item = &'a Value>,
{
    let first = values.next();
    if first.is_none() {
        return Value::new_vector();
    }
    let mut len = 1;
    let init = first.unwrap().fxb_type();
    for v in values {
        if v.fxb_type() != init {
            return Value::new_vector();
        }
        len += 1;
    }
    let vector_type = match init {
        Bool => VectorBool,
        UInt => return Value::new_uint_vector(len),
        Int => return Value::new_int_vector(len),
        Float => return Value::new_float_vector(len),
        Key => VectorKey,
        // Note that VectorString is deprecated for writing
        _ => return Value::new_vector(),
    };
    Value::Reference { address: 0, child_width: W8, fxb_type: vector_type }
}

#[inline]
pub fn store_value(buffer: &mut Vec<u8>, mut value: Value, width: BitWidth) {
    // Remap to number types.
    use Value::*;
    if let Some(offset) = value.relative_address(buffer.len()) {
        value = offset;
    } else {
        value = match value {
            Bool(x) => UInt(x.into()),
            Null => UInt(0), // Should this be 0 bytes?
            _ => value,
        }
    }
    let write_result = match (value, width) {
        (UInt(x), W8) => buffer.write_u8(x as u8),
        (UInt(x), W16) => buffer.write_u16::<LittleEndian>(x as u16),
        (UInt(x), W32) => buffer.write_u32::<LittleEndian>(x as u32),
        (UInt(x), W64) => buffer.write_u64::<LittleEndian>(x),
        (Int(x), W8) => buffer.write_i8(x as i8),
        (Int(x), W16) => buffer.write_i16::<LittleEndian>(x as i16),
        (Int(x), W32) => buffer.write_i32::<LittleEndian>(x as i32),
        (Int(x), W64) => buffer.write_i64::<LittleEndian>(x),
        (Float(x), W32) => buffer.write_f32::<LittleEndian>(x as f32),
        (Float(x), W64) => buffer.write_f64::<LittleEndian>(x),
        (Float(_), _) => unreachable!("Error: Flatbuffers does not support 8 and 16 bit floats."),
        _ => unreachable!("Variant not considered: {:?}", value),
    };
    write_result.unwrap_or_else(|err| {
        panic!("Error writing value {:?} with width {:?}: {:?}", value, width, err)
    });
}
