mod cache;
mod undo;

use bstr::ByteSlice;
use crate::common::apply::{AppliedChange, AppliedFuzz};
use crate::common::diff::{Hunk, Op};
use crate::common::git;
use crate::common::lines::{LineIndex, LineIndexRange, Lines, LinesSlice, TypedLines};
use std::{any::Any, cmp::min, fmt, iter, num::NonZeroU32, ops::Range, rc::Rc};
use super::format::FormattedSlice;
use super::interdiff::{
    DiffLine, DiffLineHalf, Interdiff, InterdiffedLine, InterdiffedLinesBuilder,
};
use super::unique_id::{UniqueId, UniqueIdGenerator};
use undo::FromBlockChange;
pub(crate) use cache::{ResolutionCache, ResolutionCacheEntry};
pub use super::frontend::ListChange;

bitflags::bitflags! {
    #[derive(Clone, Copy, Default, PartialEq)]
    pub struct Commands: u8 {
        /// Accept the suggestion.
        const ACK = 1 << 0;
        /// Run this hunk through AI.
        const AI = 1 << 1;
        /// Retry a failed attempt.
        const RETRY = 1 << 2;
        /// Present the previous AI suggestion.
        const PREV = 1 << 3;
        /// Present the next AI suggestion.
        const NEXT = 1 << 4;
        /// Edit the hunk in an external editor.
        const EDIT = 1 << 5;
        /// Move the hunk to non-applied.
        const REMOVE = 1 << 6;
        /// Undo the last command.
        const UNDO = 1 << 7;
    }
}

#[derive(PartialEq)]
enum BlockChangeState<'a> {
    Clean,
    ContextFuzz,
    Fuzzy,
    Interim,
    Editing,
    Edited,
    Failed(&'a str),
    Ai(u8),
}

#[derive(PartialEq)]
enum BlockChangeInProgress {
    None,
    Interim,
    Editing,
    Failed(String),
}

#[derive(Clone, Copy, PartialEq)]
pub enum Tristate {
    Start,
    Middle,
    End,
}

#[derive(Clone, Copy, PartialEq)]
pub enum Status {
    Commands,
    Suggestions,
}

#[derive(Clone, Copy, PartialEq)]
pub enum OpTag {
    Normal,
    Delete,
    Insert,
    Header,
    Foldable(Tristate),
    Folded,
}

pub trait FormatHalfLine {
    /// Return (indicator, indicator CSS class).
    fn format_indicator(&self) -> (FormattedSlice<'static>, &str);
    /// Return line content.
    fn format_content(&self) -> FormattedSlice<'_>;
}

pub type ActionableDiffLine = Rc<DiffLine<Status, OpTag>>;

pub struct BlockGround {
    /// The range of the lines in the original file.
    pub(crate) range: Range<LineIndex>,
    pub(crate) folded: bool,
}

/// The current lines of a changed block. This is shared between `BlockChange` and the
/// resolution cache.
pub(crate) struct BlockCurrent {
    /// The lines to be applied, replacing the old lines. There may be multiple options.
    /// The options are stored from the most likely to the least likely.
    new_lines: Vec<Lines>,
    /// The confidence of the individual options. If this change was not resolved by AI,
    /// this will be empty.
    confidence: Vec<u8>,
    /// Index of the currently applied item from new_lines.
    index: usize,
    /// Is the current new_lines a result of edit operation?
    edited: bool,
    /// Was this block acked?
    acked: bool,
}

pub struct BlockChange {
    /// Range of the region where the change should be applied, including the context.
    /// It's an inter-line position.
    pub(crate) range: Range<LineIndex>,
    /// The current (new) lines and metadata around them.
    current: BlockCurrent,
    /// The hunk that got applied.
    pub(crate) hunk: Hunk,
    /// The fuzz with which the hunk got originally applied.
    pub(crate) fuzz: AppliedFuzz,
    /// Is there any operation currently in progress? For the state computation, this takes
    /// priority.
    in_progress: BlockChangeInProgress,
    /// An indicator whether suggestion collection is in progress or whether it has finished
    /// and with what result.
    suggestion_search_state: SuggestionSearchState,
    /// List of missing dependencies.
    pub missing: Vec<Rc<git::Description>>,
}

pub struct BlockUnapplied {
    hunk: Hunk,
    acked: bool,
}

#[derive(derive_more::From)]
enum BlockType {
    /// The "ground" between two hunks. It's a piece of code that's not changed.
    Ground(BlockGround),
    Change(BlockChange),
    Unapplied(BlockUnapplied),
}

/// The rendered data of the block for displaying by UI.
#[derive(Default)]
struct BlockRendered {
    lines: Vec<ActionableDiffLine>,
    /// The count of lines in this block that have a line number.
    numbered: usize,
}

pub struct Block {
    /// The unique id of this block. Stays constant when blocks are moved around.
    pub(crate) unique_id: UniqueId,
    details: BlockType,
    undo_stack: Vec<Box<dyn undo::Undo>>,
    rendered: BlockRendered,
}

pub struct Blocks {
    blocks: Vec<Block>,
    pub(crate) old_lines: TypedLines,
    unique_id_generator: UniqueIdGenerator,
}

#[derive(Clone, Default, PartialEq)]
pub enum SuggestionSearchState {
    #[default]
    NotStarted,
    InProgress,
    Done,
    Err(String),
}

pub enum ProgressIndicator {
    None,
    AI,
    Edit,
}

impl OpTag {
    fn has_line_number(&self) -> bool {
        match self {
            Self::Normal |
            Self::Insert |
            Self::Foldable(_) => true,
            Self::Delete |
            Self::Header { .. } |
            Self::Folded => false,
        }
    }

