ass_editor/commands/
event_commands.rs

1//! Event management commands for ASS documents
2//!
3//! Provides commands for splitting, merging, timing adjustments, toggling event types,
4//! and effect modifications with proper validation and delta tracking.
5
6use super::{CommandResult, EditorCommand};
7use crate::core::{EditorDocument, EditorError, Position, Range, Result};
8use ass_core::parser::ast::{Event, EventType, Span};
9use ass_core::utils::{format_ass_time, parse_ass_time};
10
11/// Helper function to parse an ASS event line with proper comma handling
12/// Returns parsed Event struct or error if parsing fails
13fn parse_event_line(line: &str) -> core::result::Result<Event, EditorError> {
14    // Extract event type
15    let colon_pos = line
16        .find(':')
17        .ok_or_else(|| EditorError::command_failed("Invalid event format: missing colon"))?;
18    let event_type_str = &line[..colon_pos];
19    let fields_part = line[colon_pos + 1..].trim();
20
21    let event_type = match event_type_str {
22        "Dialogue" => EventType::Dialogue,
23        "Comment" => EventType::Comment,
24        _ => return Err(EditorError::command_failed("Unknown event type")),
25    };
26
27    // Parse fields carefully - Effect field can contain commas, so we need special handling
28    // Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
29    let parts: Vec<&str> = fields_part.splitn(10, ',').collect();
30    if parts.len() < 10 {
31        return Err(EditorError::command_failed(
32            "Invalid event format: insufficient fields",
33        ));
34    }
35
36    // The issue is that parts[8] (Effect) and parts[9] (Text) might be incorrectly split
37    // if Effect contains commas. We need to rejoin them properly.
38
39    // First 8 fields are safe (no commas expected)
40    let layer = parts[0].trim();
41    let start = parts[1].trim();
42    let end = parts[2].trim();
43    let style = parts[3].trim();
44    let name = parts[4].trim();
45    let margin_l = parts[5].trim();
46    let margin_r = parts[6].trim();
47    let margin_v = parts[7].trim();
48
49    // For Effect and Text, we need to find the correct comma that separates them
50    // Effect can contain commas, but Text is the final field
51
52    // Calculate where effect+text starts in the original string
53    let prefix_len = parts[0..8].iter().map(|s| s.len()).sum::<usize>() + 8; // +8 for commas
54    let remaining = &fields_part[prefix_len..];
55
56    // Find the last comma that's not inside parentheses
57    let mut split_point = None;
58    let chars: Vec<char> = remaining.chars().collect();
59    let mut paren_depth = 0;
60
61    for (i, &ch) in chars.iter().enumerate().rev() {
62        match ch {
63            ')' => paren_depth += 1,
64            '(' => paren_depth -= 1,
65            ',' if paren_depth == 0 => {
66                split_point = Some(i);
67                break;
68            }
69            _ => {}
70        }
71    }
72
73    let (effect, text) = if let Some(split) = split_point {
74        let effect_part = remaining[..split].trim();
75        let text_part = remaining[split + 1..].trim();
76        (effect_part, text_part)
77    } else {
78        // No comma found outside parentheses, treat entire remaining as effect
79        (remaining.trim(), "")
80    };
81
82    Ok(Event {
83        event_type,
84        layer,
85        start,
86        end,
87        style,
88        name,
89        margin_l,
90        margin_r,
91        margin_v,
92        margin_t: None,
93        margin_b: None,
94        effect,
95        text,
96        span: Span::new(0, line.len(), 1, 1), // Dummy span
97    })
98}
99
100#[cfg(not(feature = "std"))]
101use alloc::{
102    format,
103    string::{String, ToString},
104    vec,
105    vec::Vec,
106};
107
108/// Command to split an event at a specific time
109#[derive(Debug, Clone)]
110pub struct SplitEventCommand {
111    pub event_index: usize,
112    pub split_time: String, // Time in ASS format (H:MM:SS.CC)
113    pub description: Option<String>,
114}
115
116impl SplitEventCommand {
117    /// Create a new event split command
118    pub fn new(event_index: usize, split_time: String) -> Self {
119        Self {
120            event_index,
121            split_time,
122            description: None,
123        }
124    }
125
126    /// Set a custom description for this command
127    #[must_use]
128    pub fn with_description(mut self, description: String) -> Self {
129        self.description = Some(description);
130        self
131    }
132}
133
134impl EditorCommand for SplitEventCommand {
135    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
136        // Parse split time to validate it
137        let split_time_cs = parse_ass_time(&self.split_time).map_err(|_| {
138            EditorError::command_failed(format!("Invalid time format: {}", self.split_time))
139        })?;
140
141        // Find the event to split
142        let content = document.text();
143        let events_start = content
144            .find("[Events]")
145            .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
146
147        // Skip to first event after format line
148        let events_content = &content[events_start..];
149        let format_line_end = events_content
150            .find("Format:")
151            .and_then(|format_pos| {
152                events_content[format_pos..]
153                    .find('\n')
154                    .map(|newline_pos| events_start + format_pos + newline_pos + 1)
155            })
156            .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
157
158        // Find the event at the specified index
159        let mut current_index = 0;
160        let mut event_start = format_line_end;
161
162        while event_start < content.len() {
163            let line_end = content[event_start..]
164                .find('\n')
165                .map(|pos| event_start + pos)
166                .unwrap_or(content.len());
167
168            if event_start >= line_end {
169                break;
170            }
171
172            let line = &content[event_start..line_end];
173
174            // Check if this is an event line
175            if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
176                if current_index == self.event_index {
177                    // Found the event to split - parse using ass-core's parser
178                    let event = parse_event_line(line)?;
179
180                    // Validate split time is within event bounds
181                    let start_time_cs = event
182                        .start_time_cs()
183                        .map_err(|_| EditorError::command_failed("Invalid start time in event"))?;
184                    let end_time_cs = event
185                        .end_time_cs()
186                        .map_err(|_| EditorError::command_failed("Invalid end time in event"))?;
187
188                    if split_time_cs <= start_time_cs || split_time_cs >= end_time_cs {
189                        return Err(EditorError::command_failed(
190                            "Split time must be between event start and end times",
191                        ));
192                    }
193
194                    // Create two new events
195                    let event_type_str = match event.event_type {
196                        EventType::Dialogue => "Dialogue",
197                        EventType::Comment => "Comment",
198                        _ => "Dialogue", // Default fallback
199                    };
200                    let first_event = format!(
201                        "{}: {},{},{},{},{},{},{},{},{},{}",
202                        event_type_str,
203                        event.layer,
204                        event.start,
205                        self.split_time,
206                        event.style,
207                        event.name,
208                        event.margin_l,
209                        event.margin_r,
210                        event.margin_v,
211                        event.effect,
212                        event.text
213                    );
214
215                    let second_event = format!(
216                        "{}: {},{},{},{},{},{},{},{},{},{}",
217                        event_type_str,
218                        event.layer,
219                        self.split_time,
220                        event.end,
221                        event.style,
222                        event.name,
223                        event.margin_l,
224                        event.margin_r,
225                        event.margin_v,
226                        event.effect,
227                        event.text
228                    );
229
230                    // Replace the original event with the two new events
231                    let replacement = format!("{first_event}\n{second_event}");
232                    let range = Range::new(Position::new(event_start), Position::new(line_end));
233                    document.replace(range, &replacement)?;
234
235                    let end_pos = Position::new(event_start + replacement.len());
236                    return Ok(CommandResult::success_with_change(
237                        Range::new(Position::new(event_start), end_pos),
238                        end_pos,
239                    )
240                    .with_message(format!(
241                        "Split event {} at time {}",
242                        self.event_index, self.split_time
243                    )));
244                }
245                current_index += 1;
246            } else if line.starts_with('[') {
247                // Stop at next section
248                break;
249            }
250
251            event_start = line_end + 1;
252        }
253
254        Err(EditorError::command_failed(format!(
255            "Event index {} not found",
256            self.event_index
257        )))
258    }
259
260    fn description(&self) -> &str {
261        self.description.as_deref().unwrap_or("Split event")
262    }
263
264    fn memory_usage(&self) -> usize {
265        core::mem::size_of::<Self>()
266            + self.split_time.len()
267            + self.description.as_ref().map_or(0, |d| d.len())
268    }
269}
270
271/// Command to merge two consecutive events
272#[derive(Debug, Clone)]
273pub struct MergeEventsCommand {
274    pub first_event_index: usize,
275    pub second_event_index: usize,
276    pub merge_text_separator: String, // Text to put between merged texts
277    pub description: Option<String>,
278}
279
280impl MergeEventsCommand {
281    /// Create a new event merge command
282    pub fn new(first_event_index: usize, second_event_index: usize) -> Self {
283        Self {
284            first_event_index,
285            second_event_index,
286            merge_text_separator: " ".to_string(), // Default space separator
287            description: None,
288        }
289    }
290
291    /// Set the text separator for merged text
292    pub fn with_separator(mut self, separator: String) -> Self {
293        self.merge_text_separator = separator;
294        self
295    }
296
297    /// Set a custom description for this command
298    #[must_use]
299    pub fn with_description(mut self, description: String) -> Self {
300        self.description = Some(description);
301        self
302    }
303}
304
305impl EditorCommand for MergeEventsCommand {
306    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
307        if self.first_event_index >= self.second_event_index {
308            return Err(EditorError::command_failed(
309                "First event index must be less than second event index",
310            ));
311        }
312
313        let content = document.text();
314        let events_start = content
315            .find("[Events]")
316            .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
317
318        // Skip to first event after format line
319        let events_content = &content[events_start..];
320        let format_line_end = events_content
321            .find("Format:")
322            .and_then(|format_pos| {
323                events_content[format_pos..]
324                    .find('\n')
325                    .map(|newline_pos| events_start + format_pos + newline_pos + 1)
326            })
327            .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
328
329        // Find both events
330        let mut events = Vec::new();
331        let mut current_index = 0;
332        let mut event_start = format_line_end;
333
334        while event_start < content.len() {
335            let line_end = content[event_start..]
336                .find('\n')
337                .map(|pos| event_start + pos)
338                .unwrap_or(content.len());
339
340            if event_start >= line_end {
341                break;
342            }
343
344            let line = &content[event_start..line_end];
345
346            if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
347                if current_index == self.first_event_index
348                    || current_index == self.second_event_index
349                {
350                    events.push((current_index, event_start, line_end, line.to_string()));
351                }
352                current_index += 1;
353            } else if line.starts_with('[') {
354                break;
355            }
356
357            event_start = line_end + 1;
358        }
359
360        if events.len() != 2 {
361            return Err(EditorError::command_failed(
362                "Could not find both events to merge",
363            ));
364        }
365
366        // Parse both events using ass-core's parser
367        let first_event_line = &events[0].3;
368        let second_event_line = &events[1].3;
369
370        let first_event = parse_event_line(first_event_line)?;
371        let second_event = parse_event_line(second_event_line)?;
372
373        // Use first event's properties, second event's end time, merged text
374        let merged_text = format!(
375            "{}{}{}",
376            first_event.text, self.merge_text_separator, second_event.text
377        );
378
379        // Create merged event
380        let event_type_str = match first_event.event_type {
381            EventType::Dialogue => "Dialogue",
382            EventType::Comment => "Comment",
383            _ => "Dialogue", // Default fallback
384        };
385        let merged_event = format!(
386            "{}: {},{},{},{},{},{},{},{},{},{}",
387            event_type_str,
388            first_event.layer,
389            first_event.start,
390            second_event.end,
391            first_event.style,
392            first_event.name,
393            first_event.margin_l,
394            first_event.margin_r,
395            first_event.margin_v,
396            first_event.effect,
397            merged_text
398        );
399
400        // Replace both events with the merged one
401        let first_start = events[0].1;
402        let second_end = events[1].2 + 1; // Include newline
403        let range = Range::new(Position::new(first_start), Position::new(second_end));
404        let replacement = format!("{merged_event}\n");
405
406        document.replace(range, &replacement)?;
407
408        let end_pos = Position::new(first_start + replacement.len());
409        Ok(CommandResult::success_with_change(
410            Range::new(Position::new(first_start), end_pos),
411            end_pos,
412        )
413        .with_message(format!(
414            "Merged events {} and {}",
415            self.first_event_index, self.second_event_index
416        )))
417    }
418
419    fn description(&self) -> &str {
420        self.description.as_deref().unwrap_or("Merge events")
421    }
422
423    fn memory_usage(&self) -> usize {
424        core::mem::size_of::<Self>()
425            + self.merge_text_separator.len()
426            + self.description.as_ref().map_or(0, |d| d.len())
427    }
428}
429
430/// Command to adjust event timing (shift start/end times)
431#[derive(Debug, Clone)]
432pub struct TimingAdjustCommand {
433    pub event_indices: Vec<usize>, // Events to adjust (empty = all events)
434    pub start_offset_cs: i32,      // Offset in centiseconds for start time
435    pub end_offset_cs: i32,        // Offset in centiseconds for end time
436    pub description: Option<String>,
437}
438
439impl TimingAdjustCommand {
440    /// Create a new timing adjustment command for specific events
441    pub fn new(event_indices: Vec<usize>, start_offset_cs: i32, end_offset_cs: i32) -> Self {
442        Self {
443            event_indices,
444            start_offset_cs,
445            end_offset_cs,
446            description: None,
447        }
448    }
449
450    /// Create a timing adjustment command for all events
451    pub fn all_events(start_offset_cs: i32, end_offset_cs: i32) -> Self {
452        Self {
453            event_indices: Vec::new(), // Empty means all events
454            start_offset_cs,
455            end_offset_cs,
456            description: None,
457        }
458    }
459
460    /// Adjust only start times (keep duration constant)
461    pub fn shift_start(event_indices: Vec<usize>, offset_cs: i32) -> Self {
462        Self::new(event_indices, offset_cs, offset_cs)
463    }
464
465    /// Adjust only end times (change duration)
466    pub fn shift_end(event_indices: Vec<usize>, offset_cs: i32) -> Self {
467        Self::new(event_indices, 0, offset_cs)
468    }
469
470    /// Scale duration (multiply by factor)
471    pub fn scale_duration(event_indices: Vec<usize>, factor: f64) -> Self {
472        // This is a simplified version - actual implementation would need to calculate per-event
473        let offset = (factor * 100.0) as i32 - 100; // Convert factor to centisecond offset
474        Self::new(event_indices, 0, offset)
475    }
476
477    /// Set a custom description for this command
478    #[must_use]
479    pub fn with_description(mut self, description: String) -> Self {
480        self.description = Some(description);
481        self
482    }
483}
484
485impl EditorCommand for TimingAdjustCommand {
486    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
487        let mut content = document.text();
488        let events_start = content
489            .find("[Events]")
490            .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
491
492        let events_content = &content[events_start..];
493        let format_line_end = events_content
494            .find("Format:")
495            .and_then(|format_pos| {
496                events_content[format_pos..]
497                    .find('\n')
498                    .map(|newline_pos| events_start + format_pos + newline_pos + 1)
499            })
500            .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
501
502        let mut changes_made = 0;
503        let mut current_index = 0;
504        let mut event_start = format_line_end;
505        let mut total_range: Option<Range> = None;
506
507        while event_start < content.len() {
508            let line_end = content[event_start..]
509                .find('\n')
510                .map(|pos| event_start + pos)
511                .unwrap_or(content.len());
512
513            if event_start >= line_end {
514                break;
515            }
516
517            let line = &content[event_start..line_end];
518
519            if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
520                let should_adjust =
521                    self.event_indices.is_empty() || self.event_indices.contains(&current_index);
522
523                if should_adjust {
524                    // Parse event line using ass-core's parser
525                    if let Ok(event) = parse_event_line(line) {
526                        // Parse current times using Event methods
527                        if let (Ok(start_cs), Ok(end_cs)) =
528                            (event.start_time_cs(), event.end_time_cs())
529                        {
530                            // Apply offsets
531                            let new_start_cs =
532                                (start_cs as i32 + self.start_offset_cs).max(0) as u32;
533                            let new_end_cs = (end_cs as i32 + self.end_offset_cs).max(0) as u32;
534
535                            // Ensure end time is after start time
536                            let final_end_cs = new_end_cs.max(new_start_cs + 1);
537
538                            let new_start_time = format_ass_time(new_start_cs);
539                            let new_end_time = format_ass_time(final_end_cs);
540
541                            // Build new event line
542                            let event_type_str = match event.event_type {
543                                EventType::Dialogue => "Dialogue",
544                                EventType::Comment => "Comment",
545                                _ => "Dialogue", // Default fallback
546                            };
547                            let new_line = format!(
548                                "{}: {},{},{},{},{},{},{},{},{},{}",
549                                event_type_str,
550                                event.layer,
551                                new_start_time,
552                                new_end_time,
553                                event.style,
554                                event.name,
555                                event.margin_l,
556                                event.margin_r,
557                                event.margin_v,
558                                event.effect,
559                                event.text
560                            );
561
562                            // Replace the line
563                            let range =
564                                Range::new(Position::new(event_start), Position::new(line_end));
565                            document.replace(range, &new_line)?;
566
567                            // Update content for next iteration
568                            content = document.text();
569
570                            // Track overall range
571                            let change_range = Range::new(
572                                Position::new(event_start),
573                                Position::new(event_start + new_line.len()),
574                            );
575                            total_range = Some(match total_range {
576                                Some(existing) => existing.union(&change_range),
577                                None => change_range,
578                            });
579
580                            changes_made += 1;
581                        }
582                    }
583                }
584                current_index += 1;
585            } else if line.starts_with('[') {
586                break;
587            }
588
589            event_start = line_end + 1;
590        }
591
592        if changes_made > 0 {
593            Ok(CommandResult::success_with_change(
594                total_range.unwrap_or(Range::new(Position::new(0), Position::new(0))),
595                Position::new(content.len()),
596            )
597            .with_message(format!("Adjusted timing for {changes_made} events")))
598        } else {
599            Ok(CommandResult::success().with_message("No events were adjusted".to_string()))
600        }
601    }
602
603    fn description(&self) -> &str {
604        self.description.as_deref().unwrap_or("Adjust event timing")
605    }
606
607    fn memory_usage(&self) -> usize {
608        core::mem::size_of::<Self>()
609            + self.event_indices.len() * core::mem::size_of::<usize>()
610            + self.description.as_ref().map_or(0, |d| d.len())
611    }
612}
613
614/// Command to toggle event type between Dialogue and Comment
615#[derive(Debug, Clone)]
616pub struct ToggleEventTypeCommand {
617    pub event_indices: Vec<usize>,
618    pub description: Option<String>,
619}
620
621impl ToggleEventTypeCommand {
622    /// Create a new event type toggle command
623    pub fn new(event_indices: Vec<usize>) -> Self {
624        Self {
625            event_indices,
626            description: None,
627        }
628    }
629
630    /// Toggle a single event
631    pub fn single(event_index: usize) -> Self {
632        Self::new(vec![event_index])
633    }
634
635    /// Toggle all events
636    pub fn all() -> Self {
637        Self::new(Vec::new()) // Empty means all events
638    }
639
640    /// Set a custom description for this command
641    #[must_use]
642    pub fn with_description(mut self, description: String) -> Self {
643        self.description = Some(description);
644        self
645    }
646}
647
648impl EditorCommand for ToggleEventTypeCommand {
649    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
650        let mut content = document.text();
651        let events_start = content
652            .find("[Events]")
653            .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
654
655        let events_content = &content[events_start..];
656        let format_line_end = events_content
657            .find("Format:")
658            .and_then(|format_pos| {
659                events_content[format_pos..]
660                    .find('\n')
661                    .map(|newline_pos| events_start + format_pos + newline_pos + 1)
662            })
663            .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
664
665        let mut changes_made = 0;
666        let mut current_index = 0;
667        let mut event_start = format_line_end;
668        let mut total_range: Option<Range> = None;
669
670        while event_start < content.len() {
671            let line_end = content[event_start..]
672                .find('\n')
673                .map(|pos| event_start + pos)
674                .unwrap_or(content.len());
675
676            if event_start >= line_end {
677                break;
678            }
679
680            let line = &content[event_start..line_end];
681
682            if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
683                let should_toggle =
684                    self.event_indices.is_empty() || self.event_indices.contains(&current_index);
685
686                if should_toggle {
687                    let new_line = if line.starts_with("Dialogue:") {
688                        line.replacen("Dialogue:", "Comment:", 1)
689                    } else {
690                        line.replacen("Comment:", "Dialogue:", 1)
691                    };
692
693                    let range = Range::new(Position::new(event_start), Position::new(line_end));
694                    document.replace(range, &new_line)?;
695
696                    // Update content for next iteration
697                    content = document.text();
698
699                    // Track overall range
700                    let change_range = Range::new(
701                        Position::new(event_start),
702                        Position::new(event_start + new_line.len()),
703                    );
704                    total_range = Some(match total_range {
705                        Some(existing) => existing.union(&change_range),
706                        None => change_range,
707                    });
708
709                    changes_made += 1;
710                }
711                current_index += 1;
712            } else if line.starts_with('[') {
713                break;
714            }
715
716            event_start = line_end + 1;
717        }
718
719        if changes_made > 0 {
720            Ok(CommandResult::success_with_change(
721                total_range.unwrap_or(Range::new(Position::new(0), Position::new(0))),
722                Position::new(content.len()),
723            )
724            .with_message(format!("Toggled type for {changes_made} events")))
725        } else {
726            Ok(CommandResult::success().with_message("No events were toggled".to_string()))
727        }
728    }
729
730    fn description(&self) -> &str {
731        self.description.as_deref().unwrap_or("Toggle event type")
732    }
733
734    fn memory_usage(&self) -> usize {
735        core::mem::size_of::<Self>()
736            + self.event_indices.len() * core::mem::size_of::<usize>()
737            + self.description.as_ref().map_or(0, |d| d.len())
738    }
739}
740
741/// Command to modify event effects
742#[derive(Debug, Clone)]
743pub struct EventEffectCommand {
744    pub event_indices: Vec<usize>,
745    pub effect: String,
746    pub operation: EffectOperation,
747    pub description: Option<String>,
748}
749
750#[derive(Debug, Clone, PartialEq, Eq)]
751pub enum EffectOperation {
752    Set,     // Replace current effect
753    Append,  // Add to existing effect
754    Prepend, // Add before existing effect
755    Clear,   // Remove all effects
756}
757
758impl EventEffectCommand {
759    /// Create a new effect command
760    pub fn new(event_indices: Vec<usize>, effect: String, operation: EffectOperation) -> Self {
761        Self {
762            event_indices,
763            effect,
764            operation,
765            description: None,
766        }
767    }
768
769    /// Set effect for specific events
770    pub fn set_effect(event_indices: Vec<usize>, effect: String) -> Self {
771        Self::new(event_indices, effect, EffectOperation::Set)
772    }
773
774    /// Clear effects for specific events  
775    pub fn clear_effect(event_indices: Vec<usize>) -> Self {
776        Self::new(event_indices, String::new(), EffectOperation::Clear)
777    }
778
779    /// Append effect to specific events
780    pub fn append_effect(event_indices: Vec<usize>, effect: String) -> Self {
781        Self::new(event_indices, effect, EffectOperation::Append)
782    }
783
784    /// Set a custom description for this command
785    #[must_use]
786    pub fn with_description(mut self, description: String) -> Self {
787        self.description = Some(description);
788        self
789    }
790}
791
792impl EditorCommand for EventEffectCommand {
793    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
794        let mut content = document.text();
795        let events_start = content
796            .find("[Events]")
797            .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
798
799        let events_content = &content[events_start..];
800        let format_line_end = events_content
801            .find("Format:")
802            .and_then(|format_pos| {
803                events_content[format_pos..]
804                    .find('\n')
805                    .map(|newline_pos| events_start + format_pos + newline_pos + 1)
806            })
807            .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
808
809        let mut changes_made = 0;
810        let mut current_index = 0;
811        let mut event_start = format_line_end;
812        let mut total_range: Option<Range> = None;
813
814        while event_start < content.len() {
815            let line_end = content[event_start..]
816                .find('\n')
817                .map(|pos| event_start + pos)
818                .unwrap_or(content.len());
819
820            if event_start >= line_end {
821                break;
822            }
823
824            let line = &content[event_start..line_end];
825
826            if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
827                let should_modify =
828                    self.event_indices.is_empty() || self.event_indices.contains(&current_index);
829
830                if should_modify {
831                    // Parse event line using helper function
832                    if let Ok(event) = parse_event_line(line) {
833                        let new_effect = match self.operation {
834                            EffectOperation::Set => self.effect.clone(),
835                            EffectOperation::Clear => String::new(),
836                            EffectOperation::Append => {
837                                if event.effect.is_empty() {
838                                    self.effect.clone()
839                                } else {
840                                    format!("{} {}", event.effect, self.effect)
841                                }
842                            }
843                            EffectOperation::Prepend => {
844                                if event.effect.is_empty() {
845                                    self.effect.clone()
846                                } else {
847                                    format!("{} {}", self.effect, event.effect)
848                                }
849                            }
850                        };
851
852                        // Build new event line
853                        let event_type_str = match event.event_type {
854                            EventType::Dialogue => "Dialogue",
855                            EventType::Comment => "Comment",
856                            _ => "Dialogue", // Default fallback
857                        };
858                        let new_line = format!(
859                            "{}: {},{},{},{},{},{},{},{},{},{}",
860                            event_type_str,
861                            event.layer,
862                            event.start,
863                            event.end,
864                            event.style,
865                            event.name,
866                            event.margin_l,
867                            event.margin_r,
868                            event.margin_v,
869                            new_effect,
870                            event.text
871                        );
872
873                        let range = Range::new(Position::new(event_start), Position::new(line_end));
874                        document.replace(range, &new_line)?;
875
876                        // Update content for next iteration
877                        content = document.text();
878
879                        // Track overall range
880                        let change_range = Range::new(
881                            Position::new(event_start),
882                            Position::new(event_start + new_line.len()),
883                        );
884                        total_range = Some(match total_range {
885                            Some(existing) => existing.union(&change_range),
886                            None => change_range,
887                        });
888
889                        changes_made += 1;
890                    }
891                }
892                current_index += 1;
893            } else if line.starts_with('[') {
894                break;
895            }
896
897            event_start = line_end + 1;
898        }
899
900        if changes_made > 0 {
901            let operation_name = match self.operation {
902                EffectOperation::Set => "set",
903                EffectOperation::Clear => "cleared",
904                EffectOperation::Append => "appended",
905                EffectOperation::Prepend => "prepended",
906            };
907
908            Ok(CommandResult::success_with_change(
909                total_range.unwrap_or(Range::new(Position::new(0), Position::new(0))),
910                Position::new(content.len()),
911            )
912            .with_message(format!("Effect {operation_name} for {changes_made} events")))
913        } else {
914            Ok(CommandResult::success().with_message("No events were modified".to_string()))
915        }
916    }
917
918    fn description(&self) -> &str {
919        self.description.as_deref().unwrap_or("Modify event effect")
920    }
921
922    fn memory_usage(&self) -> usize {
923        core::mem::size_of::<Self>()
924            + self.event_indices.len() * core::mem::size_of::<usize>()
925            + self.effect.len()
926            + self.description.as_ref().map_or(0, |d| d.len())
927    }
928}
929
930#[cfg(test)]
931mod tests {
932    use super::*;
933    use crate::core::EditorDocument;
934    #[cfg(not(feature = "std"))]
935    use alloc::string::ToString;
936    #[cfg(not(feature = "std"))]
937    use alloc::vec;
938    const TEST_CONTENT: &str = r#"[Script Info]
939Title: Event Commands Test
940
941[V4+ Styles]
942Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
943Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
944
945[Events]
946Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
947Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
948Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
949Comment: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Third event
950"#;
951
952    #[test]
953    fn test_split_event_command() {
954        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
955
956        let command = SplitEventCommand::new(0, "0:00:03.00".to_string());
957        let result = command.execute(&mut doc).unwrap();
958
959        assert!(result.success);
960        assert!(result.content_changed);
961
962        // Should now have 4 events total (1 split into 2)
963        let events_count = doc
964            .text()
965            .lines()
966            .filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
967            .count();
968        assert_eq!(events_count, 4);
969
970        // Check split times
971        assert!(doc.text().contains("0:00:01.00,0:00:03.00"));
972        assert!(doc.text().contains("0:00:03.00,0:00:05.00"));
973    }
974
975    #[test]
976    fn test_merge_events_command() {
977        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
978
979        let command = MergeEventsCommand::new(0, 1).with_separator(" | ".to_string());
980        let result = command.execute(&mut doc).unwrap();
981
982        assert!(result.success);
983        assert!(result.content_changed);
984
985        // Should now have 2 events total (2 merged into 1)
986        let events_count = doc
987            .text()
988            .lines()
989            .filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
990            .count();
991        assert_eq!(events_count, 2);
992
993        // Check merged text and timing
994        assert!(doc.text().contains("First event | Second event"));
995        assert!(doc.text().contains("0:00:01.00,0:00:10.00")); // Start of first, end of second
996    }
997
998    #[test]
999    fn test_timing_adjust_command() {
1000        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1001
1002        // Shift all events forward by 2 seconds (200 centiseconds)
1003        let command = TimingAdjustCommand::all_events(200, 200);
1004        let result = command.execute(&mut doc).unwrap();
1005
1006        assert!(result.success);
1007        assert!(result.content_changed);
1008
1009        // Check that times were adjusted
1010        assert!(doc.text().contains("0:00:03.00,0:00:07.00")); // First event shifted
1011        assert!(doc.text().contains("0:00:07.00,0:00:12.00")); // Second event shifted
1012        assert!(doc.text().contains("0:00:12.00,0:00:17.00")); // Third event shifted
1013    }
1014
1015    #[test]
1016    fn test_toggle_event_type_command() {
1017        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1018
1019        let command = ToggleEventTypeCommand::single(0);
1020        let result = command.execute(&mut doc).unwrap();
1021
1022        assert!(result.success);
1023        assert!(result.content_changed);
1024
1025        // First event should now be Comment, others unchanged
1026        let text = doc.text();
1027        let lines: Vec<&str> = text.lines().collect();
1028        let event_lines: Vec<&str> = lines
1029            .iter()
1030            .filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
1031            .copied()
1032            .collect();
1033
1034        assert_eq!(event_lines.len(), 3);
1035        assert!(event_lines[0].starts_with("Comment:")); // Was Dialogue, now Comment
1036        assert!(event_lines[1].starts_with("Dialogue:")); // Unchanged
1037        assert!(event_lines[2].starts_with("Comment:")); // Unchanged
1038    }
1039
1040    #[test]
1041    fn test_event_effect_command() {
1042        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1043
1044        let command = EventEffectCommand::set_effect(vec![0, 1], "Fade(255,0)".to_string());
1045        let result = command.execute(&mut doc).unwrap();
1046
1047        assert!(result.success);
1048        assert!(result.content_changed);
1049
1050        // Check that effects were set for first two events
1051        let content = doc.text();
1052        let lines: Vec<&str> = content.lines().collect();
1053        let event_lines: Vec<&str> = lines
1054            .iter()
1055            .filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
1056            .copied()
1057            .collect();
1058
1059        assert!(event_lines[0].contains("Fade(255,0)"));
1060        assert!(event_lines[1].contains("Fade(255,0)"));
1061        assert!(!event_lines[2].contains("Fade(255,0)")); // Third event unchanged
1062    }
1063
1064    #[test]
1065    fn test_split_event_invalid_time() {
1066        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1067
1068        // Try to split outside event bounds
1069        let command = SplitEventCommand::new(0, "0:00:00.50".to_string()); // Before event start
1070        let result = command.execute(&mut doc);
1071
1072        assert!(result.is_err());
1073    }
1074
1075    #[test]
1076    fn test_merge_events_invalid_indices() {
1077        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1078
1079        // Try to merge with invalid order
1080        let command = MergeEventsCommand::new(1, 0); // Second before first
1081        let result = command.execute(&mut doc);
1082
1083        assert!(result.is_err());
1084    }
1085
1086    #[test]
1087    fn test_timing_adjust_with_specific_events() {
1088        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1089
1090        // Adjust only first event
1091        let command = TimingAdjustCommand::new(vec![0], 100, 100); // +1 second
1092        let result = command.execute(&mut doc).unwrap();
1093
1094        assert!(result.success);
1095
1096        // Only first event should be changed
1097        assert!(doc.text().contains("0:00:02.00,0:00:06.00")); // First event adjusted
1098        assert!(doc.text().contains("0:00:05.00,0:00:10.00")); // Second event unchanged
1099    }
1100
1101    #[test]
1102    fn test_effect_operations() {
1103        let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
1104
1105        // First set an effect
1106        let set_cmd = EventEffectCommand::set_effect(vec![0], "Fade(255,0)".to_string());
1107        set_cmd.execute(&mut doc).unwrap();
1108
1109        // Then append to it
1110        let append_cmd = EventEffectCommand::append_effect(vec![0], "Move(100,200)".to_string());
1111        append_cmd.execute(&mut doc).unwrap();
1112
1113        // Check that both effects are present
1114        // println!("Document after append: {}", doc.text());
1115        assert!(doc.text().contains("Fade(255,0) Move(100,200)"));
1116
1117        // Clear the effect
1118        let clear_cmd = EventEffectCommand::clear_effect(vec![0]);
1119        clear_cmd.execute(&mut doc).unwrap();
1120
1121        // Check that effect field is empty (has the right number of commas)
1122        let text = doc.text();
1123        let lines: Vec<&str> = text.lines().collect();
1124        let first_event = lines
1125            .iter()
1126            .find(|line| line.starts_with("Dialogue:"))
1127            .unwrap();
1128        let parts: Vec<&str> = first_event.split(',').collect();
1129        assert_eq!(parts[8].trim(), ""); // Effect field should be empty
1130    }
1131}