#![recursion_limit="256"]

#[macro_use]
extern crate cpp;

extern crate libc;

use std::ffi::CStr;
use std::ffi::CString;

use std::vec::Vec;
use std::collections::HashMap;
use std::convert::TryInto;

pub struct AudioProperties {
    pub length: i32,
    pub bitrate: i32,
    pub sample_rate: i32,
    pub channels: i32,
}

pub struct Tag {
    pub title: String,
    pub artist: String,
    pub album: String,
    pub album_artist: Option<String>,
    pub comment: String,
    pub genre: String,
    pub year: u32,
    pub track: u32,
    pub is_empty: bool,
}

pub struct File {
    pub properties: HashMap<String, Vec<String>>,
    pub tag: Tag,
    pub audio_properties: AudioProperties,
}
impl File {
    pub fn new<'a>(filename: &'a String) -> Result<Self, &str> {
        unsafe {
            let file = FileRef_Ptr::new(&filename)?;
            let tag = file.tag();
            let audio_properties = file.audio_properties();
            let album_artist: Option<String>;
            let properties = file.properties();
            album_artist =
                if properties.contains_key("ALBUMARTIST") &&
                   properties["ALBUMARTIST"].len() > 0 {
                    Some(properties["ALBUMARTIST"][0].clone())
                }
                else if properties.contains_key("ALBUM ARTIST") &&
                   properties["ALBUM ARTIST"].len() > 0 {
                    Some(properties["ALBUM ARTIST"][0].clone())
                }
                else {
                    None
                };
            Ok(File {
                properties: properties,
                tag: Tag {
                    title: tag.title(),
                    artist: tag.artist(),
                    album: tag.album(),
                    album_artist: album_artist,
                    comment: tag.comment(),
                    genre: tag.genre(),
                    year: tag.year(),
                    track: tag.track(),
                    is_empty: tag.is_empty(),
                },
                audio_properties: AudioProperties {
                    length: audio_properties.length(),
                    bitrate: audio_properties.bitrate(),
                    sample_rate: audio_properties.sample_rate(),
                    channels: audio_properties.channels(),
                },
            })
        }
    }
}

cpp!{{
    #include <iostream>
    #include <memory>
    #include <string.h>
    #include <taglib/tag.h>
    #include <taglib/fileref.h>
    #include <taglib/tpropertymap.h>
}}


cpp_class!(pub unsafe struct AudioProperties_Ptr as "TagLib::AudioProperties*");
impl AudioProperties_Ptr {
    unsafe fn length(self) -> i32 {
        cpp!([self as "TagLib::AudioProperties*"] -> i32 as "int32_t" {
            return self->length();
        })
    }
    unsafe fn bitrate(self) -> i32 {
        cpp!([self as "TagLib::AudioProperties*"] -> i32 as "int32_t" {
            return self->bitrate();
        })
    }
    unsafe fn sample_rate(self) -> i32 {
        cpp!([self as "TagLib::AudioProperties*"] -> i32 as "int32_t" {
            return self->sampleRate();
        })
    }
    unsafe fn channels(self) -> i32 {
        cpp!([self as "TagLib::AudioProperties*"] -> i32 as "int32_t" {
            return self->channels();
        })
    }
}