    fn css_class(&self) -> &'static str {
        match self {
            Self::Normal => "diff-normal",
            Self::Delete => "diff-delete",
            Self::Insert => "diff-insert",
            Self::Header { .. } => "diff-header",
            Self::Foldable(_) => "diff-expanded",
            Self::Folded => "diff-folded",
        }
    }

    fn indicator(&self) -> &'static str {
        match self {
            Self::Normal => " ",
            Self::Delete => "-",
            Self::Insert => "+",
            Self::Header { .. } => " ",
            Self::Foldable(state) => match state {
                Tristate::Start => "┌",
                Tristate::Middle => "│",
                Tristate::End => "└",
            }
            Self::Folded => "✀",
        }
    }
}

impl From<Op> for OpTag {
    fn from(value: Op) -> Self {
        match value {
            Op::Added => Self::Insert,
            Op::Removed => Self::Delete,
            Op::Context => Self::Normal,
        }
    }
}

impl FormatHalfLine for DiffLineHalf<OpTag> {
    fn format_indicator(&self) -> (FormattedSlice<'static>, &str) {
        let indicator = self.op.indicator();
        let css_class = self.op.css_class();
        let fmt = if self.op_highlighted {
            FormattedSlice::from_str_highlighted(indicator)
        } else {
            FormattedSlice::from_str(indicator)
        };
        (fmt, css_class)
    }

    fn format_content(&self) -> FormattedSlice<'_> {
        self.content.as_slice_nonl()
    }
}

impl<'a> fmt::Display for BlockChangeState<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Clean => write!(f, "clean"),
            Self::ContextFuzz => write!(f, "context fuzz"),
            Self::Fuzzy => write!(f, "fuzzy"),
            Self::Interim => write!(f, "in progress…"),
            Self::Edited => write!(f, "edited"),
            Self::Editing => write!(f, "editing…"),
            Self::Failed(msg) => write!(f, "failed: {}", msg),
            Self::Ai(confidence) => write!(f, "AI confidence {}%", confidence),
        }
    }
}

impl BlockGround {
    fn new(range: Range<LineIndex>, folded: bool) -> Self {
        Self {
            range,
            folded,
        }
    }

    fn iter_bytes<'a>(&self, old_lines: &'a Lines)
        -> Box<dyn Iterator<Item = &'a [u8]> + 'a>
    {
        Box::new(old_lines.line_slice(self.range.clone()).iter())
    }
}

impl BlockChange {
    fn new(applied: AppliedChange) -> Self {
        Self {
            range: applied.range,
            current: BlockCurrent {
                new_lines: vec![applied.new_lines],
                confidence: Vec::new(),
                index: 0,
                edited: false,
                acked: false,
            },
            hunk: applied.hunk,
            fuzz: applied.fuzz,
            in_progress: BlockChangeInProgress::None,
            suggestion_search_state: SuggestionSearchState::default(),
            missing: Vec::new(),
        }
    }

    fn from_current(current: BlockCurrent, applied: AppliedChange) -> Self {
        Self {
            range: applied.range,
            current,
            hunk: applied.hunk,
            fuzz: applied.fuzz,
            in_progress: BlockChangeInProgress::None,
            suggestion_search_state: SuggestionSearchState::default(),
            missing: Vec::new(),
        }
    }

    fn state(&self) -> BlockChangeState<'_> {
        match &self.in_progress {
            BlockChangeInProgress::None => {
                if self.current.edited {
                    BlockChangeState::Edited
                } else if self.current.confidence.is_empty() {
                    match &self.fuzz {
                        AppliedFuzz::None => BlockChangeState::Clean,
                        AppliedFuzz::Context => BlockChangeState::ContextFuzz,
                        AppliedFuzz::Content => BlockChangeState::Fuzzy,
                    }
                } else {
                    BlockChangeState::Ai(
                        self.current.confidence[self.current.index],
                    )
                }
            }
            BlockChangeInProgress::Interim => BlockChangeState::Interim,
            BlockChangeInProgress::Editing => BlockChangeState::Editing,
            BlockChangeInProgress::Failed(msg) => BlockChangeState::Failed(msg),
        }
    }

    fn get_available_commands(&self) -> Commands {
        if self.current.acked {
            return Commands::UNDO;
        }
        let state = self.state();
        let mut commands = Commands::ACK | Commands::EDIT | Commands::REMOVE | Commands::UNDO;
        match state {
            BlockChangeState::Interim |
            BlockChangeState::Editing => return Commands::empty(),
            BlockChangeState::Edited => return commands,
            BlockChangeState::Failed(_) => return Commands::RETRY,
            BlockChangeState::Clean |
            BlockChangeState::ContextFuzz |
            BlockChangeState::Fuzzy |
            BlockChangeState::Ai(_) => (),
        }
        if self.current.index > 0 {
            commands |= Commands::PREV;
        }
        if self.current.new_lines.len() > self.current.index + 1 {
            commands |= Commands::NEXT;
        }
        if state == BlockChangeState::ContextFuzz {
            commands |= Commands::AI;
        }
        commands
    }

    fn get_progress_indicator(&self) -> ProgressIndicator {
        match self.state() {
            BlockChangeState::Interim => ProgressIndicator::AI,
            BlockChangeState::Editing => ProgressIndicator::Edit,
            _ => ProgressIndicator::None,
        }
    }

    fn iter_bytes(&self) -> Box<dyn Iterator<Item = &[u8]> + '_> {
        match self.state() {
            BlockChangeState::Clean |
            BlockChangeState::ContextFuzz |
            BlockChangeState::Fuzzy |
            BlockChangeState::Edited |
            BlockChangeState::Ai(_) =>
                Box::new(self.current.new_lines[self.current.index].iter()),
            BlockChangeState::Interim |
            BlockChangeState::Editing |
            BlockChangeState::Failed(_) =>
                panic!("Iteration over a non-resolved block"),
        }
    }

    fn set_editing(&mut self, editing: bool) {
        if editing {
            assert!(self.in_progress == BlockChangeInProgress::None);
            self.in_progress = BlockChangeInProgress::Editing;
        } else {
            assert!(self.in_progress == BlockChangeInProgress::Editing);
            self.in_progress = BlockChangeInProgress::None;
        }
    }
}

