use std::ops::Range;

#[allow(unused_imports)]
use crate::future::IsSomeAnd;
use crate::highlight::HighlightSpec;
use crate::wchar::prelude::*;

/// An edit action that can be undone.
#[derive(Clone, Eq, PartialEq)]
pub struct Edit {
    /// When undoing the edit we use this to restore the previous cursor position.
    pub cursor_position_before_edit: usize,
    pub cursor_position_before_undo: Option<usize>,

    /// The span of text that is replaced by this edit.
    pub range: std::ops::Range<usize>,

    /// The strings that are removed and added by this edit, respectively.
    pub old: WString,
    pub replacement: WString,

    /// edit_t is only for contiguous changes, so to restore a group of arbitrary changes to the
    /// command line we need to have a group id as forcibly coalescing changes is not enough.
    group_id: Option<usize>,
}

impl Edit {
    pub fn new(range: std::ops::Range<usize>, replacement: WString) -> Self {
        Self {
            cursor_position_before_edit: 0,
            cursor_position_before_undo: None,
            range,
            old: WString::new(),
            replacement,
            group_id: None,
        }
    }
}

/// Modify a string and its syntax highlighting according to the given edit.
/// Currently exposed for testing only.
pub fn apply_edit(target: &mut WString, colors: &mut Vec<HighlightSpec>, edit: &Edit) {
    let range = &edit.range;
    target.replace_range(range.clone(), &edit.replacement);

    // Now do the same to highlighting.
    let last_color = edit
        .range
        .start
        .checked_sub(1)
        .map(|i| colors[i])
        .unwrap_or_default();
    colors.splice(
        range.clone(),
        std::iter::repeat(last_color).take(edit.replacement.len()),
    );
}

/// The history of all edits to some command line.
#[derive(Clone, Default)]
pub struct UndoHistory {
    /// The stack of edits that can be undone or redone atomically.
    pub edits: Vec<Edit>,

    /// The position in the undo stack that corresponds to the current
    /// state of the input line.
    /// Invariants:
    ///     edits_applied - 1 is the index of the next edit to undo.
    ///     edits_applied     is the index of the next edit to redo.
    ///
    /// For example, if nothing was undone, edits_applied is edits.size().
    /// If every single edit was undone, edits_applied is 0.
    pub edits_applied: usize,

    /// Whether we allow the next edit to be grouped together with the
    /// last one.
    may_coalesce: bool,

    /// Whether to be more aggressive in coalescing edits. Ideally, it would be "force coalesce"
    /// with guaranteed atomicity but as `edit_t` is strictly for contiguous changes, that guarantee
    /// can't be made at this time.
    try_coalesce: bool,
}

impl UndoHistory {
    /// Empty the history.
    pub fn clear(&mut self) {
        self.edits.clear();
        self.edits_applied = 0;
        self.may_coalesce = false;
    }
}

/// Helper class for storing a command line.
#[derive(Clone, Default)]
pub struct EditableLine {
    /// The command line.
    text: WString,
    /// Syntax highlighting.
    colors: Vec<HighlightSpec>,
    /// The current position of the cursor in the command line.
    position: usize,

    /// The history of all edits.
    undo_history: UndoHistory,
    /// The nesting level for atomic edits, so that recursive invocations of start_edit_group()
    /// are not ended by one end_edit_group() call.
    edit_group_level: Option<usize>,
    /// Monotonically increasing edit group, ignored when edit_group_level_ is -1. Allowed to wrap.
    edit_group_id: usize,
}

impl EditableLine {
    pub fn text(&self) -> &wstr {
        &self.text
    }

    pub fn colors(&self) -> &[HighlightSpec] {
        &self.colors
    }
    pub fn set_colors(&mut self, colors: Vec<HighlightSpec>) {
        assert_eq!(colors.len(), self.len());
        self.colors = colors;
    }

