use std::ops::RangeInclusive;

use camino::Utf8Path;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::MutTxn;
use libpijul::working_copy::WorkingCopyRead;
use libpijul::{ArcTxn, ChangeId, ChannelRef, Vertex};

use crate::file_system::changes::recorded::RecordedStateError;
use crate::file_system::changes::unrecorded::UnrecordedChangesError;
use crate::file_system::open_file::contents::FileContents;

pub mod recorded;
pub mod unrecorded;

// TODO: conflicts etc
#[derive(Clone, Debug)]
pub enum HunkDiff {
    TextChange {
        lines_added: Vec<String>,
        lines_removed: Vec<String>,
    },
}

#[derive(Clone, Debug)]
pub struct ActiveHunk {
    pub index: usize,
    pub diff: HunkDiff,
}

#[derive(Clone, Copy, Debug)]
pub enum HunkOffset {
    Replacement {
        old_line_count: usize,
        new_line_count: usize,
    },
    Addition {
        lines_added: usize,
    },
    Deletion {
        lines_removed: usize,
    },
}

#[derive(Clone, Debug)]
pub struct Span<T: std::fmt::Debug> {
    pub value: T,
    pub lines: RangeInclusive<usize>,
}

impl<T: Clone + std::fmt::Debug> Span<T> {
    pub fn next_span_start(&self) -> usize {
        self.lines.end() + 1
    }