impl BlockUnapplied {
    fn new(hunk: Hunk) -> Self {
        Self {
            hunk: hunk,
            acked: false,
        }
    }

    fn get_available_commands(&self) -> Commands {
        let mut commands = Commands::UNDO;
        if !self.acked {
            commands |= Commands::ACK;
        }
        commands
    }
}

impl BlockType {
    fn to_change(self) -> BlockChange {
        match self {
            Self::Change(change) => change,
            _ => panic!("not a change block"),
        }
    }

    fn to_unapplied(self) -> BlockUnapplied {
        match self {
            Self::Unapplied(unapplied) => unapplied,
            _ => panic!("not an unapplied block"),
        }
    }
}

impl BlockRendered {
    fn render_interdiff(
        old_lines: LinesSlice<'_>,
        new_lines: &Lines,
        hunk: &Hunk,
    ) -> Self {
        let input = imara_diff::InternedInput::new(
            old_lines.iter(),
            new_lines.iter(),
        );
        let mut diff = imara_diff::Diff::compute(imara_diff::Algorithm::Histogram, &input);
        diff.postprocess_lines(&input);
        let builder = InterdiffedLinesBuilder::new(
            old_lines,
            new_lines.as_line_slice(),
        );
        let backport_lines = builder.build(diff.hunks());
        let upstream_lines: Vec<InterdiffedLine> = hunk.lines.iter()
            .map(|line| line.into())
            .collect();
        let numbered = backport_lines.iter()
            .map(|line| if line.op != Op::Removed { 1 } else { 0 })
            .sum();
        let mut interdiff = Interdiff::new(backport_lines, upstream_lines);
        interdiff.add_header(Status::Commands, OpTag::Header, hunk.format_header());
        Self {
            lines: interdiff.generate(),
            numbered,
        }
    }

    /// Create lines with the hunk on the original side and a single status line on the
    /// backport side.
    fn render_one_side_lines(hunk: &Hunk) -> Self {
        let mut lines = Vec::with_capacity(hunk.lines.len() + 1);
        lines.push(Rc::new(
            DiffLine::new_header(Status::Commands, OpTag::Header, hunk.format_header())
        ));
        lines.extend(hunk.lines.iter()
            .map(|line| {
                Rc::new(DiffLine::new_original(
                    line.op.into(),
                    line.content.to_str_lossy().into_owned(),
                ))
            }));
        Self {
            lines,
            numbered: 0,
        }
    }

    fn render_ground(lines: &Lines, range: Range<LineIndex>) -> Self {
        let len = range.len();
        let lines = lines.line_slice(range).iter_strings()
            .enumerate()
            .map(|(i, s)| {
                let state = if i == 0 {
                    Tristate::Start
                } else if i == len - 1 {
                    Tristate::End
                } else {
                    Tristate::Middle
                };
                Rc::new(DiffLine::new_backport(OpTag::Foldable(state), s))
            })
            .collect();
        Self {
            lines,
            numbered: len,
        }
    }

    fn render_folded(range: Range<LineIndex>) -> Self {
        Self {
            lines: vec![Rc::new(DiffLine::new_backport(OpTag::Folded, "[...]".to_string()))],
            numbered: range.len(),
        }
    }
}

impl Block {
    fn new_init<F: FnOnce(&mut Block)>(
        old_lines: &Lines,
        unique_id: UniqueId,
        details: impl Into<BlockType>,
        init: F,
    ) -> Self {
        let mut result = Self {
            unique_id,
            details: details.into(),
            undo_stack: Vec::new(),
            rendered: BlockRendered::default(),
        };
        init(&mut result);
        result.render(old_lines);
        result
    }

    fn new(
        old_lines: &Lines,
        unique_id: UniqueId,
        details: impl Into<BlockType>,
    ) -> Self {
        Self::new_init(old_lines, unique_id, details, |_| ())
    }

    fn with_undo_stack(mut self, undo_stack: Vec<Box<dyn undo::Undo>>) -> Self {
        self.undo_stack = undo_stack;
        self
    }