    pub fn position(&self) -> usize {
        self.position
    }
    pub fn set_position(&mut self, position: usize) {
        assert!(position <= self.len());
        self.position = position;
    }

    // Gets the length of the text.
    pub fn len(&self) -> usize {
        self.text.len()
    }

    pub fn is_empty(&self) -> bool {
        self.text.is_empty()
    }

    pub fn at(&self, idx: usize) -> char {
        self.text.char_at(idx)
    }

    pub fn offset_to_line(&self, offset: usize) -> usize {
        self.text[0..offset].chars().filter(|&c| c == '\n').count()
    }

    /// Modify the commandline according to @edit. Most modifications to the
    /// text should pass through this function.
    pub fn push_edit(&mut self, mut edit: Edit, allow_coalesce: bool) {
        let range = &edit.range;
        let is_insertion = range.is_empty();
        // Coalescing insertion does not create a new undo entry but adds to the last insertion.
        if allow_coalesce && is_insertion && self.want_to_coalesce_insertion_of(&edit.replacement) {
            assert!(range.start == self.position());
            let last_edit = self.undo_history.edits.last_mut().unwrap();
            last_edit.replacement.push_utfstr(&edit.replacement);
            apply_edit(&mut self.text, &mut self.colors, &edit);
            self.set_position(self.position() + edit.replacement.len());

            assert!(self.undo_history.may_coalesce);
            return;
        }

        if range.is_empty() && edit.replacement.is_empty() {
            return; // nop
        }

        // Assign a new group id or propagate the old one if we're in a logical grouping of edits
        if self.edit_group_level.is_some() {
            edit.group_id = Some(self.edit_group_id);
        }

        if self.undo_history.edits_applied != self.undo_history.edits.len() {
            // After undoing some edits, the user is making a new edit;
            // we are about to create a new edit branch.
            // Discard all edits that were undone because we only support
            // linear undo/redo, they will be unreachable.
            self.undo_history
                .edits
                .truncate(self.undo_history.edits_applied);
        }
        edit.cursor_position_before_edit = self.position();
        edit.old = self.text[range.clone()].to_owned();
        apply_edit(&mut self.text, &mut self.colors, &edit);
        self.set_position(cursor_position_after_edit(&edit));
        assert_eq!(
            self.undo_history.edits_applied,
            self.undo_history.edits.len()
        );
        self.undo_history.may_coalesce =
            is_insertion && (self.undo_history.try_coalesce || edit.replacement.len() == 1);
        self.undo_history.edits_applied += 1;
        self.undo_history.edits.push(edit);
    }

    /// Undo the most recent edit that was not yet undone. Returns true on success.
    pub fn undo(&mut self) -> bool {
        let mut did_undo = false;
        let mut last_group_id = None;
        let position_before_undo = self.position();
        let end = self.undo_history.edits_applied;
        while self.undo_history.edits_applied != 0 {
            let edit = &self.undo_history.edits[self.undo_history.edits_applied - 1];
            if did_undo
                && edit
                    .group_id
                    .is_none_or(|group_id| Some(group_id) != last_group_id)
            {
                // We've restored all the edits in this logical undo group
                break;
            }
            last_group_id = edit.group_id;
            self.undo_history.edits_applied -= 1;
            let range = &edit.range;
            let mut inverse = Edit::new(
                range.start..range.start + edit.replacement.len(),
                L!("").to_owned(),
            );
            inverse.replacement = edit.old.clone();
            let old_position = edit.cursor_position_before_edit;
            apply_edit(&mut self.text, &mut self.colors, &inverse);
            self.set_position(old_position);
            did_undo = true;
        }
        if did_undo {
            let edit = &mut self.undo_history.edits[end - 1];
            edit.cursor_position_before_undo = Some(position_before_undo);
        }

        self.end_edit_group();
        self.undo_history.may_coalesce = false;
        did_undo
    }

