//! Undoing and replaying of commands.

use crate::common::apply::AppliedFuzz;
use crate::common::lines::{LineIndex, Lines};
use std::{any::Any, ops::Range};
use super::{BlockChange, Blocks, ListChange};

pub(crate) trait Undo: Any {
    /// Attempts to undo the command. If successful, returns the range of changed lines.
    /// Returns None If the operation cannot be undone.
    fn undo(self: Box<Self>, blocks: &mut Blocks, block_index: usize) -> Option<ListChange>;
}

pub(crate) trait FromBlockChange {
    fn from_block_change(block: &BlockChange) -> Self;
}

macro_rules! undo_struct {
    ($vis:vis struct $name:ident { $($field:ident: $ty:ty),* $(,)? }) => {
        $vis struct $name {
            $($field: $ty,)*
        }

        impl FromBlockChange for $name {
            fn from_block_change(block: &BlockChange) -> Self {
                Self {
                    $($field: block.current.$field.clone(),)*
                }
            }
        }
    };
}

pub(crate) struct UndoAck {}

undo_struct! {
    pub(crate) struct UndoPrevNext {
        index: usize,
    }
}

undo_struct! {
    pub(crate) struct UndoEdited {
        new_lines: Vec<Lines>,
        confidence: Vec<u8>,
        index: usize,
        edited: bool,
    }
}

undo_struct! {
    pub(crate) struct UndoAi {
        new_lines: Vec<Lines>,
        confidence: Vec<u8>,
        index: usize,
        edited: bool,
    }
}

pub(crate) struct UndoUnapply {
    range: Range<LineIndex>,
    new_lines: Vec<Lines>,
    confidence: Vec<u8>,
    index: usize,
    fuzz: AppliedFuzz,
    edited: bool,
}

impl FromBlockChange for UndoUnapply {
    fn from_block_change(block: &BlockChange) -> Self {
        Self {
            range: block.range.clone(),
            new_lines: block.current.new_lines.clone(),
            confidence: block.current.confidence.clone(),
            index: block.current.index,
            fuzz: block.fuzz.clone(),
            edited: block.current.edited,
        }
    }
}

impl Undo for UndoAck {
    fn undo(self: Box<Self>, blocks: &mut Blocks, block_index: usize) -> Option<ListChange>
    {
        Some(blocks.set_acked(block_index, false))
    }
}

impl Undo for UndoPrevNext {
    fn undo(self: Box<Self>, blocks: &mut Blocks, block_index: usize) -> Option<ListChange>
    {
        Some(blocks.set_current_index(block_index, self.index))
    }
}

impl Undo for UndoEdited {
    fn undo(self: Box<Self>, blocks: &mut Blocks, block_index: usize) -> Option<ListChange>
    {
        Some(blocks.set_new_lines(
            block_index, self.new_lines, self.index, self.confidence, self.edited,
        ))
    }
}

impl Undo for UndoAi {
    fn undo(self: Box<Self>, blocks: &mut Blocks, block_index: usize) -> Option<ListChange>
    {
        Some(blocks.set_new_lines(
            block_index, self.new_lines, self.index, self.confidence, self.edited,
        ))
    }
}

impl Undo for UndoUnapply {
    fn undo(self: Box<Self>, blocks: &mut Blocks, block_index: usize) -> Option<ListChange>
    {
        let target_index = blocks.find_ground_with_range(self.range.clone())?;
        let old_len = blocks.rendered_len();
        let old_block = blocks.remove(block_index);
        let change = BlockChange {
            range: self.range,
            current: super::BlockCurrent {
                new_lines: self.new_lines,
                confidence: self.confidence,
                index: self.index,
                edited: self.edited,
                acked: false,
            },
            hunk: old_block.details.to_unapplied().hunk,
            fuzz: self.fuzz,
            in_progress: super::BlockChangeInProgress::None,
            suggestion_search_state: super::SuggestionSearchState::default(),
            missing: Vec::new(),
        };
        let block = blocks.create_block(change)
            .with_undo_stack(old_block.undo_stack);
        // TODO: restart the search for missing commits
        Some(blocks.undo_unapply(target_index, block, old_len))
    }
}