    fn render(&mut self, old_lines: &Lines) {
        self.rendered = match &self.details {
            BlockType::Ground(ground) => match ground.folded {
                false => BlockRendered::render_ground(old_lines, ground.range.clone()),
                true => BlockRendered::render_folded(ground.range.clone()),
            }
            BlockType::Change(change) => match change.state() {
                BlockChangeState::Clean |
                BlockChangeState::ContextFuzz |
                BlockChangeState::Fuzzy |
                BlockChangeState::Editing |
                BlockChangeState::Edited |
                BlockChangeState::Ai(_) =>
                    BlockRendered::render_interdiff(
                        old_lines.line_slice(change.range.clone()),
                        &change.current.new_lines[change.current.index],
                        &change.hunk,
                    ),
                BlockChangeState::Interim |
                BlockChangeState::Failed(_) =>
                    BlockRendered::render_one_side_lines(&change.hunk),
            },
            BlockType::Unapplied(unapplied) => BlockRendered::render_one_side_lines(
                &unapplied.hunk,
            ),
        };
        self.ensure_suggestions_rendered();
    }

    fn rendered_len(&self) -> usize {
        match &self.details {
            BlockType::Ground(ground) =>
                if ground.folded && self.rendered.lines.len() > 0 {
                    1
                } else {
                    self.rendered.lines.len()
                },
            BlockType::Change(_) => self.rendered.lines.len(),
            BlockType::Unapplied(_) => self.rendered.lines.len(),
        }
    }

    /// Calculate line number of the given line (relative index in the block). The
    /// line number is relative in this block, with the first numbered line in the
    /// block having number 1. Returns None if the line does not have a number.
    fn calculate_line_number(&self, index: usize) -> Option<NonZeroU32> {
        let mut line_number = 0;
        if matches!(
            self.rendered.lines[index].backport.op(), Some(op) if op.has_line_number()
        ) {
            line_number += 1;
            for j in 0..index {
                if matches!(
                    self.rendered.lines[j].backport.op(), Some(op) if op.has_line_number()
                ) {
                    line_number += 1;
                }
            }
        }
        NonZeroU32::new(line_number as u32)
    }

    pub fn rendered_lines(&self) -> &[ActionableDiffLine] {
        &self.rendered.lines
    }

    pub(crate) fn as_ground(&self) -> &BlockGround {
        match &self.details {
            BlockType::Ground(ground) => ground,
            _ => panic!("not a ground block"),
        }
    }

    pub(crate) fn as_ground_mut(&mut self) -> &mut BlockGround {
        match &mut self.details {
            BlockType::Ground(ground) => ground,
            _ => panic!("not a ground block"),
        }
    }

    pub(crate) fn to_ground(self) -> BlockGround {
        match self.details {
            BlockType::Ground(ground) => ground,
            _ => panic!("not a ground block"),
        }
    }

    pub fn as_change(&self) -> &BlockChange {
        match &self.details {
            BlockType::Change(change) => change,
            _ => panic!("not a change block"),
        }
    }

    pub(crate) fn as_change_mut(&mut self) -> &mut BlockChange {
        match &mut self.details {
            BlockType::Change(change) => change,
            _ => panic!("not a change block"),
        }
    }

    pub fn try_as_change(&self) -> Option<&BlockChange> {
        match &self.details {
            BlockType::Change(change) => Some(change),
            _ => None,
        }
    }

    pub(crate) fn as_unapplied_mut(&mut self) -> &mut BlockUnapplied {
        match &mut self.details {
            BlockType::Unapplied(unapplied) => unapplied,
            _ => panic!("not an unapplied block"),
        }
    }

    pub(crate) fn is_change(&self) -> bool {
        matches!(self.details, BlockType::Change(_))
    }

    pub(crate) fn is_unapplied(&self) -> bool {
        matches!(self.details, BlockType::Unapplied(_))
    }

    pub(crate) fn is_unacked(&self) -> bool {
        match &self.details {
            BlockType::Ground(_) => false,
            BlockType::Change(change) => !change.current.acked,
            BlockType::Unapplied(unapplied) => !unapplied.acked,
        }
    }

    pub(crate) fn is_unacked_clean(&self) -> bool {
        match &self.details {
            BlockType::Ground(_) => false,
            BlockType::Change(change) =>
                !change.current.acked && change.state() == BlockChangeState::Clean,
            BlockType::Unapplied(_) => false,
        }
    }

    /// Return `Ok(true)` if the block is pending external resolution (AI inference or edit)
    /// and `Ok(false)` if it's not. Return `Err` if the block failed external resolution.
    pub fn is_pending(&self) -> Result<bool, String> {
        Ok(match &self.details {
            BlockType::Ground(_) => false,
            BlockType::Change(change) => match change.state() {
                BlockChangeState::Clean => false,
                BlockChangeState::ContextFuzz => false,
                BlockChangeState::Fuzzy => false,
                BlockChangeState::Interim => true,
                BlockChangeState::Editing => true,
                BlockChangeState::Edited => false,
                BlockChangeState::Failed(msg) => Err(msg.to_string())?,
                BlockChangeState::Ai(_) => false,
            }
            BlockType::Unapplied(_) => false,
        })
    }

    /// Return `true` if suggestions are still being resolved for the block.
    pub fn has_pending_suggestions(&self) -> bool {
        match &self.details {
            BlockType::Ground(_) => false,
            BlockType::Change(change) => match &change.suggestion_search_state {
                SuggestionSearchState::NotStarted => false,
                SuggestionSearchState::InProgress => true,
                SuggestionSearchState::Done => false,
                SuggestionSearchState::Err(_) => false,
            }
            BlockType::Unapplied(_) => false,
        }
    }

    fn was_ai_used(&self) -> bool {
        match &self.details {
            BlockType::Ground(_) => (),
            BlockType::Change(change) =>
                if matches!(change.state(), BlockChangeState::Ai(_)) {
                    return true;
                }
            BlockType::Unapplied(_) => (),
        }
        for op in self.undo_stack.iter() {
            if (op as &dyn Any).is::<undo::UndoAi>() {
                return true;
            }
        }
        false
    }

