#![warn(missing_docs)]

//! # Emote Mapper
//!
//! `emote_mapper` is a collection of utilities to create and use emote maps from [`twitchmote`](https://github.com/ModProg/twitchmote)

#[cfg(feature = "regex")]
use std::borrow::Cow;
use std::{
    collections::HashMap,
    convert::TryFrom,
    fmt::Display,
    fs::File,
    io::{Read, Write},
    path::Path,
    str::FromStr,
};

use csv::Trim;
#[cfg(feature = "regex")]
use lazy_static::*;
#[cfg(feature = "regex")]
use regex::*;
use serde::*;
use serde_with::{serde_as, TryFromInto};
use thiserror::Error;
use EmoteMapperError::*;

/// The EmoteMapper holds a Map of emote names and code points
/// It can be used for:
/// - using an emote map stored as csv to map emotes
/// - storing an emote map as csv
pub struct EmoteMapper {
    map: HashMap<String, CodePoint>,
    emote_width: usize,
}

impl Default for EmoteMapper {
    fn default() -> Self { Self { map: Default::default(), emote_width: 3 } }
}

impl EmoteMapper {
    /// The emote width used for `to_string` and `replace_all`
    ///
    /// **NOTE:** This depends both on the emote and the primary font used by the terminal
    pub fn emote_width(mut self, emote_width: usize) -> Self {
        self.emote_width = emote_width;
        self
    }

    /// Returns the char representing the `emote_name`
    pub fn to_char(&self, emote_name: &str) -> Option<char> { self.map.get(emote_name).copied().map(char::from) }

    /// Returns a string with the char representing the `emote_name` padded to `emte_width`
    pub fn to_string(&self, emote_name: &str) -> Option<String> {
        self.to_char(emote_name).map(|c| format!("{}{}", c, " ".repeat(self.emote_width - 1)))
    }

    /// Replaces all emotes in the `message` given
    #[cfg(feature = "regex")]
    pub fn replace_all<'a>(&self, message: &'a str) -> Cow<'a, str> {
        lazy_static! {
            static ref RE: Regex = Regex::new(r"[^,.\s]*").unwrap();
        }

        RE.replace_all(message, |w: &Captures| {
            self.to_string(w.get(0).unwrap().as_str()).unwrap_or_else(|| w.get(0).unwrap().as_str().to_owned())
        })
    }
}

/// Parsing
impl EmoteMapper {
    /// Creates an EmoteMapper from a Reader containing csv data
    pub fn from_reader(rdr: impl Read) -> Result<Self, EmoteMapperError> {
        let mut rdr = csv::ReaderBuilder::new().has_headers(false).trim(Trim::All).from_reader(rdr);

        let map = rdr
            .deserialize::<Mapping>()
            .map(|r| {
                let r: Mapping = r.map_err(StringParsingError)?;
                Ok((r.name, r.code_point))
            })
            .collect::<Result<_, EmoteMapperError>>()?;
        Ok(EmoteMapper { map, ..EmoteMapper::default() })
    }

    /// Creates an EmoteMapper from a File containing csv data
    pub fn from_path(p: &Path) -> Result<Self, EmoteMapperError> { Self::from_reader(File::open(p).map_err(IOError)?) }
}

impl FromStr for EmoteMapper {
    type Err = EmoteMapperError;

    /// Creates an EmoteMapper from a String containing csv data
    fn from_str(s: &str) -> Result<Self, Self::Err> { Self::from_reader(s.as_bytes()) }
}

/// Saving
impl EmoteMapper {
    /// Writes the EmoteMapper as csv to a Writer
    pub fn to_writer(self, wtr: impl Write) -> Result<(), EmoteMapperError> {
        let mut wtr = csv::WriterBuilder::new().has_headers(false).from_writer(wtr);
        for mapping in self.map.into_iter().map(Mapping::from) {
            wtr.serialize(mapping).unwrap();
        }
        wtr.flush().map_err(IOError)
    }

    /// Stores the EmoteMapper as csv in a File
    pub fn to_path(self, p: &Path) -> Result<(), EmoteMapperError> { self.to_writer(File::open(p).map_err(IOError)?) }
}

impl From<EmoteMapper> for String {
    /// The EmoteMapper as CSV
    fn from(val: EmoteMapper) -> Self {
        let mut s = "".to_string();
        {
            val.to_writer(unsafe { s.as_mut_vec() }).expect("Writing to String should not fail");
        }
        s
    }
}

#[test]
fn from_into_str() {
    let str = "testEmote,F4000\n";
    let em = EmoteMapper::from_str(str).unwrap();
    assert_eq!(em.map.get("testEmote").unwrap().0, '\u{F4000}');
    assert_eq!(String::from(em), str)
}

#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
struct Mapping {
    name: String,
    #[serde_as(as = "TryFromInto<String>")]
    code_point: CodePoint,
}

impl From<(String, CodePoint)> for Mapping {
    fn from((name, code_point): (String, CodePoint)) -> Self { Mapping { name, code_point } }
}

#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum EmoteMapperError {
    #[error("codepoint is expected to be a hex number: `{0}`")]
    InvalidCodepoint(String),
    #[error("Parsing from string failed with:\n{0}")]
    StringParsingError(csv::Error),
    #[error("Failed due to IO-Error:\n{0}")]
    IOError(std::io::Error),
}

#[derive(Clone, Copy, Debug)]
struct CodePoint(char);

impl From<CodePoint> for char {
    fn from(cp: CodePoint) -> Self { cp.0 }
}

impl Display for CodePoint {
    fn fmt(&self, f: &mut __private::Formatter<'_>) -> std::fmt::Result { char::from(*self).fmt(f) }
}

impl From<CodePoint> for String {
    fn from(val: CodePoint) -> Self { format!("{:X}", val.0 as u32) }
}

impl TryFrom<String> for CodePoint {
    type Error = EmoteMapperError;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        Ok(CodePoint(
            char::from_u32(u32::from_str_radix(&s, 16).map_err(|_| InvalidCodepoint(s.to_string()))?)
                .ok_or_else(|| InvalidCodepoint(s.to_string()))?,
        ))
    }
}