// Copyright (c) 2015 T. Okubo
// This file is part of vlc-rs.
// Licensed under the MIT license, see the LICENSE file.

use crate::enums::*;
use crate::tools::{from_cstr, from_cstr_ref, linked_list_iter, to_cstr};
use libc::{c_char, c_int, c_void};
use std::borrow::Cow;
use std::convert::TryInto;
use std::ffi::CString;
use std::fmt;
use std::i32;
use std::ptr;
use vlc_sys as sys;

#[derive(Debug)]
pub struct InternalError;

impl fmt::Display for InternalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "libvlc internal error")
    }
}

impl std::error::Error for InternalError {}


linked_list_iter!(
    libvlc_module_description, // C raw type
    module_description, // Rust base struct name
    {
      name: (String, str),
      shortname: (String, str),
      longname: (String, str),
      help: (String, str),
    } // fields
);

linked_list_iter!(
    libvlc_audio_output, // C raw type
    audio_output, // Rust base struct name
    {
        name: (String, str),
        description: (String, str),
    } // fields
);

linked_list_iter!(
    libvlc_audio_output_device, // C raw type
    audio_output_device, // Rust base struct name
    {
        device: (String, str),
        description: (String, str),
    } // fields
);

/// Retrieve libvlc version.
pub fn version() -> String {
    unsafe {
        from_cstr_ref(sys::libvlc_get_version())
            .unwrap()
            .into_owned()
    }
}

/// Retrieve libvlc compiler version.
pub fn compiler() -> String {
    unsafe {
        from_cstr_ref(sys::libvlc_get_compiler())
            .unwrap()
            .into_owned()
    }
}

pub struct Instance {
    pub(crate) ptr: *mut sys::libvlc_instance_t,
}

unsafe impl Send for Instance {}

impl Instance {
    /// Create and initialize a libvlc instance with specified args.
    /// Note: args.len() has to be less or equal to i32::MAX
    /// Note: libvlc discourages using arguments as these are not guaranteed to be stable between different versions of libvlc
    pub fn with_args(args: Option<Vec<String>>) -> Option<Instance> {
        let args_c_ptr: Vec<*const c_char>;
        let args_c: Vec<CString>;
        if let Some(argv) = args {
            args_c = argv
                .into_iter()
                .map(|x| CString::new(x).expect("Error: Unexpected null byte"))
                .collect();
            args_c_ptr = args_c.iter().map(|x| x.as_ptr()).collect();
        } else {
            args_c_ptr = Vec::new();
        }

        unsafe {
            let p = if args_c_ptr.is_empty() {
                sys::libvlc_new(0, ptr::null())
            } else {
                sys::libvlc_new(args_c_ptr.len() as i32, args_c_ptr.as_ptr())
            };

            if p.is_null() {
                return None;
            }

            Some(Instance { ptr: p })
        }
    }

    /// Create and initialize a libvlc instance.
    pub fn new() -> Option<Instance> {
        Instance::with_args(None)
    }

    /// Try to start a user interface for the libvlc instance.
    pub fn add_intf(&self, name: &str) -> Result<(), InternalError> {
        let cstr = to_cstr(name);

        let result = unsafe { sys::libvlc_add_intf(self.ptr, cstr.as_ptr()) };

        if result == 0 {
            Ok(())
        } else {
            Err(InternalError)
        }
    }

    /// Sets the application name.
    /// LibVLC passes this as the user agent string when a protocol requires it.
    pub fn set_user_agent(&self, name: &str, http: &str) {
        unsafe {
            sys::libvlc_set_user_agent(self.ptr, to_cstr(name).as_ptr(), to_cstr(http).as_ptr());
        }
    }

    /// Waits until an interface causes the instance to exit.
    pub fn wait(&self) {
        unsafe { sys::libvlc_wait(self.ptr) };
    }

    /// Sets some meta-information about the application.
    pub fn set_app_id(&self, id: &str, version: &str, icon: &str) {
        unsafe {
            sys::libvlc_set_app_id(
                self.ptr,
                to_cstr(id).as_ptr(),
                to_cstr(version).as_ptr(),
                to_cstr(icon).as_ptr(),
            );
        }
    }