    pub fn get_state_label(&self) -> String {
        match &self.details {
            BlockType::Ground(_) => String::new(),
            BlockType::Change(change) => change.state().to_string(),
            BlockType::Unapplied(_) => "not applied".to_string(),
        }
    }

    pub fn get_progress_indicator(&self) -> ProgressIndicator {
        match &self.details {
            BlockType::Ground(_) => ProgressIndicator::None,
            BlockType::Change(change) => change.get_progress_indicator(),
            BlockType::Unapplied(_) => ProgressIndicator::None,
        }
    }

    pub fn get_available_commands(&self) -> Commands {
        let mut commands = match &self.details {
            BlockType::Ground(_) => Commands::empty(),
            BlockType::Change(change) => change.get_available_commands(),
            BlockType::Unapplied(unapplied) => unapplied.get_available_commands(),
        };
        if self.undo_stack.is_empty() {
            commands &= !Commands::UNDO;
        }
        commands
    }

    pub(crate) fn set_acked(&mut self, acked: bool) {
        match &mut self.details {
            BlockType::Ground(_) => panic!("ground block cannot be acked"),
            BlockType::Change(change) => change.current.acked = acked,
            BlockType::Unapplied(unapplied) => unapplied.acked = acked,
        }
    }

    fn iter_bytes<'a>(&'a self, old_lines: &'a Lines)
        -> Box<dyn Iterator<Item = &'a [u8]> + 'a>
    {
        match &self.details {
            BlockType::Ground(ground) => ground.iter_bytes(old_lines),
            BlockType::Change(change) => change.iter_bytes(),
            BlockType::Unapplied(_) => Box::new(iter::empty()),
        }
    }

    fn old_lines_slice<'a>(&self, old_lines: &'a Lines) -> LinesSlice<'a> {
        match &self.details {
            BlockType::Ground(ground) => old_lines.line_slice(ground.range.clone()),
            BlockType::Change(change) => old_lines.line_slice(change.range.clone()),
            BlockType::Unapplied(_) => panic!("unapplied block does not have old lines"),
        }
    }

    fn ensure_suggestions_rendered(&mut self) {
        if let BlockType::Change(change) = &mut self.details {
            if 
                self.rendered.lines.len() < 2 ||
                !self.rendered.lines[1].is_header_with_status(|&s| s == Status::Suggestions)
            {
                if change.suggestion_search_state == SuggestionSearchState::NotStarted {
                    return;
                }
                let diff_line = Rc::new(DiffLine::new_header(
                    Status::Suggestions,
                    OpTag::Header,
                    "\n".to_string()
                ));
                self.rendered.lines.insert(1, diff_line);
            } else {
                assert!(change.suggestion_search_state != SuggestionSearchState::NotStarted);
            }
        }
    }

    pub(crate) fn set_suggestion_search_state(&mut self, state: SuggestionSearchState) {
        assert!(state != SuggestionSearchState::NotStarted);
        self.as_change_mut().suggestion_search_state = state;
        self.ensure_suggestions_rendered();
    }

    pub fn suggestion_search_state(&self) -> &SuggestionSearchState {
        &self.as_change().suggestion_search_state
    }
}

impl Blocks {
    pub(crate) fn new(old_lines: TypedLines, unique_id_generator: UniqueIdGenerator) -> Self {
        Self {
            blocks: Vec::new(),
            old_lines,
            unique_id_generator,
        }
    }

    fn create_block(&self, details: impl Into<BlockType>) -> Block {
        Block::new(
            &self.old_lines,
            self.unique_id_generator.generate(),
            details,
        )
    }

    /// Create a new ground block (that is, collapsable block with unchanged lines between
    /// hunks).
    pub(crate) fn create_ground(&self, range: Range<LineIndex>, folded: bool) -> Block
    {
        Block::new(
            &self.old_lines,
            self.unique_id_generator.generate(),
            BlockGround::new(range, folded),
        )
    }

    /// Create a new change block for the given applied hunk.
    pub(crate) fn create_change(&self, change: AppliedChange, acked_clean: bool) -> Block {
        Block::new_init(
            &self.old_lines,
            self.unique_id_generator.generate(),
            BlockChange::new(change),
            |block| {
                let hunk = block.as_change_mut();
                if acked_clean && hunk.state() == BlockChangeState::Clean {
                    hunk.current.acked = true;
                }
            }
        )
    }

    /// Create an interim block (waiting for an AI operation to finish).
    pub(crate) fn create_interim(&self, change: AppliedChange) -> Block {
        Block::new_init(
            &self.old_lines,
            self.unique_id_generator.generate(),
            BlockChange::new(change),
            |block| {
                block.as_change_mut().in_progress = BlockChangeInProgress::Interim;
            }
        )
    }

    /// Create an unapplied block.
    pub(crate) fn create_unapplied(&self, hunk: Hunk, acked: bool) -> Block {
        Block::new_init(
            &self.old_lines,
            self.unique_id_generator.generate(),
            BlockUnapplied::new(hunk),
            |block| {
                let unapplied = block.as_unapplied_mut();
                unapplied.acked = acked;
            }
        )
    }

    /// Create a new change block from a resolution cache.
    pub(crate) fn create_from_cache(
        &self,
        cache: cache::ResolutionCacheEntry,
        change: AppliedChange,
    ) -> Block {
        Block::new_init(
            &self.old_lines,
            self.unique_id_generator.generate(),
            BlockChange::from_current(cache.current, change),
            |block| {
                block.undo_stack = cache.undo_stack;
            }
        )
    }