cpp_class!(pub unsafe struct Tag_Ptr as "TagLib::Tag*");
impl Tag_Ptr {
    unsafe fn title(self) -> String {
        let cstring = cpp!([self as "TagLib::Tag*"] -> *mut libc::c_char as "char*" {
            return strdup(self->title().toCString()); 
        });
        let string = CStr::from_ptr(cstring).to_string_lossy().to_string();
        cpp!([cstring as "char*"] {
            free(cstring);
        });
        return string;
    }
    unsafe fn artist(self) -> String {
        let cstring = cpp!([self as "TagLib::Tag*"] -> *mut libc::c_char as "char*" {
            return strdup(self->artist().toCString()); 
        });
        let string = CStr::from_ptr(cstring).to_string_lossy().to_string();
        cpp!([cstring as "char*"] {
            free(cstring);
        });
        return string;
    }
    unsafe fn album(self) -> String {
        let cstring = cpp!([self as "TagLib::Tag*"] -> *mut libc::c_char as "char*" {
            return strdup(self->album().toCString()); 
        });
        let string = CStr::from_ptr(cstring).to_string_lossy().to_string();
        cpp!([cstring as "char*"] {
            free(cstring);
        });
        return string;
    }
    unsafe fn comment(self) -> String {
        let cstring = cpp!([self as "TagLib::Tag*"] -> *mut libc::c_char as "char*" {
            return strdup(self->comment().toCString()); 
        });
        let string = CStr::from_ptr(cstring).to_string_lossy().to_string();
        cpp!([cstring as "char*"] {
            free(cstring);
        });
        return string;
    }
    unsafe fn genre(self) -> String {
        let cstring = cpp!(unsafe [self as "TagLib::Tag*"] -> *mut libc::c_char as "char*" {
            return strdup(self->genre().toCString()); 
        });
        let string = CStr::from_ptr(cstring).to_string_lossy().to_string();
        cpp!([cstring as "char*"] {
            free(cstring);
        });
        return string;
    }
    unsafe fn year(self) -> u32 {
        cpp!([self as "TagLib::Tag*"] -> u32 as "uint32_t" {
            return self->year();
        })
    }
    unsafe fn track(self) -> u32 {
        cpp!([self as "TagLib::Tag*"] -> u32 as "uint32_t" {
            return self->track();
        })
    }
    unsafe fn is_empty(self) -> bool {
        cpp!([self as "TagLib::Tag*"] -> bool as "bool" {
            return self->isEmpty();
        })
    }
}

cpp_class!(pub unsafe struct FileRef_Ptr as "std::unique_ptr<TagLib::FileRef>");
impl FileRef_Ptr {
    unsafe fn new<'a>(filename: &'a String) -> Result<Self, &str> {
        let c_str = CString::new(filename.as_bytes()).unwrap();
        let c_str_ptr = c_str.as_ptr();
        let file_ref = cpp!([c_str_ptr as "const char *"] -> FileRef_Ptr as "std::unique_ptr<TagLib::FileRef>" {
            return std::make_unique<TagLib::FileRef>(c_str_ptr);
        });
        match file_ref.is_null() {
            false => Ok(file_ref),
            true => Err("FileRef is null"),
        }
    }
    unsafe fn is_null(&self) -> bool {
        cpp!([self as "std::unique_ptr<TagLib::FileRef>*"] -> bool as "bool" {
            return (*self)->isNull();
        })
    }
    unsafe fn tag(&self) -> Tag_Ptr {
        cpp!([self as "std::unique_ptr<TagLib::FileRef>*"] -> Tag_Ptr as "TagLib::Tag*" {
            return (*self)->tag();
        })
    }
    unsafe fn audio_properties(&self) -> AudioProperties_Ptr {
        cpp!([self as "std::unique_ptr<TagLib::FileRef>*"] -> AudioProperties_Ptr as "TagLib::AudioProperties*" {
            return (*self)->audioProperties();
        })
    }
    unsafe fn properties(&self) -> HashMap<String, Vec<String>> {
        let mut property_map = HashMap::new();
        let property_map_ptr = &mut property_map;
        cpp!([self as "std::unique_ptr<TagLib::FileRef>*", property_map_ptr as "void *"] {
            TagLib::PropertyMap pmap = (*self)->file()->properties();
            for(auto e = pmap.begin(); e != pmap.end(); e++) {
                const char *field = e->first.toCString();
                size_t properties_size = e->second.size();
                char **properties = (char**)malloc(sizeof(char *) * properties_size);
                unsigned int i = 0;
                for(TagLib::StringList::ConstIterator j = e->second.begin(); j != e->second.end(); ++j) {
                    properties[i] = strdup(j->toCString());
                    i++;
                }
                rust!(add_to_map [field : *const libc::c_char as "const char *",
                                  properties : *const *const libc::c_char as "char **",
                                  properties_size : libc::size_t as "size_t",
                                  property_map_ptr : &mut HashMap<String, Vec<String>> as "void *"] {
                    let property_field = CStr::from_ptr(field).to_string_lossy().to_string();
                    let mut property_data = Vec::<String>::default();
                    let properties_size_signed = properties_size.try_into().unwrap();
                    for i in 0..properties_size_signed {
                        let to_append = CStr::from_ptr(*(properties.offset(i))).to_string_lossy().to_string();
                        property_data.push(to_append);
                    }
                    property_map_ptr.insert(property_field, property_data);
                });
                for(i = 0; i < properties_size; i++) {
                    free(properties[i]);
                }
                free(properties);
            }
        });
        property_map
    }
}
