use crate::MergeTextFormatDto;
use anyhow::{Result, anyhow};
use common::database::CommandUnitOfWork;
use common::database::rope_helpers::{block_char_length, block_char_to_byte_in_block};
use common::direct_access::document::document_repository::DocumentRelationshipField;
use common::direct_access::frame::frame_repository::FrameRelationshipField;
use common::direct_access::root::root_repository::RootRelationshipField;
use common::entities::{Block, Document, Frame, Root};
use common::format_runs::{
CharacterFormat, FormatRun, capture_image_formats_in_range, capture_runs_in_range,
debug_assert_well_formed, splice_range,
};
use common::types::{EntityId, ROOT_ENTITY_ID};
use common::undo_redo::UndoRedoCommand;
use std::any::Any;
pub trait MergeTextFormatUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn MergeTextFormatUnitOfWorkTrait>;
}
#[macros::uow_action(entity = "Root", action = "Get")]
#[macros::uow_action(entity = "Root", action = "GetRelationship")]
#[macros::uow_action(entity = "Document", action = "Get")]
#[macros::uow_action(entity = "Document", action = "GetRelationship")]
#[macros::uow_action(entity = "Frame", action = "Get")]
#[macros::uow_action(entity = "Frame", action = "GetRelationship")]
#[macros::uow_action(entity = "Block", action = "Get")]
#[macros::uow_action(entity = "Block", action = "GetMulti")]
#[macros::uow_action(entity = "Block", action = "GetRelationship")]
pub trait MergeTextFormatUnitOfWorkTrait: CommandUnitOfWork {}
#[derive(Clone, Debug)]
struct BlockFormatInverse {
block_id: EntityId,
byte_range: (u32, u32),
prior_runs: Vec<FormatRun>,
prior_image_formats: Vec<(u32, CharacterFormat)>,
}
fn merge_dto(base: &CharacterFormat, dto: &MergeTextFormatDto) -> CharacterFormat {
let mut out = base.clone();
if let Some(ref family) = dto.font_family
&& !family.is_empty()
{
out.font_family = Some(family.clone());
}
if let Some(v) = dto.font_bold {
out.font_bold = Some(v);
}
if let Some(v) = dto.font_italic {
out.font_italic = Some(v);
}
if let Some(v) = dto.font_underline {
out.font_underline = Some(v);
}
if let Some(v) = dto.font_strikeout {
out.font_strikeout = Some(v);
}
out
}
fn build_replacement_runs(
existing_runs: &[FormatRun],
byte_start: u32,
byte_end: u32,
dto: &MergeTextFormatDto,
) -> Vec<FormatRun> {
let mut out: Vec<FormatRun> = Vec::new();
let mut cursor = byte_start;
for run in existing_runs {
if run.byte_end <= byte_start || run.byte_start >= byte_end {
continue;
}
let overlap_start = std::cmp::max(run.byte_start, byte_start);
let overlap_end = std::cmp::min(run.byte_end, byte_end);
if overlap_start > cursor {
out.push(FormatRun {
byte_start: cursor,
byte_end: overlap_start,
format: merge_dto(&CharacterFormat::default(), dto),
});
}
out.push(FormatRun {
byte_start: overlap_start,
byte_end: overlap_end,
format: merge_dto(&run.format, dto),
});
cursor = overlap_end;
}
if cursor < byte_end {
out.push(FormatRun {
byte_start: cursor,
byte_end,
format: merge_dto(&CharacterFormat::default(), dto),
});
}
out
}
fn execute_merge_text_format(
uow: &mut Box<dyn MergeTextFormatUnitOfWorkTrait>,
dto: &MergeTextFormatDto,
) -> Result<Vec<BlockFormatInverse>> {
let root = uow
.get_root(&ROOT_ENTITY_ID)?
.ok_or_else(|| anyhow!("Root entity not found"))?;
let doc_ids = uow.get_root_relationship(&root.id, &RootRelationshipField::Document)?;
let doc_id = *doc_ids
.first()
.ok_or_else(|| anyhow!("Root has no document"))?;
let _document = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
let frame_ids = uow.get_document_relationship(&doc_id, &DocumentRelationshipField::Frames)?;
let mut all_block_ids = Vec::new();
for fid in &frame_ids {
let block_ids = uow.get_frame_relationship(fid, &FrameRelationshipField::Blocks)?;
all_block_ids.extend(block_ids);
}
let blocks_opt = uow.get_block_multi(&all_block_ids)?;
let mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
let range_start = std::cmp::min(dto.position, dto.anchor);
let range_end = std::cmp::max(dto.position, dto.anchor);
let mut inverse: Vec<BlockFormatInverse> = Vec::new();
if range_start == range_end {
return Ok(inverse);
}
let store = uow.store();
for block in &blocks {
let block_start = block.document_position;
let block_end = block_start + block_char_length(block, &store);
if block_end <= range_start || block_start >= range_end {
continue;
}
let local_char_start = std::cmp::max(0, range_start - block_start) as usize;
let local_char_end =
std::cmp::min(block_char_length(block, &store), range_end - block_start) as usize;
let (byte_start, content_byte_len) =
block_char_to_byte_in_block(&store, block.id, local_char_start);
let (byte_end, _) = block_char_to_byte_in_block(&store, block.id, local_char_end);
if byte_start >= byte_end {
continue;
}
let prior_runs = {
let runs_map = store.format_runs.read();
runs_map
.get(&block.id)
.map(|runs| capture_runs_in_range(runs, byte_start, byte_end))
.unwrap_or_default()
};
let prior_image_formats = {
let images_map = store.block_images.read();
images_map
.get(&block.id)
.map(|images| capture_image_formats_in_range(images, byte_start, byte_end))
.unwrap_or_default()
};
{
let mut runs_map = store.format_runs.write();
let runs = runs_map.entry(block.id).or_default();
let replacement = build_replacement_runs(runs, byte_start, byte_end, dto);
splice_range(runs, byte_start..byte_end, replacement);
debug_assert_well_formed(runs, content_byte_len);
}
{
let mut images_map = store.block_images.write();
if let Some(images) = images_map.get_mut(&block.id) {
for img in images.iter_mut() {
if img.byte_offset >= byte_start && img.byte_offset < byte_end {
img.format = merge_dto(&img.format, dto);
}
}
}
}
inverse.push(BlockFormatInverse {
block_id: block.id,
byte_range: (byte_start, byte_end),
prior_runs,
prior_image_formats,
});
}
Ok(inverse)
}
fn apply_inverse(
uow: &mut Box<dyn MergeTextFormatUnitOfWorkTrait>,
inverse: &[BlockFormatInverse],
) -> Result<()> {
let store = uow.store();
for entry in inverse {
{
let mut runs_map = store.format_runs.write();
let runs = runs_map.entry(entry.block_id).or_default();
splice_range(
runs,
entry.byte_range.0..entry.byte_range.1,
entry.prior_runs.clone(),
);
}
{
let mut images_map = store.block_images.write();
if let Some(images) = images_map.get_mut(&entry.block_id) {
for (byte_offset, format) in &entry.prior_image_formats {
if let Some(img) = images.iter_mut().find(|i| i.byte_offset == *byte_offset) {
img.format = format.clone();
}
}
}
}
}
Ok(())
}
pub struct MergeTextFormatUseCase {
uow_factory: Box<dyn MergeTextFormatUnitOfWorkFactoryTrait>,
inverse: Option<Vec<BlockFormatInverse>>,
last_dto: Option<MergeTextFormatDto>,
}
impl MergeTextFormatUseCase {
pub fn new(uow_factory: Box<dyn MergeTextFormatUnitOfWorkFactoryTrait>) -> Self {
MergeTextFormatUseCase {
uow_factory,
inverse: None,
last_dto: None,
}
}
pub fn execute(&mut self, dto: &MergeTextFormatDto) -> Result<()> {
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let inverse = execute_merge_text_format(&mut uow, dto)?;
self.inverse = Some(inverse);
self.last_dto = Some(dto.clone());
uow.commit()?;
Ok(())
}
}
impl UndoRedoCommand for MergeTextFormatUseCase {
fn undo(&mut self) -> Result<()> {
let inverse = self
.inverse
.as_ref()
.ok_or_else(|| anyhow!("No inverse data available for undo"))?
.clone();
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
apply_inverse(&mut uow, &inverse)?;
uow.commit()?;
Ok(())
}
fn redo(&mut self) -> Result<()> {
let dto = self
.last_dto
.as_ref()
.ok_or_else(|| anyhow!("No DTO available for redo"))?
.clone();
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let inverse = execute_merge_text_format(&mut uow, &dto)?;
self.inverse = Some(inverse);
uow.commit()?;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}