    /// Returns a list of audio filters that are available.
    pub fn audio_filter_list_get(&self) -> Option<ModuleDescriptionList> {
        unsafe {
            let p = sys::libvlc_audio_filter_list_get(self.ptr);
            if p.is_null() {
                None
            } else {
                Some(ModuleDescriptionList::new(p))
            }
        }
    }

    /// Returns a list of video filters that are available.
    pub fn video_filter_list_get(&self) -> Option<ModuleDescriptionList> {
        unsafe {
            let p = sys::libvlc_video_filter_list_get(self.ptr);
            if p.is_null() {
                None
            } else {
                Some(ModuleDescriptionList::new(p))
            }
        }
    }

    /// Returns the VLM event manager
    pub fn vlm_event_manager<'a>(&'a self) -> EventManager<'a> {
        unsafe {
            let p = sys::libvlc_vlm_get_event_manager(self.ptr);
            assert!(!p.is_null());
            EventManager {
                ptr: p,
                _phantomdata: ::std::marker::PhantomData,
            }
        }
    }

    /// Set logging callback
    pub fn set_log<F: Fn(LogLevel, Log, Cow<str>) + Send + 'static>(&self, f: F) {
        let cb: Box<Box<dyn Fn(LogLevel, Log, Cow<str>) + Send + 'static>> = Box::new(Box::new(f));

        unsafe {
            sys::libvlc_log_set(self.ptr, Some(logging_cb), Box::into_raw(cb) as *mut _);
        }
    }

    /// Returns raw pointer
    pub fn raw(&self) -> *mut sys::libvlc_instance_t {
        self.ptr
    }
}

impl Drop for Instance {
    fn drop(&mut self) {
        unsafe {
            sys::libvlc_release(self.ptr);
        }
    }
}

const BUF_SIZE: usize = 1024; // Write log message to the buffer by vsnprintf.
unsafe extern "C" fn logging_cb(
    data: *mut c_void,
    level: c_int,
    ctx: *const sys::libvlc_log_t,
    fmt: *const c_char,
    args: *mut sys::__va_list_tag,
) {
    let f: &Box<dyn Fn(LogLevel, Log, Cow<str>) + Send + 'static> = ::std::mem::transmute(data);
    let mut buf: [c_char; BUF_SIZE] = [0; BUF_SIZE];

    sys::vsnprintf(buf.as_mut_ptr(), BUF_SIZE.try_into().unwrap(), fmt, args);

    f(
        (level as u32).into(),
        Log { ptr: ctx },
        from_cstr_ref(buf.as_ptr()).unwrap(),
    );
}

pub fn errmsg() -> Option<String> {
    unsafe { from_cstr(sys::libvlc_errmsg()) }
}

pub fn clearerr() {
    unsafe { sys::libvlc_clearerr() };
}

#[derive(Clone, Debug)]
pub enum Event {
    MediaMetaChanged(Meta),
    MediaSubItemAdded,
    MediaDurationChanged(i64),
    MediaParsedChanged(i32),
    MediaFreed,
    MediaStateChanged(State),
    MediaSubItemTreeAdded,

    MediaPlayerMediaChanged,
    MediaPlayerNothingSpecial,
    MediaPlayerOpening,
    MediaPlayerBuffering(f32),
    MediaPlayerPlaying,
    MediaPlayerPaused,
    MediaPlayerStopped,
    MediaPlayerForward,
    MediaPlayerBackward,
    MediaPlayerEndReached,
    MediaPlayerEncounteredError,
    MediaPlayerTimeChanged,
    MediaPlayerPositionChanged(f32),
    MediaPlayerSeekableChanged,
    MediaPlayerPausableChanged,
    MediaPlayerTitleChanged,
    MediaPlayerSnapshotTaken,
    MediaPlayerLengthChanged,
    MediaPlayerVout,
    MediaPlayerScrambledChanged,

    MediaListItemAdded,
    MediaListWillAddItem,
    MediaListItemDeleted,
    MediaListWillDeleteItem,

