use std::error;
use std::ffi::{CString, NulError};
use std::fmt;
use std::os::raw::{c_char, c_int};
use std::ptr;

use crate::get_error;
use crate::video::Window;
use crate::Error;

use crate::sys;

bitflags! {
    #[derive(Debug)]
    pub struct MessageBoxFlag: u32 {
        const ERROR =
            sys::messagebox::SDL_MESSAGEBOX_ERROR ;
        const WARNING =
            sys::messagebox::SDL_MESSAGEBOX_WARNING ;
        const INFORMATION =
            sys::messagebox::SDL_MESSAGEBOX_INFORMATION ;
    }
}

bitflags! {
    #[derive(Debug)]
    pub struct MessageBoxButtonFlag: u32 {
        const ESCAPEKEY_DEFAULT =
            sys::messagebox::SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
        const RETURNKEY_DEFAULT =
            sys::messagebox::SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
        const NOTHING = 0;
    }
}

#[derive(Debug)]
pub struct MessageBoxColorScheme {
    pub background: (u8, u8, u8),
    pub text: (u8, u8, u8),
    pub button_border: (u8, u8, u8),
    pub button_background: (u8, u8, u8),
    pub button_selected: (u8, u8, u8),
}

impl From<MessageBoxColorScheme> for sys::messagebox::SDL_MessageBoxColorScheme {
    fn from(val: MessageBoxColorScheme) -> Self {
        sys::messagebox::SDL_MessageBoxColorScheme { colors: val.into() }
    }
}

impl From<sys::messagebox::SDL_MessageBoxColorScheme> for MessageBoxColorScheme {
    fn from(prim: sys::messagebox::SDL_MessageBoxColorScheme) -> MessageBoxColorScheme {
        prim.colors.into()
    }
}

/// `button_id` is the integer that will be returned
/// by `show_message_box`. It is not sed by SDL2,
/// and should only be used to know which button has been triggered
#[derive(Debug)]
pub struct ButtonData<'a> {
    pub flags: MessageBoxButtonFlag,
    pub button_id: i32,
    pub text: &'a str,
}

#[derive(Debug)]
pub enum ClickedButton<'a> {
    CloseButton,
    CustomButton(&'a ButtonData<'a>),
}

impl From<MessageBoxColorScheme> for [sys::messagebox::SDL_MessageBoxColor; 5] {
    fn from(scheme: MessageBoxColorScheme) -> [sys::messagebox::SDL_MessageBoxColor; 5] {
        fn to_message_box_color(t: (u8, u8, u8)) -> sys::messagebox::SDL_MessageBoxColor {
            sys::messagebox::SDL_MessageBoxColor {
                r: t.0,
                g: t.1,
                b: t.2,
            }
        }
        [
            to_message_box_color(scheme.background),
            to_message_box_color(scheme.text),
            to_message_box_color(scheme.button_border),
            to_message_box_color(scheme.button_background),
            to_message_box_color(scheme.button_selected),
        ]
    }
}

impl From<[sys::messagebox::SDL_MessageBoxColor; 5]> for MessageBoxColorScheme {
    fn from(val: [sys::messagebox::SDL_MessageBoxColor; 5]) -> Self {
        fn from_message_box_color(
            prim_color: sys::messagebox::SDL_MessageBoxColor,
        ) -> (u8, u8, u8) {
            (prim_color.r, prim_color.g, prim_color.b)
        }
        MessageBoxColorScheme {
            background: from_message_box_color(val[0]),
            text: from_message_box_color(val[1]),
            button_border: from_message_box_color(val[2]),
            button_background: from_message_box_color(val[3]),
            button_selected: from_message_box_color(val[4]),
        }
    }
}

#[derive(Debug, Clone)]
pub enum ShowMessageError {
    InvalidTitle(NulError),
    InvalidMessage(NulError),
    /// Second argument of the tuple (i32) corresponds to the
    /// first button_id having an error
    InvalidButton(NulError, i32),
    SdlError(Error),
}

impl fmt::Display for ShowMessageError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use self::ShowMessageError::*;

        match *self {
            InvalidTitle(ref e) => write!(f, "Invalid title: {e}"),
            InvalidMessage(ref e) => write!(f, "Invalid message: {e}"),
            InvalidButton(ref e, value) => write!(f, "Invalid button ({value}): {e}"),
            SdlError(ref e) => write!(f, "SDL error: {e}"),
        }
    }
}

