use crate::{
    error::{value_error, Error},
    io::BinaryData,
    micropython::{
        buffer::{hexlify_bytes, StrBuffer},
        gc::Gc,
        list::List,
        obj::Obj,
        util::{iter_into_array, try_or_raise},
    },
    storage::{get_avatar_len, load_avatar},
    strutil::TString,
    ui::{
        component::text::{
            paragraphs::{Paragraph, ParagraphSource},
            TextStyle,
        },
        util::set_animation_disabled,
    },
};

cfg_if::cfg_if! {
    if #[cfg(feature = "layout_bolt")] {
        use crate::ui::layout_bolt::theme;
    } else if #[cfg(feature = "layout_caesar")] {
        use crate::ui::layout_caesar::theme;
    } else if #[cfg(feature = "layout_delizia")] {
        use crate::ui::layout_delizia::theme;
    } else if #[cfg(feature = "layout_eckhart")] {
        use crate::ui::layout_eckhart::theme;
    } else {
        compile_error!("Non supported layout feature enabled");
    }
}

use theme::{
    PROPS_KEY_FONT, PROPS_SPACING, PROPS_VALUE_FONT, PROPS_VALUE_MONO_FONT, PROP_INNER_SPACING,
};

/// Maximum number of characters that can be displayed on screen at once. Used
/// for on-the-fly conversion of binary data to hexadecimal representation.
/// NOTE: can be fine-tuned for particular model screen to decrease memory
/// consumption and conversion time.
pub const MAX_HEX_CHARS_ON_SCREEN: usize = 256;

#[derive(Clone)]
pub enum StrOrBytes {
    Str(TString<'static>),
    Bytes(Obj),
}

impl StrOrBytes {
    pub fn as_str_offset(&self, offset: usize) -> TString<'static> {
        match self {
            StrOrBytes::Str(x) => x.skip_prefix(offset),
            StrOrBytes::Bytes(x) => hexlify_bytes(*x, offset, MAX_HEX_CHARS_ON_SCREEN)
                .unwrap_or_else(|_| StrBuffer::from("ERROR"))
                .into(),
        }
    }
}

impl TryFrom<Obj> for StrOrBytes {
    type Error = Error;

    fn try_from(obj: Obj) -> Result<Self, Error> {
        if obj.is_str() {
            Ok(StrOrBytes::Str(obj.try_into()?))
        } else if obj.is_bytes() {
            Ok(StrOrBytes::Bytes(obj))
        } else {
            Err(Error::TypeError)
        }
    }
}

impl From<TString<'static>> for StrOrBytes {
    fn from(value: TString<'static>) -> Self {
        StrOrBytes::Str(value)
    }
}

#[derive(Clone)]
pub struct ConfirmValueParams {
    pub description: TString<'static>,
    pub extra: TString<'static>,
    pub value: StrOrBytes,
    pub font: &'static TextStyle,
    pub description_font: &'static TextStyle,
    pub extra_font: &'static TextStyle,
}

impl ParagraphSource<'static> for ConfirmValueParams {
    fn at(&self, index: usize, offset: usize) -> Paragraph<'static> {
        match index {
            0 => Paragraph::new(self.description_font, self.description.skip_prefix(offset)),
            1 => Paragraph::new(self.extra_font, self.extra.skip_prefix(offset)),
            2 => Paragraph::new(self.font, self.value.as_str_offset(offset)),
            _ => unreachable!(),
        }
    }

    fn size(&self) -> usize {
        3
    }
}

pub struct PropsList {
    items: Gc<List>,
    key_font: &'static TextStyle,
    value_font: &'static TextStyle,
    value_mono_font: &'static TextStyle,
    key_value_padding: i16,
    props_padding: i16,
}

impl PropsList {
    pub fn new_styled(
        obj: Obj,
        key_font: &'static TextStyle,
        value_font: &'static TextStyle,
        value_mono_font: &'static TextStyle,
        key_value_padding: i16,
        props_padding: i16,
    ) -> Result<Self, Error> {
        Ok(Self {
            items: obj.try_into()?,
            key_font,
            value_font,
            value_mono_font,
            key_value_padding,
            props_padding,
        })
    }