    MediaListViewItemAdded,
    MediaListViewWillAddItem,
    MediaListViewItemDeleted,
    MediaListViewWillDeleteItem,

    MediaListPlayerPlayed,
    MediaListPlayerNextItemSet,
    MediaListPlayerStopped,

    MediaDiscovererStarted,
    MediaDiscovererEnded,

    VlmMediaAdded(Option<String>, Option<String>),
    VlmMediaRemoved(Option<String>, Option<String>),
    VlmMediaChanged(Option<String>, Option<String>),
    VlmMediaInstanceStarted(Option<String>, Option<String>),
    VlmMediaInstanceStopped(Option<String>, Option<String>),
    VlmMediaInstanceStatusInit(Option<String>, Option<String>),
    VlmMediaInstanceStatusOpening(Option<String>, Option<String>),
    VlmMediaInstanceStatusPlaying(Option<String>, Option<String>),
    VlmMediaInstanceStatusPause(Option<String>, Option<String>),
    VlmMediaInstanceStatusEnd(Option<String>, Option<String>),
    VlmMediaInstanceStatusError(Option<String>, Option<String>),
}

pub struct EventManager<'a> {
    pub(crate) ptr: *mut sys::libvlc_event_manager_t,
    pub(crate) _phantomdata: ::std::marker::PhantomData<&'a sys::libvlc_event_manager_t>,
}

impl<'a> EventManager<'a> {
    pub fn attach<F>(&self, event_type: EventType, callback: F) -> Result<(), InternalError>
    where
        F: Fn(Event, VLCObject) + Send + 'static,
    {
        // Explicit type annotation is needed
        let callback: Box<Box<dyn Fn(Event, VLCObject) + Send + 'static>> =
            Box::new(Box::new(callback));

        let result = unsafe {
            sys::libvlc_event_attach(
                self.ptr,
                event_type as i32,
                Some(event_manager_callback),
                Box::into_raw(callback) as *mut c_void,
            )
        };

        if result == 0 {
            Ok(())
        } else {
            Err(InternalError)
        }
    }

    /// Returns raw pointer
    pub fn raw(&self) -> *mut sys::libvlc_event_manager_t {
        self.ptr
    }
}

unsafe extern "C" fn event_manager_callback(pe: *const sys::libvlc_event_t, data: *mut c_void) {
    let f: &Box<dyn Fn(Event, VLCObject) + Send + 'static> = ::std::mem::transmute(data);

    f(conv_event(pe), VLCObject { ptr: (*pe).p_obj });
}