    /// Redo the most recent undo. Returns true on success.
    pub fn redo(&mut self) -> bool {
        let mut did_redo = false;

        let mut last_group_id = None;
        while let Some(edit) = self.undo_history.edits.get(self.undo_history.edits_applied) {
            if did_redo
                && edit
                    .group_id
                    .is_none_or(|group_id| Some(group_id) != last_group_id)
            {
                // We've restored all the edits in this logical undo group
                break;
            }
            last_group_id = edit.group_id;
            self.undo_history.edits_applied += 1;
            apply_edit(&mut self.text, &mut self.colors, edit);
            edit.cursor_position_before_undo
                .map(|pos| self.set_position(pos));
            did_redo = true;
        }

        self.end_edit_group();
        did_redo
    }

    /// Start a logical grouping of command line edits that should be undone/redone together.
    pub fn begin_edit_group(&mut self) {
        if self.edit_group_level.is_some() {
            return;
        }
        self.edit_group_level = Some(1);
        // Indicate that the next change must trigger the creation of a new history item
        self.undo_history.may_coalesce = false;
        // Indicate that future changes should be coalesced into the same edit if possible.
        self.undo_history.try_coalesce = true;
        // Assign a logical edit group id to future edits in this group
        self.edit_group_id += 1;
    }

    /// End a logical grouping of command line edits that should be undone/redone together.
    pub fn end_edit_group(&mut self) {
        match self.edit_group_level.as_mut() {
            Some(edit_group_level) => {
                *edit_group_level -= 1;
                if *edit_group_level > 0 {
                    return;
                }
            }
            None => {
                // Prevent unbalanced end_edit_group() calls from breaking everything.
                return;
            }
        }

        self.edit_group_level = None;
        self.undo_history.try_coalesce = false;
        self.undo_history.may_coalesce = false;
    }

    /// Whether we want to append this string to the previous edit.
    fn want_to_coalesce_insertion_of(&self, s: &wstr) -> bool {
        // The previous edit must support coalescing.
        if !self.undo_history.may_coalesce {
            return false;
        }
        // Only consolidate single character inserts.
        if s.len() != 1 {
            return false;
        }
        // Make an undo group after every space.
        if s.as_char_slice()[0] == ' ' && !self.undo_history.try_coalesce {
            return false;
        }
        let last_edit = self.undo_history.edits.last().unwrap();
        // Don't add to the last edit if it deleted something.
        if !last_edit.range.is_empty() {
            return false;
        }
        // Must not have moved the cursor!
        if cursor_position_after_edit(last_edit) != self.position() {
            return false;
        }
        true
    }
}

/// Returns the number of characters left of the cursor that are removed by the
/// deletion in the given edit.
fn chars_deleted_left_of_cursor(edit: &Edit) -> usize {
    if edit.cursor_position_before_edit > edit.range.start {
        return std::cmp::min(
            edit.range.len(),
            edit.cursor_position_before_edit - edit.range.start,
        );
    }
    0
}

/// Compute the position of the cursor after the given edit.
fn cursor_position_after_edit(edit: &Edit) -> usize {
    let cursor = edit.cursor_position_before_edit + edit.replacement.len();
    let removed = chars_deleted_left_of_cursor(edit);
    cursor.saturating_sub(removed)
}

pub fn range_of_line_at_cursor(buffer: &wstr, cursor: usize) -> Range<usize> {
    let start = buffer[0..cursor]
        .as_char_slice()
        .iter()
        .rposition(|&c| c == '\n')
        .map(|newline| newline + 1)
        .unwrap_or(0);
    let mut end = buffer[cursor..]
        .as_char_slice()
        .iter()
        .position(|&c| c == '\n')
        .map(|pos| cursor + pos)
        .unwrap_or(buffer.len());
    // Remove any trailing newline
    if end != start && buffer.char_at(end - 1) == '\n' {
        end -= 1;
    }
    start..end
}

pub fn line_at_cursor(buffer: &wstr, cursor: usize) -> &wstr {
    &buffer[range_of_line_at_cursor(buffer, cursor)]
}
