use crate::{
    channels::{entry::Entry, prototypes::Template},
    matcher::{injector::Injector, matched_item::MatchedItem},
};
use fast_strip_ansi::strip_ansi_string;

/// Implementors of this trait define two things:
/// - how to push lines into the matcher, including any preprocessing steps (e.g. stripping ANSI
///   codes, applying templates, etc.)
/// - how to construct the final Entry objects used by the rest of the application from the matched
///   items
///
/// The associated type `Data` defines the type of data stored in the matcher for each line.
pub trait EntryProcessor: Send + Sync + Clone + 'static {
    type Data: Send + Sync + Clone + 'static;

    fn push_to_injector(&self, line: String, injector: &Injector<Self::Data>);

    fn make_entry(
        &self,
        item: MatchedItem<Self::Data>,
        source_output: Option<&Template>,
    ) -> Entry;

    fn has_ansi(&self) -> bool;
}

/// A processor that does no special processing: matches the raw lines as-is and stores
/// them directly into the first matcher column.
///
/// Uses `Matcher<()>` since no extra data is needed which reduces memory usage.
#[derive(Clone, Debug)]
pub struct PlainProcessor;

impl EntryProcessor for PlainProcessor {
    type Data = ();

    fn push_to_injector(&self, line: String, injector: &Injector<()>) {
        injector.push((), |(), cols| {
            cols[0] = line.into();
        });
    }

    fn make_entry(
        &self,
        item: MatchedItem<()>,
        source_output: Option<&Template>,
    ) -> Entry {
        let mut entry = Entry::new(item.matched_string.clone())
            .with_match_indices(&item.match_indices);
        if let Some(output) = source_output {
            entry = entry.with_output(output.clone());
        }
        entry
    }

    fn has_ansi(&self) -> bool {
        false
    }
}

/// A processor that preserves ANSI codes in the matched lines by storing two versions of each
/// line in the matcher:
///
/// - the original line with ANSI codes
/// - a stripped version without ANSI codes for matching (matcher column 0)
///
/// Uses `Matcher<String>` to store original with ANSI codes.
#[derive(Clone, Debug)]
pub struct AnsiProcessor;

impl EntryProcessor for AnsiProcessor {
    type Data = String;

    fn push_to_injector(&self, line: String, injector: &Injector<String>) {
        injector.push(line, |original, cols| {
            cols[0] = strip_ansi_string(original).into();
        });
    }

    fn make_entry(
        &self,
        item: MatchedItem<String>,
        source_output: Option<&Template>,
    ) -> Entry {
        let mut entry = Entry::new(item.inner)
            .with_display(item.matched_string)
            .with_match_indices(&item.match_indices)
            .ansi(true);
        if let Some(output) = source_output {
            entry = entry.with_output(output.clone());
        }
        entry
    }

    fn has_ansi(&self) -> bool {
        true
    }
}

/// A processor that applies a display template to each line before matching, also storing the
/// original line into the matcher for further uses (e.g. output templates).
///
/// Uses `Matcher<String>` to store original lines.
#[derive(Clone, Debug)]
pub struct DisplayProcessor {
    pub template: Template,
}

impl EntryProcessor for DisplayProcessor {
    type Data = String;

    fn push_to_injector(&self, line: String, injector: &Injector<String>) {
        let template = self.template.clone();
        injector.push(line, move |original, cols| {
            cols[0] = template.format(original)
                .unwrap_or_else(|_| {
                    panic!(
                        "Failed to format display expression '{}' with entry '{}'",
                        template.raw(),
                        original
                    )
                })
                .into();
        });
    }

    fn make_entry(
        &self,
        item: MatchedItem<String>,
        source_output: Option<&Template>,
    ) -> Entry {
        let mut entry = Entry::new(item.inner)
            .with_display(item.matched_string)
            .with_match_indices(&item.match_indices)
            .ansi(false);
        if let Some(output) = source_output {
            entry = entry.with_output(output.clone());
        }
        entry
    }

    fn has_ansi(&self) -> bool {
        false
    }
}