// Convert c-style libvlc_event_t to Event
fn conv_event(pe: *const sys::libvlc_event_t) -> Event {
    let event_type: EventType = (unsafe { (*pe).type_ } as u32).into();

    match event_type {
        EventType::MediaMetaChanged => unsafe {
            Event::MediaMetaChanged((*pe).u.media_meta_changed.meta_type.into())
        },
        EventType::MediaSubItemAdded => Event::MediaSubItemAdded,
        EventType::MediaDurationChanged => unsafe {
            Event::MediaDurationChanged((*pe).u.media_duration_changed.new_duration)
        },
        EventType::MediaParsedChanged => unsafe {
            Event::MediaParsedChanged((*pe).u.media_parsed_changed.new_status)
        },
        EventType::MediaFreed => Event::MediaFreed,
        EventType::MediaStateChanged => unsafe {
            let new_state: sys::libvlc_state_t =
                (*pe).u.media_state_changed.new_state.try_into().unwrap();
            Event::MediaStateChanged(new_state.into())
        },
        EventType::MediaSubItemTreeAdded => Event::MediaSubItemTreeAdded,
        EventType::MediaPlayerMediaChanged => Event::MediaPlayerMediaChanged,
        EventType::MediaPlayerNothingSpecial => Event::MediaPlayerNothingSpecial,
        EventType::MediaPlayerOpening => Event::MediaPlayerOpening,
        EventType::MediaPlayerBuffering => unsafe {
            Event::MediaPlayerBuffering((*pe).u.media_player_buffering.new_cache)
        },
        EventType::MediaPlayerPlaying => Event::MediaPlayerPlaying,
        EventType::MediaPlayerPaused => Event::MediaPlayerPaused,
        EventType::MediaPlayerStopped => Event::MediaPlayerStopped,
        EventType::MediaPlayerForward => Event::MediaPlayerForward,
        EventType::MediaPlayerBackward => Event::MediaPlayerBackward,
        EventType::MediaPlayerEndReached => Event::MediaPlayerEndReached,
        EventType::MediaPlayerEncounteredError => Event::MediaPlayerEncounteredError,
        EventType::MediaPlayerTimeChanged => Event::MediaPlayerTimeChanged,
        EventType::MediaPlayerPositionChanged => unsafe {
            Event::MediaPlayerPositionChanged((*pe).u.media_player_position_changed.new_position)
        },
        EventType::MediaPlayerSeekableChanged => Event::MediaPlayerSeekableChanged,
        EventType::MediaPlayerPausableChanged => Event::MediaPlayerPausableChanged,
        EventType::MediaPlayerTitleChanged => Event::MediaPlayerTitleChanged,
        EventType::MediaPlayerSnapshotTaken => Event::MediaPlayerSnapshotTaken,
        EventType::MediaPlayerLengthChanged => Event::MediaPlayerLengthChanged,
        EventType::MediaPlayerVout => Event::MediaPlayerVout,
        EventType::MediaPlayerScrambledChanged => Event::MediaPlayerScrambledChanged,
        EventType::MediaListItemAdded => Event::MediaListItemAdded,
        EventType::MediaListWillAddItem => Event::MediaListWillAddItem,
        EventType::MediaListItemDeleted => Event::MediaListItemDeleted,
        EventType::MediaListWillDeleteItem => Event::MediaListWillDeleteItem,
        EventType::MediaListViewItemAdded => Event::MediaListViewItemAdded,
        EventType::MediaListViewWillAddItem => Event::MediaListViewWillAddItem,
        EventType::MediaListViewItemDeleted => Event::MediaListViewItemDeleted,
        EventType::MediaListViewWillDeleteItem => Event::MediaListViewWillDeleteItem,
        EventType::MediaListPlayerPlayed => Event::MediaListPlayerPlayed,
        EventType::MediaListPlayerNextItemSet => Event::MediaListPlayerNextItemSet,
        EventType::MediaListPlayerStopped => Event::MediaListPlayerStopped,
        EventType::MediaDiscovererStarted => Event::MediaDiscovererStarted,
        EventType::MediaDiscovererEnded => Event::MediaDiscovererEnded,
        EventType::VlmMediaAdded => unsafe {
            Event::VlmMediaAdded(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaRemoved => unsafe {
            Event::VlmMediaRemoved(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaChanged => unsafe {
            Event::VlmMediaChanged(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStarted => unsafe {
            Event::VlmMediaInstanceStarted(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStopped => unsafe {
            Event::VlmMediaInstanceStopped(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStatusInit => unsafe {
            Event::VlmMediaInstanceStatusInit(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStatusOpening => unsafe {
            Event::VlmMediaInstanceStatusOpening(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStatusPlaying => unsafe {
            Event::VlmMediaInstanceStatusPlaying(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStatusPause => unsafe {
            Event::VlmMediaInstanceStatusPause(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStatusEnd => unsafe {
            Event::VlmMediaInstanceStatusEnd(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
        EventType::VlmMediaInstanceStatusError => unsafe {
            Event::VlmMediaInstanceStatusError(
                from_cstr((*pe).u.vlm_media_event.psz_instance_name),
                from_cstr((*pe).u.vlm_media_event.psz_media_name),
            )
        },
    }
}

pub struct VLCObject {
    ptr: *mut c_void,
}

impl VLCObject {
    /// Returns raw pointer
    pub fn raw(&self) -> *mut c_void {
        self.ptr
    }
}

pub struct Log {
    pub(crate) ptr: *const sys::libvlc_log_t,
}

impl Log {
    /// Returns raw pointer
    pub fn raw(&self) -> *const sys::libvlc_log_t {
        self.ptr
    }
}