    /// Appends a block and returns its block_index.
    pub(crate) fn push(&mut self, block: Block) -> usize {
        self.blocks.push(block);
        self.blocks.len() - 1
    }

    /// Inserts the block to the given block_index.
    fn insert(&mut self, block_index: usize, block: Block) {
        self.blocks.insert(block_index, block);
    }

    /// Remove the given block. Does not calculate the changed lines, as it is assumed this
    /// is only a part of a larger change.
    pub(crate) fn remove(&mut self, block_index: usize) -> Block {
        self.blocks.remove(block_index)
    }

    fn update_block<F: FnOnce(&mut Block)>(
        &mut self,
        block_index: usize,
        f: F,
    ) -> ListChange {
        let position = self.block_to_rendered_index(block_index);
        let block = &mut self.blocks[block_index];
        let old_block_len = block.rendered_len();
        let old_numbered = block.rendered.numbered;
        f(block);
        block.render(&self.old_lines);
        let new_block_len = block.rendered_len();
        if block.rendered.numbered == old_numbered {
            ListChange::changed(position, old_block_len, new_block_len)
        } else {
            // The line numbers in the following blocks are now different. Need to redraw
            // everything till the end of the file.
            let new_len = self.rendered_len();
            ListChange::changed(
                position,
                new_len - new_block_len + old_block_len - position,
                new_len - position,
            )
        }
    }

    fn update_ground(
        &mut self,
        block_index: usize,
        range: Range<LineIndex>,
        folded: bool,
    ) -> ListChange {
        self.update_block(block_index, |block| {
            let ground = block.as_ground_mut();
            ground.range = range;
            ground.folded = folded;
        })
    }

    fn set_acked(&mut self, block_index: usize, acked: bool) -> ListChange {
        let block = &mut self.blocks[block_index];
        assert!(block.is_unacked() == acked);
        block.set_acked(acked);
        ListChange::updated_item(self.block_to_rendered_index(block_index))
    }

    /// Mark the given block as acked. Panics if the block is not a change or unapplied block.
    /// Returns the range of changed lines.
    pub(crate) fn command_ack(&mut self, block_index: usize) -> Option<ListChange> {
        if self.get(block_index).is_unacked() {
            self.push_undo(block_index, undo::UndoAck {});
            Some(self.set_acked(block_index, true))
        } else {
            None
        }
    }

    /// Mark the given block as acked if it is a change block and if it is clean and not
    /// yet acked.
    /// Returns the range of changed lines if the block was newly acked. Otherwise, returns
    /// None.
    pub(crate) fn command_ack_if_clean(&mut self, block_index: usize) -> Option<ListChange> {
        if self.get(block_index).is_unacked_clean() {
            self.push_undo(block_index, undo::UndoAck {});
            self.get_mut(block_index).set_acked(true);
            Some(ListChange::updated_item(self.block_to_rendered_index(block_index)))
        } else {
            None
        }
    }

    fn set_current_index(&mut self, block_index: usize, index: usize) -> ListChange {
        self.update_block(block_index, |block| {
            block.as_change_mut().current.index = index;
        })
    }

    /// Switch to the next or previous new lines. Panics if the block is not a change block.
    /// Returns the range of changed lines.
    pub(crate) fn command_prev_next(
        &mut self,
        block_index: usize,
        next: bool,
    ) -> ListChange {
        let (index, undo) = {
            let change = self.get(block_index).as_change();
            let cur = &change.current;
            let new_index = if next && cur.index + 1 < cur.new_lines.len() {
                cur.index + 1
            } else if !next && cur.index > 0 {
                cur.index - 1
            } else {
                cur.index
            };
            (new_index, undo::UndoPrevNext::from_block_change(change))
        };
        self.push_undo(block_index, undo);
        self.set_current_index(block_index, index)
    }

    /// Mark the given block as being edited. Panics if the block is not a change block.
    /// Panics if there's already editing or AI inference in progress on the block.
    /// Returns the range of changed lines.
    pub(crate) fn set_change_editing(
        &mut self,
        block_index: usize,
    ) -> (Vec<u8>, UniqueId, ListChange) {
        let position = self.block_to_rendered_index(block_index);
        let block = &mut self.blocks[block_index];
        let data = block.iter_bytes(&self.old_lines)
            .flatten()
            .copied()
            .collect();
        block.as_change_mut().set_editing(true);
        (data, block.unique_id, ListChange::updated_item(position))
    }

    /// Cancel editation of the given block. Panics if the block is not a change block
    /// or if it's not marked as being edited.
    /// Returns the range of changed lines.
    pub(crate) fn clear_change_editing(
        &mut self,
        block_index: usize,
    ) -> ListChange {
        let position = self.block_to_rendered_index(block_index);
        let block = &mut self.blocks[block_index];
        block.as_change_mut().set_editing(false);
        ListChange::updated_item(position)
    }

    /// Mark the block at the given block index as having AI inference in progress.
    /// Panics if the block is not a change block. Panics if there's already editing or
    /// AI inference in progress on the block.
    /// Returns the range of changed lines.
    pub(crate) fn set_change_interim(&mut self, block_index: usize) -> ListChange {
        self.update_block(block_index, |block| {
            let change = block.as_change_mut();
            assert!(
                change.in_progress == BlockChangeInProgress::None ||
                matches!(change.in_progress, BlockChangeInProgress::Failed(_))
            );
            change.in_progress = BlockChangeInProgress::Interim;
        })
    }

