use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::board::{char::CharacterCode, BoardData};
use super::VbmlError;
lazy_static::lazy_static! {
pub static ref PROPS_REGEX: regex::Regex = regex::Regex::new(r#"\{(\d+)\}"#).expect("failed to create regex");
pub static ref TEMPLATE_REGEX: regex::Regex = regex::Regex::new(r#"\{(\d+)\}|\{\{([A-Za-z0-9]+)\}\}"#).expect("failed to create regex");
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Justify {
Center,
Left,
Right,
Justified,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Align {
Center,
Top,
Bottom,
Justified,
Absolute,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AbsolutePosition {
pub x: u32,
pub y: u32,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComponentStyle {
pub justify: Option<Justify>,
pub align: Option<Align>,
pub height: Option<u32>,
pub width: Option<u32>,
pub absolute_position: Option<AbsolutePosition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VbmlStyle {
pub height: Option<u32>,
pub width: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VbmlProps(HashMap<String, String>);
impl VbmlProps {
pub fn replace_template(&self) -> HashMap<String, String> {
self
.0
.iter()
.map(|(k, v)| {
(
k.to_string(),
PROPS_REGEX
.replace_all(v, |caps: ®ex::Captures| {
let char_code = caps.get(1).unwrap().as_str().parse::<u8>().unwrap();
let char: char = CharacterCode::from(char_code).into();
format!("{}", char)
})
.to_string(),
)
})
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VbmlRawComponent<const ROWS: usize, const COLS: usize> {
#[serde(default)]
pub style: ComponentStyle,
pub raw_characters: BoardData<ROWS, COLS>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VbmlTemplateComponent {
#[serde(default)]
pub style: ComponentStyle,
pub template: String,
}
impl VbmlTemplateComponent {
pub fn render(&self, props: Option<&HashMap<String, String>>) -> Result<String, VbmlError> {
let string = TEMPLATE_REGEX
.replace_all(&self.template, |caps: ®ex::Captures| {
if let Some(char_code) = caps.get(1) {
let char_code = char_code.as_str().parse::<u8>().unwrap();
let char: char = CharacterCode::from(char_code).into();
return format!("{}", char);
}
if let Some(template) = caps.get(2) {
if let Some(props) = props {
if let Some(value) = props.get(template.as_str()) {
return value.to_string();
}
}
}
"".to_string()
})
.to_string();
Ok(string)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum VbmlComponent<const ROWS: usize, const COLS: usize> {
Raw(VbmlRawComponent<ROWS, COLS>),
Template(VbmlTemplateComponent),
}
impl<const ROWS: usize, const COLS: usize> VbmlComponent<ROWS, COLS> {
pub fn get_word_rows(
&self,
props: Option<&HashMap<String, String>>,
) -> (usize, usize, Option<[Vec<CharacterCode>; ROWS]>) {
match self {
VbmlComponent::Template(template) => {
let style = self.get_style();
let comp_height = style.height.unwrap_or(ROWS as u32) as usize;
let comp_width = style.width.unwrap_or(COLS as u32) as usize;
let mut text = template.render(props).unwrap_or(String::new());
const ARRAY_REPEAT_VALUE: Vec<CharacterCode> = Vec::new();
let mut text_mapping: [Vec<CharacterCode>; ROWS] = [ARRAY_REPEAT_VALUE; ROWS];
if text.is_empty() {
(0..comp_height).for_each(|i| (0..comp_width).for_each(|_| text_mapping[i].push(CharacterCode::Blank)));
return (comp_height, comp_width, Some(text_mapping));
}
let mut remove_space = true;
text = text
.chars()
.rev()
.filter(|c| {
if remove_space && *c == ' ' {
return false;
}
remove_space = *c == '\n';
true
})
.collect();
text = text.chars().rev().collect();
tracing::trace!("text: {:?}", text);
let mut words = text.split_inclusive('\n').flat_map(|s| s.split(' ')).peekable();
tracing::trace!("words: {:?}", words.clone().collect::<Vec<_>>());
let mut row: usize = 0;
let mut col: usize = 0;
while let Some(word) = words.next() {
let next_word = words.peek();
tracing::trace!("word: {word}; next_word: {:?}; col: {col}; row: {row}", next_word);
if word.len() > (comp_width - col) && word.len() < comp_width && word.chars().nth(0).unwrap_or(' ') != '\n' {
col = 0;
row += 1;
}
let mut ended_on_newline = false;
for char in word.chars().map(CharacterCode::from) {
tracing::trace!("char: {char}; col: {col}; row: {row}");
if col >= comp_width {
col = 0;
row += 1;
if char == CharacterCode::Newline {
ended_on_newline = true;
continue;
}
}
if char == CharacterCode::Newline {
col = 0;
row += 1;
ended_on_newline = true;
continue;
}
if row >= comp_height {
break;
}
text_mapping[row].push(char);
col += 1;
}
if let Some(next_word) = next_word {
if col < comp_width && next_word.len() < comp_width - col && !ended_on_newline {
text_mapping[row].push(CharacterCode::Blank);
col += 1;
}
}
}
let text_widest_width = text_mapping.iter().map(|row| row.len()).max().unwrap_or(0);
(row + 1, text_widest_width, Some(text_mapping))
}
VbmlComponent::Raw(_) => (ROWS, COLS, None),
}
}
pub fn get_style(&self) -> &ComponentStyle {
match self {
VbmlComponent::Raw(raw) => &raw.style,
VbmlComponent::Template(template) => &template.style,
}
}
}