impl error::Error for ShowMessageError {
    fn description(&self) -> &str {
        use self::ShowMessageError::*;

        match *self {
            InvalidTitle(_) => "invalid title",
            InvalidMessage(_) => "invalid message",
            InvalidButton(..) => "invalid button",
            SdlError(ref e) => &e.0,
        }
    }
}

/// Show a simple message box, meant to be informative only.
///
/// There is no way to know if the user clicked "Ok" or closed the message box,
/// If you want to retrieve which button was clicked and customize a bit more
/// your message box, use `show_message_box` instead.
#[doc(alias = "SDL_ShowSimpleMessageBox")]
pub fn show_simple_message_box<'a, W>(
    flags: MessageBoxFlag,
    title: &str,
    message: &str,
    window: W,
) -> Result<(), ShowMessageError>
where
    W: Into<Option<&'a Window>>,
{
    use self::ShowMessageError::*;
    let result = unsafe {
        let title = match CString::new(title) {
            Ok(s) => s,
            Err(err) => return Err(InvalidTitle(err)),
        };
        let message = match CString::new(message) {
            Ok(s) => s,
            Err(err) => return Err(InvalidMessage(err)),
        };
        sys::messagebox::SDL_ShowSimpleMessageBox(
            flags.bits(),
            title.as_ptr() as *const c_char,
            message.as_ptr() as *const c_char,
            window.into().map_or(ptr::null_mut(), |win| win.raw()),
        )
    };

    if result {
        Ok(())
    } else {
        Err(SdlError(get_error()))
    }
}

/// Show a customizable message box.
///
/// An array of buttons is required for it to work. The array can be empty,
/// but it will have no button beside the close button.
///
/// On success, it will return either the button clicked or the close button.
/// Note that the variant of the `ClickedButton` enum will also be returned if the message box
/// has been forcefully closed (Alt-F4, ...)
///
#[doc(alias = "SDL_ShowMessageBox")]
pub fn show_message_box<'a, 'b, W, M>(
    flags: MessageBoxFlag,
    buttons: &'a [ButtonData],
    title: &str,
    message: &str,
    window: W,
    scheme: M,
) -> Result<ClickedButton<'a>, ShowMessageError>
where
    W: Into<Option<&'b Window>>,
    M: Into<Option<MessageBoxColorScheme>>,
{
    let window = window.into();
    let scheme = scheme.into();

    use self::ShowMessageError::*;
    let mut button_id: c_int = 0;
    let title = match CString::new(title) {
        Ok(s) => s,
        Err(err) => return Err(InvalidTitle(err)),
    };
    let message = match CString::new(message) {
        Ok(s) => s,
        Err(err) => return Err(InvalidMessage(err)),
    };
    let button_texts: Result<Vec<_>, (_, i32)> = buttons
        .iter()
        .map(|b| CString::new(b.text).map_err(|e| (e, b.button_id)))
        .collect(); // Create CString for every button; and catch any CString Error
    let button_texts = match button_texts {
        Ok(b) => b,
        Err(e) => return Err(InvalidButton(e.0, e.1)),
    };
    let raw_buttons: Vec<sys::messagebox::SDL_MessageBoxButtonData> = buttons
        .iter()
        .zip(button_texts.iter())
        .map(|(b, b_text)| sys::messagebox::SDL_MessageBoxButtonData {
            flags: b.flags.bits(),
            buttonID: b.button_id as c_int,
            text: b_text.as_ptr(),
        })
        .collect();
    let result = unsafe {
        let msg_box_data = sys::messagebox::SDL_MessageBoxData {
            flags: flags.bits(),
            window: window.map_or(ptr::null_mut(), |win| win.raw()),
            title: title.as_ptr() as *const c_char,
            message: message.as_ptr() as *const c_char,
            numbuttons: raw_buttons.len() as c_int,
            buttons: raw_buttons.as_ptr(),
            colorScheme: if let Some(scheme) = scheme {
                &sys::messagebox::SDL_MessageBoxColorScheme {
                    colors: From::from(scheme),
                } as *const _
            } else {
                ptr::null()
            },
        };
        sys::messagebox::SDL_ShowMessageBox(&msg_box_data as *const _, &mut button_id as &mut _)
    };
    if result {
        match button_id {
            -1 => Ok(ClickedButton::CloseButton),
            id => {
                let button = buttons.iter().find(|b| b.button_id == id);
                Ok(ClickedButton::CustomButton(button.unwrap()))
            }
        }
    } else {
        Err(SdlError(get_error()))
    }
}