    /// Cancel AI inference of the given block and set an error message. Panics if the block
    /// is not a change block or if it's not marked as having AI inference in progress.
    /// Returns the range of changed lines.
    pub(crate) fn set_change_failed(&mut self, block_index: usize, msg: String)
        -> ListChange
    {
        self.update_block(block_index, |block| {
            let change = block.as_change_mut();
            assert!(change.in_progress == BlockChangeInProgress::Interim);
            change.in_progress = BlockChangeInProgress::Failed(msg);
        })
    }

    fn set_new_lines(
        &mut self,
        block_index: usize,
        new_lines: Vec<Lines>,
        index: usize,
        confidence: Vec<u8>,
        edited: bool,
    ) -> ListChange {
        self.update_block(block_index, |block| {
            let change = block.as_change_mut();
            change.in_progress = BlockChangeInProgress::None;
            change.current.new_lines = new_lines;
            change.current.index = index;
            change.current.confidence = confidence;
            change.current.edited = edited;
        })
    }

    /// Set the new content of the block and mark it as edited. Panics if the block is not
    /// a change block or if it's not marked as being edited.
    /// Returns the range of changed lines.
    pub(crate) fn command_edited(
        &mut self,
        block_index: usize,
        new_lines: Lines,
    ) -> ListChange {
        let change = self.get(block_index).as_change();
        assert!(change.in_progress == BlockChangeInProgress::Editing);
        let undo = undo::UndoEdited::from_block_change(change);
        self.push_undo(block_index, undo);
        self.set_new_lines(block_index, vec![new_lines], 0, Vec::new(), true)
    }

    /// Replace the content of the block at the given block index with the newly computed
    /// lines.
    pub(crate) fn command_ai(
        &mut self,
        block_index: usize,
        new_lines: Vec<Lines>,
        confidence: Vec<u8>,
        no_undo: bool,
    ) -> ListChange {
        let change = self.get(block_index).as_change();
        assert!(change.in_progress == BlockChangeInProgress::Interim);
        if !no_undo {
            let undo = undo::UndoAi::from_block_change(change);
            self.push_undo(block_index, undo);
        }
        self.set_new_lines(block_index, new_lines, 0, confidence, false)
    }

    /// Unapply the given block. Remove it from its current position and move it to the
    /// end. Merge the surrounding ground blocks. Panics if the block is not a change block.
    /// Returns the range of changed lines.
    pub(crate) fn command_unapply(&mut self, block_index: usize) -> ListChange {
        // We're copying the fields to UndoUnapply, only to later drop them. It would be
        // more efficient to move them. However, it would make the code more complex.
        // Giving precedence to code maintainability here.
        let undo = undo::UndoUnapply::from_block_change(self.get(block_index).as_change());
        self.push_undo(block_index, undo);
        let old_len = self.rendered_len();
        // There's always a ground block before and after this change block. Merge them.
        let position = self.block_to_rendered_index(block_index - 1);
        let next = self.remove(block_index + 1).to_ground();
        let prev = self.get(block_index - 1).as_ground();
        let range = prev.range.start .. next.range.end;
        let folded = prev.folded && next.folded;
        self.update_ground(block_index - 1, range, folded);
        // remove the block and add it as unapplied
        let removed = self.remove(block_index);
        let block = self.create_unapplied(removed.details.to_change().hunk, true)
            .with_undo_stack(removed.undo_stack);
        self.push(block);
        ListChange::changed(position, old_len - position, self.rendered_len() - position)
    }

    fn undo_unapply(&mut self, block_index: usize, block: Block, old_len: usize)
        -> ListChange
    {
        let position = self.block_to_rendered_index(block_index);
        let ground_range = &self.get(block_index).as_ground().range;
        let change_range = &block.as_change().range;
        let prev_range = ground_range.start .. change_range.start;
        let next_range = change_range.end .. ground_range.end;
        let folded = self.get(block_index).as_ground().folded;
        self.update_ground(block_index, prev_range, folded);
        self.insert(block_index + 1, block);
        let next = self.create_ground(next_range, folded);
        self.insert(block_index + 2, next);
        ListChange::changed(position, old_len - position, self.rendered_len() - position)
    }

    /// Toggle fold of the given block. If `allow_shrink` is false, the block won't be
    /// folded. If `allow_expand` is false, the block won't be unfolded. Panics if the block
    /// is not a ground block.
    /// Returns the range of changed lines if the block was changed or None if it was not.
    pub(crate) fn set_or_toggle_fold(
        &mut self,
        block_index: usize,
        allow_shrink: bool,
        allow_expand: bool,
    ) -> Option<ListChange> {
        let ground = self.get(block_index).as_ground();
        if (!allow_shrink && !ground.folded) || (!allow_expand && ground.folded) {
            return None;
        }
        Some(self.update_block(block_index, |block| {
            let ground = block.as_ground_mut();
            ground.folded = !ground.folded;
        }))
    }

    fn push_undo(&mut self, block_index: usize, entry: impl undo::Undo + 'static) {
        self.get_mut(block_index).undo_stack.push(Box::new(entry));
    }

    pub(crate) fn pop_undo(&mut self, block_index: usize) -> Option<Box<dyn undo::Undo>> {
        self.get_mut(block_index).undo_stack.pop()
    }