    pub fn new(obj: Obj) -> Result<Self, Error> {
        Self::new_styled(
            obj,
            &PROPS_KEY_FONT,
            &PROPS_VALUE_FONT,
            &PROPS_VALUE_MONO_FONT,
            PROP_INNER_SPACING,
            PROPS_SPACING,
        )
    }

    pub fn empty() -> Result<Self, Error> {
        let empty_list = List::alloc(&[])?; // Create an empty GC list
        Self::new(empty_list.into())
    }
}

impl ParagraphSource<'static> for PropsList {
    fn at(&self, index: usize, offset: usize) -> Paragraph<'static> {
        let block = move || {
            let entry = self.items.get(index / 2)?;
            let [key, value, value_is_mono]: [Obj; 3] = iter_into_array(entry)?;
            let value_is_mono: bool = bool::try_from(value_is_mono)?;
            let obj: Obj;
            let style: &TextStyle;

            if index % 2 == 0 {
                if !key.is_str() && key != Obj::const_none() {
                    return Err(Error::TypeError);
                }
                style = self.key_font;
                obj = key;
            } else {
                if value_is_mono {
                    style = self.value_mono_font;
                } else {
                    if value.is_bytes() {
                        return Err(Error::TypeError);
                    }
                    style = self.value_font;
                }
                obj = value;
            };

            if obj == Obj::const_none() {
                return Ok(Paragraph::new(style, StrBuffer::empty())
                    .with_bottom_padding(0)
                    .with_top_padding(0));
            }

            let para = if obj.is_str() {
                let content: StrBuffer = obj.try_into()?;
                Paragraph::new(style, content.skip_prefix(offset))
            } else if obj.is_bytes() {
                let content = hexlify_bytes(obj, offset, MAX_HEX_CHARS_ON_SCREEN)?;
                Paragraph::new(style, content)
            } else {
                return Err(Error::TypeError);
            };

            if obj == key && value != Obj::const_none() {
                Ok(para.with_bottom_padding(self.key_value_padding).no_break())
            } else if (obj == key && value == Obj::const_none() && index != self.size() - 2)
                || (obj == value && index != self.size() - 1)
            {
                Ok(para.with_bottom_padding(self.props_padding))
            } else {
                Ok(para)
            }
        };
        match block() {
            Ok(para) => para,
            Err(_) => Paragraph::new(self.value_font, StrBuffer::from("ERROR")),
        }
    }

    fn size(&self) -> usize {
        2 * self.items.len()
    }
}

/// RecoveryType as defined in `common/protob/messages-management.proto`,
/// used as arguments coming from micropython into rust world for layouts or
/// flows.
pub enum RecoveryType {
    Normal = 0,
    DryRun = 1,
    UnlockRepeatedBackup = 2,
}

// Converting `Obj` into `RecoveryType` enum
#[cfg(feature = "micropython")]
impl TryFrom<Obj> for RecoveryType {
    type Error = Error;

    fn try_from(obj: Obj) -> Result<Self, Self::Error> {
        let val = u32::try_from(obj)?;
        let this = Self::try_from(val)?;
        Ok(this)
    }
}

// Converting `u32` to `RecoveryType`
impl TryFrom<u32> for RecoveryType {
    type Error = Error;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(RecoveryType::Normal),
            1 => Ok(RecoveryType::DryRun),
            2 => Ok(RecoveryType::UnlockRepeatedBackup),
            _ => Err(value_error!(c"Invalid RecoveryType")),
        }
    }
}

pub extern "C" fn upy_disable_animation(disable: Obj) -> Obj {
    let block = || {
        set_animation_disabled(disable.try_into()?);
        Ok(Obj::const_none())
    };
    unsafe { try_or_raise(block) }
}

pub fn get_user_custom_image() -> Result<BinaryData<'static>, Error> {
    let len = get_avatar_len()?;
    let mut data = Gc::<[u8]>::new_slice(len)?;
    // SAFETY: buffer is freshly allocated so nobody else has it.
    load_avatar(unsafe { Gc::<[u8]>::as_mut(&mut data) })?;
    Ok(data.into())
}

/// ContentType is used to define the type of content that can be displayed
/// in a layout or flow. It can either be an address or a public key.
pub enum ContentType {
    Address(TString<'static>),
    PublicKey(TString<'static>),
}

/// Maximum number of extended public keys (xpubs) that can be displayed in
/// a layout or flow.
pub const MAX_XPUBS: usize = 16;
