use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::board::{Board, BoardData, FLAGSHIP_COLS, FLAGSHIP_ROWS};
mod format;
pub use format::{
AbsolutePosition, Align, ComponentStyle, Justify, VbmlComponent, VbmlProps, VbmlRawComponent, VbmlStyle,
VbmlTemplateComponent,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vbml<const ROWS: usize = FLAGSHIP_ROWS, const COLS: usize = FLAGSHIP_COLS> {
pub props: Option<VbmlProps>,
pub style: Option<VbmlStyle>,
pub components: Vec<VbmlComponent<ROWS, COLS>>,
}
impl<const ROWS: usize, const COLS: usize> Vbml<ROWS, COLS> {
pub fn parse(&self) -> Result<BoardData<ROWS, COLS>, VbmlError> {
let mut board: Board<ROWS, COLS> = BoardData::<ROWS, COLS>::default().into();
let mut components = self.components.to_vec();
components.sort_by(
|a, b| match (a.get_style().absolute_position, b.get_style().absolute_position) {
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
_ => std::cmp::Ordering::Equal,
},
);
let props = self.props.as_ref().map(|props| props.replace_template());
let mut cur_row: usize = 0;
let mut max_row: usize = 0;
let mut cur_col: usize = 0;
for component in &components {
let style = component.get_style();
let component_height = style.height.unwrap_or(ROWS as u32) as usize;
let component_width = style.width.unwrap_or(COLS as u32) as usize;
let (content_height, content_widest_width, content) = component.get_word_rows(props.as_ref());
tracing::trace!(
"component_height: {component_height}; component_width: {component_width}; content_height: {content_height}; content_widest_width: {content_widest_width};",
);
tracing::trace!("content: {:?}", content);
if cur_col + component_width > COLS {
cur_col = 0;
cur_row = max_row;
}
if let Some(absolute) = &style.absolute_position {
cur_row = absolute.y as usize;
cur_col = absolute.x as usize;
}
let mut starting_row = 0;
match &style.align {
Some(Align::Center) => starting_row = ((component_height - content_height) as f64 / 2.0).floor() as usize,
Some(Align::Bottom) => starting_row = component_height - content_height,
Some(Align::Justified) => starting_row = ((component_height - content_height) as f64 / 2.0).ceil() as usize,
_ => {}
}
match content {
Some(content_rows) => {
for (row_offset, content_row) in content_rows.iter().filter(|row| !row.is_empty()).enumerate() {
let mut starting_col = 0;
match &style.justify {
Some(Justify::Center) => starting_col = ((component_width - content_row.len()) as f64 / 2.0) as usize,
Some(Justify::Right) => starting_col = component_width - content_row.len(),
Some(Justify::Justified) => {
starting_col = ((component_width - content_widest_width) as f64 / 2.0) as usize
}
_ => {}
}
let row = cur_row + starting_row + row_offset;
for (col_offset, content_col) in content_row.iter().enumerate() {
let col = cur_col + starting_col + col_offset;
tracing::trace!("row: {row}; col: {col}; content_col: {content_col};",);
if row >= ROWS || col >= COLS {
tracing::error!("row or col out of bounds");
continue;
}
board[row][col] = (*content_col).into();
}
}
cur_col += component_width;
max_row = max_row.max(cur_row + component_height);
}
None => {
if let VbmlComponent::Raw(raw) = component {
board = *raw.raw_characters
}
}
};
}
Ok(board.into())
}
}
impl<const ROWS: usize, const COLS: usize> std::str::FromStr for Vbml<ROWS, COLS> {
type Err = VbmlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s).map_err(VbmlError::Deserialize)
}
}
impl<const ROWS: usize, const COLS: usize> TryFrom<Vbml<ROWS, COLS>> for String {
type Error = VbmlError;
fn try_from(value: Vbml<ROWS, COLS>) -> Result<Self, Self::Error> {
serde_json::to_string(&value).map_err(VbmlError::Serialize)
}
}
impl<const ROWS: usize, const COLS: usize> TryFrom<Vbml<ROWS, COLS>> for Board<ROWS, COLS> {
type Error = VbmlError;
fn try_from(value: Vbml<ROWS, COLS>) -> Result<Self, Self::Error> {
Ok(value.parse()?.into())
}
}
#[derive(Error, Debug)]
pub enum VbmlError {
#[error("failed to deserialize into VBML")]
Deserialize(serde_json::Error),
#[error("failed to serialize from VBML")]
Serialize(serde_json::Error),
#[error("failed to replace VBML template")]
Regex(#[from] regex::Error),
}