    /// Get the range of the line with suggestions as `ListChange`. This is useful to
    /// signalize UI update. Panics if the given block does not have suggestions displayed.
    pub(crate) fn suggestion_range(&self, block_index: usize) -> ListChange {
        let position = self.block_to_rendered_index(block_index);
        let block = self.get(block_index);
        assert!(
            block.as_change().suggestion_search_state != SuggestionSearchState::NotStarted
        );
        ListChange::updated_item(position + 1)
    }

    /// Find the ground block that contains the given range. It must contain the whole range.
    /// Returns the block_index of the ground block. If no such single ground block exists,
    /// returns None.
    pub(crate) fn find_ground_with_range(&self, range: Range<LineIndex>) -> Option<usize> {
        for (block_index, block) in self.blocks.iter().enumerate() {
            if let BlockType::Ground(ground) = &block.details {
                if
                    ground.range.start <= range.start &&
                    ground.range.end >= range.end
                {
                    return Some(block_index);
                }
            }
        }
        None
    }

    /// Find the block the given line index belongs to. Returns None if the index is too
    /// big. Returns the block index, relative position in that block and the last line
    /// number before the found block.
    fn find_rendered_block(&self, index: usize) -> Option<(usize, usize, usize)> {
        let mut i = 0;
        let mut cnt = 0;
        for (block_index, block) in self.blocks.iter().enumerate() {
            let visible = block.rendered_len();
            if i + visible > index {
                return Some((block_index, index - i, cnt));
            }
            i += visible;
            cnt += block.rendered.numbered;
        }
        None
    }

    /// Find the block the given rendered line index belongs to. Returns None if the index is
    /// too big. Otherwise, returns the block index and the position in the block.
    pub fn rendered_index_to_block_pos(&self, index: usize) -> Option<(usize, usize)> {
        self.find_rendered_block(index).map(|(block_index, pos, _)| (block_index, pos))
    }

    /// Find the block the given rendered line index belongs to. Returns None if the index is
    /// too big. Otherwise, returns the block index, relative position of the line in the
    /// block and the line number of the line.
    pub fn rendered_index_to_block_details(&self, index: usize)
            -> Option<(usize, usize, Option<NonZeroU32>)> {
        let (block_index, pos, line_number) = self.find_rendered_block(index)?;
        let block = &self.blocks[block_index];
        let line_number = block.calculate_line_number(pos)
            .map(|add| add.saturating_add(line_number as u32));
        Some((block_index, pos, line_number))
    }

    /// Return the rendered index of the first line of the given block. For block_index
    /// pointing behind the last block, the total number of rendered lines is returned.
    pub(crate) fn block_to_rendered_index(&self, block_index: usize) -> usize {
        self.blocks[0 .. block_index].iter()
            .map(|block| block.rendered_len())
            .sum()
    }

    /// Return the line index given the block index and a relative position in the block.
    /// `block_to_index(bi)` is equivalent to `block_pos_to_index(bi, 0)`.
    pub fn block_pos_to_rendered_index(&self, block_index: usize, pos: usize)
        -> usize
    {
        let block = &self.blocks[block_index];
        let start = self.block_to_rendered_index(block_index);
        start + min(pos, block.rendered_len().saturating_sub(1))
    }

    /// Return the rendered line index range of the given block.
    pub fn block_to_rendered_index_range(&self, block_index: usize) -> Range<usize> {
        let block = &self.blocks[block_index];
        let start = self.block_to_rendered_index(block_index);
        start .. start + block.rendered_len()
    }

    /// Return the number of visible lines.
    pub fn rendered_len(&self) -> usize {
        self.block_to_rendered_index(self.blocks.len())
    }

    pub fn count_unacked(&self) -> usize {
        self.blocks.iter().filter(|block| block.is_unacked()).count()
    }

    pub fn count_unacked_clean(&self) -> usize {
        self.blocks.iter().filter(|block| block.is_unacked_clean()).count()
    }

    /// Get the number of blocks that were modified. That is, edited and AI generated blocks.
    pub(crate) fn count_modified(&self) -> usize {
        self.blocks.iter()
            .filter(|block| block.is_change())
            .filter(|block| match block.as_change().state() {
                BlockChangeState::Edited | BlockChangeState::Ai(_) => true,
                _ => false,
            })
            .count()
    }

    pub(crate) fn was_ai_used(&self) -> bool {
        self.blocks.iter().any(|block| block.was_ai_used())
    }

    /// Return the number of blocks.
    pub(crate) fn len(&self) -> usize {
        self.blocks.len()
    }

    /// Get a reference to the block with the given block index.
    pub fn get(&self, block_index: usize) -> &Block {
        &self.blocks[block_index]
    }

    /// Get a mutable reference to the block with the given block index.
    pub(crate) fn get_mut(&mut self, block_index: usize) -> &mut Block {
        &mut self.blocks[block_index]
    }

    /// Get an iterator over blocks.
    pub(crate) fn iter(&self) -> impl Iterator<Item = &Block> {
        self.blocks.iter()
    }

    /// Iterate over the resolved file, returning lines as bytes.
    pub fn iter_bytes(&self) -> impl Iterator<Item = &[u8]> {
        self.blocks.iter().flat_map(|block| block.iter_bytes(&self.old_lines))
    }

    /// Get a slice with the old lines of the given block. Panics if the block is not
    /// a change block or a ground block.
    pub(crate) fn old_lines_slice(&self, block_index: usize) -> LinesSlice<'_> {
        self.blocks[block_index].old_lines_slice(&self.old_lines)
    }
}