    pub fn spans_within_lines(
        spans: &[Self],
        line_range: RangeInclusive<usize>,
    ) -> impl Iterator<Item = Self> {
        debug_assert!(
            line_range.start() <= line_range.end(),
            "Invalid range: {line_range:#?}"
        );
        debug_assert!(spans.is_sorted_by_key(|span| span.lines.start()));

        let lines_start = *line_range.start();
        let lines_end = *line_range.end();

        spans
            .iter()
            .skip_while(move |span| *span.lines.end() < lines_start)
            .take_while(move |span| *span.lines.start() <= lines_end)
            .filter_map(move |span| {
                let span_start = *span.lines.start().max(&lines_start);
                let span_end = *span.lines.end().min(&lines_end);

                if span_end >= span_start {
                    Some(Span {
                        value: span.value.clone(),
                        lines: span_start..=span_end,
                    })
                } else {
                    None
                }
            })
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CreditSource {
    Tracked { vertex: Vertex<ChangeId> },
    Untracked { hunk_index: usize },
}

#[derive(Debug, thiserror::Error)]
pub enum FileCreditsError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
    #[error("unable to get tracked file state: {0:?}")]
    TrackedState(#[from] RecordedStateError<C>),
    #[error("unable to get untracked changes: {0:?}")]
    UntrackedChanges(#[from] UnrecordedChangesError<C, W>),
}

#[derive(Clone, Debug)]
pub struct FileCredits {
    pub recorded_state: recorded::RecordedState,
    pub unrecorded_state: unrecorded::UnrecordedState,
    pub spans: Vec<Span<CreditSource>>,
}

impl FileCredits {
    pub fn new<C, W>(
        path: &Utf8Path,
        file_contents: &FileContents,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        change_store: &C,
        working_copy: &W,
    ) -> Result<Self, FileCreditsError<C::Error, W::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
        W: WorkingCopyRead + Clone + Send + Sync + 'static,
    {
        let recorded_state =
            recorded::RecordedState::new(path, transaction, channel, change_store)?;
        let unrecorded_state = unrecorded::UnrecordedState::new(
            path,
            transaction,
            channel,
            change_store,
            working_copy,
        )?;

        let mut tracked_file = Self {
            recorded_state,
            unrecorded_state,
            spans: Vec::new(),
        };
        tracked_file.spans = tracked_file.recompute_spans(file_contents);

        Ok(tracked_file)
    }

    pub fn recompute_spans(&self, file: &FileContents) -> Vec<Span<CreditSource>> {
        let mut merged_spans = Vec::new();
        let mut untracked_spans = self.unrecorded_state.spans.iter().enumerate().peekable();
        let mut current_offset: isize = 0;
        let mut incomplete_span_start: Option<usize> = Some(0);

        while let Some((hunk_index, untracked_span)) = untracked_spans.next() {
            if let Some(incomplete_span_start) = incomplete_span_start.take()
                && *untracked_span.lines.start() > incomplete_span_start
            {
                merged_spans.extend(self.recorded_state.spans_within_lines(
                    incomplete_span_start..=(untracked_span.lines.start().strict_sub(1)),
                    current_offset,
                ));
            }

            match untracked_span.value {
                HunkOffset::Replacement {
                    old_line_count,
                    new_line_count,
                } => {
                    merged_spans.push(Span {
                        value: CreditSource::Untracked { hunk_index },
                        lines: untracked_span.lines.clone(),
                    });

                    let replacement_line_offset =
                        (new_line_count as isize).strict_add_unsigned(old_line_count);
                    current_offset = current_offset.strict_add(replacement_line_offset);
                }
                HunkOffset::Addition { lines_added } => {
                    merged_spans.push(Span {
                        value: CreditSource::Untracked { hunk_index },
                        lines: untracked_span.lines.clone(),
                    });

                    current_offset = current_offset.strict_sub_unsigned(lines_added);
                }
                HunkOffset::Deletion { lines_removed } => {
                    // Deletions don't contribute any spans, only change the offset
                    // to skip the deleted lines.
                    current_offset = current_offset.strict_add_unsigned(lines_removed);

                    incomplete_span_start = Some(*untracked_span.lines.start());
                }
            }

            if let Some((_next_hunk_index, next_untracked_span)) = untracked_spans.peek()
                && *next_untracked_span.lines.start() > untracked_span.next_span_start()
            {
                merged_spans.extend(self.recorded_state.spans_within_lines(
                    untracked_span.next_span_start()..=(next_untracked_span.lines.start() - 1),
                    current_offset,
                ));
            }
        }

        let file_length = file.text.len_lines(ropey::LineType::LF_CR).strict_sub(1);

        if let Some(final_span) = merged_spans.last()
            && *final_span.lines.end() < file_length.strict_sub(1)
        {
            merged_spans.extend(self.recorded_state.spans_within_lines(
                (final_span.lines.end() + 1)..=file_length.strict_sub(1),
                current_offset,
            ));
        }

        if merged_spans.is_empty() {
            merged_spans.extend(
                self.recorded_state
                    .spans_within_lines(0..=file_length.strict_sub(1), 0),
            );
        }

        // If the final line ends in a newline, text editors will allow selecting the "next line"
        // For example: `Line 1\nLine 2\n` will be rendered as:
        // ```
        // Line 1
        // Line 2
        //
        // ```
        // So extend the span of line 2 (index 1) to include this empty line
        if let Some(final_span) = merged_spans.last_mut()
            && let Some(final_character) = file.text.chars().last()
            && final_character == '\n'
        {
            let start_line = *final_span.lines.start();
            let end_line = *final_span.lines.end();

            final_span.lines = start_line..=(end_line + 1);
        }

        // Check the spans were generated correctly
        if cfg!(debug_assertions) {
            let mut span_windows = merged_spans.windows(2);

            // Span ranges must be in order
            while let Some([first, second]) = span_windows.next() {
                assert_eq!(
                    first.lines.end() + 1,
                    *second.lines.start(),
                    "Invalid span ranges: {merged_spans:#?}"
                )
            }

            // Entire file is covered
            let last_span_line = merged_spans
                .last()
                .map(|span| *span.lines.end())
                .unwrap_or(0);
            let last_text_lines = file.text.len_lines(ropey::LineType::LF_CR) - 1;

            assert_eq!(
                last_span_line, last_text_lines,
                "Incorrect span end (got {last_span_line}, expected {last_text_lines}): {merged_spans:#?}"
            );
        }

        // Combine consecutive spans of the same type into a single span
        merged_spans =
            merged_spans
                .into_iter()
                .fold(Vec::new(), |mut previous_spans, current_span| {
                    if let Some(previous_span) = previous_spans.last_mut()
                        && previous_span.value == current_span.value
                    {
                        let previous_start = *previous_span.lines.start();
                        previous_span.lines = previous_start..=*current_span.lines.end();
                    } else {
                        previous_spans.push(current_span);
                    }

                    previous_spans
                });

        merged_spans
    }

    // TODO: check that range is within limits?
    pub fn spans_within_lines(
        &self,
        line_range: RangeInclusive<usize>,
    ) -> impl Iterator<Item = Span<CreditSource>> {
        Span::spans_within_lines(&self.spans, line_range)
    }
}