mod model;
mod view;

pub mod prelude {
    pub use super::ArticlesList;
}

use crate::ui::articles_list::{model::ArticleListModelData, view::FilterState};
use log::{info, trace};
use news_flash::models::{Article, ArticleID, Marked, Read, Tag};
use view::ArticleListViewData;

use crate::prelude::*;
use std::{collections::HashSet, mem::take, sync::Arc};

use tokio::sync::mpsc::UnboundedSender;

pub struct ArticlesList {
    config: Arc<Config>,

    message_sender: UnboundedSender<Message>,

    view_data: ArticleListViewData<'static>,
    filter_state: FilterState,
    model_data: ArticleListModelData,

    is_focused: bool,
}

impl ArticlesList {
    pub fn new(
        config: Arc<Config>,
        news_flash_utils: Arc<NewsFlashUtils>,
        message_sender: UnboundedSender<Message>,
    ) -> Self {
        Self {
            config: config.clone(),
            message_sender,

            view_data: ArticleListViewData::default(),
            filter_state: FilterState::new(config.article_scope, config.default_sort_order.clone()),

            model_data: ArticleListModelData::new(news_flash_utils.clone()),

            is_focused: false,
        }
    }

    pub(super) fn restore_sensible_selection(
        &mut self,
        article_id: Option<&ArticleID>,
    ) -> color_eyre::Result<()> {
        // get current selection index
        let current_index = self.view_data.get_table_state().selected().unwrap_or(0);

        // save offset distance
        let offset = *self.view_data.get_table_state_mut().offset_mut();
        let offset_distance = self
            .view_data
            .get_table_state_mut()
            .selected()
            .unwrap_or(0)
            .saturating_sub(offset);

        // first, we try to select the article with article_id
        if let Some(article_id) = article_id
            && let Some(index) = self
                .model_data
                .articles()
                .iter()
                .position(|article| article.article_id == *article_id)
        {
            *self.view_data.get_table_state_mut().offset_mut() =
                index.saturating_sub(offset_distance);
            return self.select_index_and_send_message(Some(index));
        }

        // the previous article is not there, next we select the first unread article
        // try to select previous index
        if current_index < self.model_data.articles().len() {
            return self.select_index_and_send_message(Some(current_index));
        } else {
            self.select_index_and_send_message(Some(0))?;
        }

        Ok(())
    }

    pub(super) fn select_index_and_send_message(
        &mut self,
        index: Option<usize>,
    ) -> color_eyre::Result<()> {
        let index = index
            .or(self.view_data.get_table_state().selected())
            .unwrap_or_default();
        if let Some(article) = self.model_data.articles().get(index) {
            self.view_data.table_state_mut().select(Some(index));
            self.message_sender
                .send(Message::Event(Event::ArticleSelected(
                    article.article_id.to_owned(),
                )))?;
        }

        self.adjust_offset();
        Ok(())
    }

    pub(super) fn select_next_unread(&mut self) -> color_eyre::Result<()> {
        let select = self.first_unread();

        match select {
            Some(_) => self.select_index_and_send_message(select)?,
            None => {
                // ask feed list to select the next unread item
                trace!("no next unread item");
                self.message_sender.send(Message::Command(Command::In(
                    Panel::FeedList,
                    Box::new(Command::SelectNextUnread),
                )))?;
            }
        }

        Ok(())
    }

    fn first_unread(&self) -> Option<usize> {
        let current_index = self.view_data.table_state().selected().unwrap_or(0);

        self.model_data
            .articles()
            .iter()
            .enumerate()
            .find(|(index, article)| *index >= current_index && article.unread == Read::Unread)
            .map(|(index, _)| index)
    }

    pub(super) fn open_in_browser(&mut self, action_scope: &ActionScope) -> color_eyre::Result<()> {
        let articles = self.get_articles_by_action_scope(action_scope)?;

        for article in articles.iter() {
            if let Some(url) = article.url.as_ref()
                && let Err(error) = webbrowser::open(url.as_ref())
            {
                tooltip(
                    &self.message_sender,
                    format!("unable to open article in webbrowser: {}", error).as_str(),
                    TooltipFlavor::Error,
                )?;
                break;
            }
        }

        Ok(())
    }

    pub(super) fn get_current_article(&self) -> Option<Article> {
        if let Some(index) = self.view_data.get_table_state().selected() {
            return self.model_data.articles().get(index).cloned();
        }
        None
    }

    pub(super) fn search(
        &self,
        articles: &[Article],
        article_query: &ArticleQuery,
        reversed: bool,
    ) -> Option<usize> {
        let predicate = |article: &Article| {
            article_query.test(
                article,
                &ArticleQueryContext {
                    feed_map: self.model_data.feed_map(),
                    category_for_feed: self.model_data.category_for_feed(),
                    tags_for_article: self.model_data.tags_for_article(),
                    tag_map: self.model_data.tag_map(),
                    last_sync: self.model_data.last_sync(),
                    flagged: self.model_data.flagged_articles(),
                },
            )
        };

        if !reversed {
            articles.iter().position(predicate)
        } else {
            articles.iter().rposition(predicate)
        }
    }

    fn adjust_offset(&mut self) {
        let Some(index) = self.view_data.get_table_state_mut().selected() else {
            return;
        };

        let scrollbar_state = self.view_data.scrollbar_state_mut();
        *scrollbar_state = scrollbar_state.position(index);

        let Some(lines) = *self.view_data.article_lines() else {
            return;
        };
        let offset = self.view_data.get_table_state_mut().offset_mut();
        let max_lines_above =
            (lines as usize).saturating_sub(self.config.articles_after_selection + 1);

        if index.saturating_sub(*offset) > max_lines_above {
            *offset = index.saturating_sub(max_lines_above);
        }
    }

    pub(super) fn get_articles_by_action_scope(
        &mut self,
        action_scope: &ActionScope,
    ) -> color_eyre::Result<Vec<Article>> {
        use ActionScope as S;
        Ok(match action_scope {
            S::All => self.model_data.articles().clone(),
            S::Current => {
                if self.model_data.effectively_flagged_articles().is_empty() {
                    self.get_current_article().iter().cloned().collect()
                } else {
                    self.model_data
                        .articles()
                        .iter()
                        .filter(|article| {
                            self.model_data
                                .effectively_flagged_articles()
                                .contains(&article.article_id)
                        })
                        .cloned()
                        .collect()
                    // self.model_data.flagged_articles_mut().clear(); // don't so that other
                    // operations also can work on flagged articles --- user should clear flags
                    // manually
                }
            }
            direction @ (S::Above | S::Below) => {
                if let Some(index) = self.view_data.table_state().selected() {
                    let offset = if matches!(direction, S::Above) { 1 } else { 0 };
                    let (above, below) = self.model_data.articles().split_at(index + offset);
                    (if matches!(direction, S::Above) {
                        above
                    } else {
                        below
                    })
                    .to_vec()
                } else {
                    Default::default()
                }
            }
            S::Query(query) => self.model_data.get_queried_articles(query).to_vec(),
        })
    }

    pub(super) fn get_article_ids_by_action_scope(
        &mut self,
        action_scope: &ActionScope,
    ) -> color_eyre::Result<Vec<ArticleID>> {
        Ok(self
            .get_articles_by_action_scope(action_scope)?
            .into_iter()
            .map(|article| article.article_id)
            .collect())
    }

    pub(super) fn set_action_scope_read_status(
        &mut self,
        action_scope: &ActionScope,
        read: Read,
    ) -> color_eyre::Result<usize> {
        let articles = self.get_article_ids_by_action_scope(action_scope)?;
        let amount = self.model_data.set_read_status(articles, read)?;

        tooltip(
            &self.message_sender,
            format!(
                "set status of {} articles to {}",
                amount,
                match read {
                    Read::Read => "read",
                    Read::Unread => "read",
                }
            )
            .as_str(),
            TooltipFlavor::Info,
        )?;

        Ok(amount)
    }

    pub(super) fn set_action_scope_marked_status(
        &mut self,
        action_scope: &ActionScope,
        marked: Marked,
    ) -> color_eyre::Result<usize> {
        let articles = self.get_article_ids_by_action_scope(action_scope)?;
        let amount = self.model_data.set_marked_status(articles, marked)?;

        tooltip(
            &self.message_sender,
            format!(
                "set status of {} articles to {}",
                amount,
                match marked {
                    Marked::Marked => "marked",
                    Marked::Unmarked => "unmarked",
                }
            )
            .as_str(),
            TooltipFlavor::Info,
        )?;

        Ok(amount)
    }

    pub(super) fn change_tag(
        &mut self,
        action_scope: &ActionScope,
        tag: Tag,
        add: bool,
    ) -> color_eyre::Result<()> {
        let article_ids = self.get_article_ids_by_action_scope(action_scope)?;
        if add {
            let amount = self
                .model_data
                .tag_articles(article_ids, tag.tag_id.clone())?;
            tooltip(
                &self.message_sender,
                format!("adding tag {} to {} articles", tag.label, amount).as_str(),
                TooltipFlavor::Info,
            )?;
            amount
        } else {
            let amount = self
                .model_data
                .untag_articles(article_ids, tag.tag_id.clone())?;
            tooltip(
                &self.message_sender,
                format!("removing tag {} from {} articles", tag.label, amount).as_str(),
                TooltipFlavor::Info,
            )?;
            amount
        };

        Ok(())
    }

    pub(super) fn search_next(
        &mut self,
        skip_current: bool,
        reversed: bool,
    ) -> color_eyre::Result<()> {
        let offset = if skip_current { 1 } else { 0 };
        let Some(article_query) = self.filter_state.article_search_query().as_ref() else {
            return tooltip(
                &self.message_sender,
                "no search query",
                TooltipFlavor::Warning,
            );
        };

        if let Some(selected) = self.view_data.get_table_state_mut().selected() {
            let split_index = if !reversed {
                selected + offset
            } else {
                selected.saturating_sub(offset)
            };

            let slices = self.model_data.articles().split_at(split_index);

            let (first_range, second_range) = if reversed {
                slices
            } else {
                (slices.1, slices.0)
            };

            let (first_offset, second_offset) = if !reversed {
                (split_index, 0)
            } else {
                (0, split_index)
            };

            match self.search(first_range, article_query, reversed) {
                Some(index) => {
                    return self.select_index_and_send_message(Some(index + first_offset));
                }
                None => match self.search(second_range, article_query, reversed) {
                    Some(index) => {
                        tooltip(
                            &self.message_sender,
                            if !reversed {
                                "end reached, starting from beginning"
                            } else {
                                "beginning reached, starting from end"
                            },
                            TooltipFlavor::Warning,
                        )?;
                        return self.select_index_and_send_message(Some(index + second_offset));
                    }
                    None => {
                        tooltip(
                            &self.message_sender,
                            "no match found",
                            TooltipFlavor::Warning,
                        )?;
                    }
                },
            }
        }
        {}

        Ok(())
    }

    fn on_tag_or_untag(
        &mut self,
        action_scope: ActionScope,
        tag_name: String,
        tag_articles: bool,
    ) -> Result<(), color_eyre::eyre::Error> {
        match self
            .model_data
            .tag_map()
            .values()
            .find(|&tag| tag.label == tag_name)
        {
            Some(tag) => {
                self.change_tag(&action_scope, tag.clone(), tag_articles)?;
            }
            None => {
                log::warn!("could not find tag with name {}", tag_name);
                tooltip(
                    &self.message_sender,
                    format!("unknown tag: {}", tag_name).as_str(),
                    TooltipFlavor::Error,
                )?;
            }
        }
        Ok(())
    }

    fn set_action_scope_flagged(
        &mut self,
        action_scope: &ActionScope,
        flag: bool,
    ) -> color_eyre::Result<()> {
        let articles = match action_scope {
            ActionScope::Current => self
                .get_current_article()
                .map(|article| article.article_id)
                .iter()
                .cloned()
                .collect(),
            action_scope => self.get_article_ids_by_action_scope(action_scope)?,
        };

        let flagged_articles = self.model_data.flagged_articles_mut();

        if flag {
            flagged_articles.extend(articles);
        } else {
            articles.iter().for_each(|article_id| {
                flagged_articles.remove(article_id);
            });
        }

        Ok(())
    }

    fn invert_flagged(&mut self, action_scope: &ActionScope) -> color_eyre::Result<()> {
        let to_invert: HashSet<ArticleID> = HashSet::from_iter(match action_scope {
            ActionScope::Current => self
                .get_current_article()
                .map(|article| article.article_id)
                .iter()
                .cloned()
                .collect(),
            action_scope => self.get_article_ids_by_action_scope(action_scope)?,
        });

        let flagged: HashSet<ArticleID> = take(self.model_data.flagged_articles_mut());

        let to_flag: HashSet<&ArticleID> = to_invert.difference(&flagged).collect();
        let to_unflag: HashSet<&ArticleID> = to_invert.intersection(&flagged).collect();

        *self.model_data.flagged_articles_mut() = flagged
            .iter()
            .collect::<HashSet<&ArticleID>>()
            .difference(&to_unflag)
            .cloned()
            .collect::<HashSet<&ArticleID>>()
            .union(&to_flag)
            .cloned()
            .cloned()
            .collect::<HashSet<ArticleID>>();

        // *self.model_data.flagged_articles_mut() = HashSet::from_iter(
        //     self.model_data
        //         .articles()
        //         .iter()
        //         .map(|article| &article.article_id)
        //         .cloned(),
        // )
        // .difference(&flagged)
        // .cloned()
        // .collect();

        Ok(())
    }
}

impl crate::messages::MessageReceiver for ArticlesList {
    async fn process_command(&mut self, message: &Message) -> color_eyre::Result<()> {
        if matches!(message, Message::Event(Event::Tick)) {
            return Ok(());
        }

        let mut current_article = self.get_current_article().map(|article| article.article_id);
        let mut model_needs_update = false;
        let mut view_needs_update = false;

        if let Message::Command(command) = message {
            use Command as C;
            let mut handle_command = false;

            let Some(command) = (match command {
                C::In(Panel::ArticleList, command) => {
                    handle_command = true;
                    Some(*command.to_owned())
                }
                C::In(..) => None,
                command => {
                    handle_command = self.is_focused;
                    Some(command.to_owned())
                }
            }) else {
                return Ok(());
            };

            match command {
                C::NavigateUp if handle_command => {
                    self.view_data.get_table_state_mut().select_previous();
                    self.select_index_and_send_message(None)?;
                }
                C::NavigateDown if handle_command => {
                    self.view_data.get_table_state_mut().select_next();
                    self.select_index_and_send_message(None)?;
                }
                C::NavigatePageUp if handle_command => {
                    let lines = *self.view_data.article_lines();
                    if let Some(lines) = lines {
                        self.view_data
                            .get_table_state_mut()
                            .scroll_up_by(lines.saturating_sub(1));
                        self.select_index_and_send_message(None)?;
                    };
                }
                C::NavigatePageDown if handle_command => {
                    let lines = *self.view_data.article_lines();
                    if let Some(lines) = lines {
                        self.view_data.get_table_state_mut().scroll_down_by(
                            lines.saturating_sub(self.config.articles_after_selection as u16),
                        );
                        self.select_index_and_send_message(None)?;
                    }
                }
                C::NavigateFirst if handle_command => {
                    self.view_data.get_table_state_mut().select_first();
                    self.select_index_and_send_message(None)?;
                }
                C::NavigateLast if handle_command => {
                    self.view_data.get_table_state_mut().select_last();
                    // manually "select" as select_last does not know the number of rows
                    self.select_index_and_send_message(Some(self.model_data.articles().len() - 1))?;
                }

                C::Show(scope) if handle_command => {
                    *self.filter_state.article_scope_mut() = scope;
                    model_needs_update = true;
                }

                // actions
                C::ActionOpenInBrowser(action_scope) => {
                    self.open_in_browser(&action_scope)?;
                }

                C::ActionSetRead(action_scope) if handle_command => {
                    // first we check if we can reroute this to feed list to make it quicker
                    use ActionScope::*;
                    match action_scope {
                        // queries must be handled by article list
                        Query(_) => {
                            self.set_action_scope_read_status(&action_scope, Read::Read)?;
                            view_needs_update = true;
                        }

                        // if all articles must be set to read and no adhoc filter is applied, the
                        // feed list can set the articles to read
                        All if !self.filter_state.apply_article_adhoc_filter() => {
                            info!("re-routing set read command to feed list");
                            self.message_sender.send(Message::Command(C::In(
                                Panel::FeedList,
                                Box::new(C::ActionSetRead(ActionScope::Current)),
                            )))?;
                        }

                        _ => {
                            self.set_action_scope_read_status(&action_scope, Read::Read)?;
                            view_needs_update = true;
                        }
                    }
                }

                C::ActionSetUnread(action_scope) => {
                    self.set_action_scope_read_status(&action_scope, Read::Unread)?;
                    view_needs_update = true;
                }

                C::ActionSetMarked(action_scope) => {
                    self.set_action_scope_marked_status(&action_scope, Marked::Marked)?;
                    view_needs_update = true;
                }

                C::ActionSetFlagged(action_scope) => {
                    self.set_action_scope_flagged(&action_scope, true)?;
                    view_needs_update = true;
                }

                C::ActionSetUnflagged(action_scope) => {
                    self.set_action_scope_flagged(&action_scope, false)?;
                    view_needs_update = true;
                }

                C::ActionFlagInvert(action_scope) => {
                    self.invert_flagged(&action_scope)?;
                    view_needs_update = true;
                }

                C::ActionSetUnmarked(action_scope) => {
                    self.set_action_scope_marked_status(&action_scope, Marked::Unmarked)?;
                    view_needs_update = true;
                }

                C::ActionTagArticles(action_scope, tag_name) => {
                    self.on_tag_or_untag(action_scope, tag_name, true)?;
                    view_needs_update = true;
                }

                C::ActionUntagArticles(action_scope, tag_name) => {
                    self.on_tag_or_untag(action_scope, tag_name, false)?;
                    view_needs_update = true;
                }

                C::InputSearch if handle_command => {
                    self.message_sender
                        .send(Message::Command(C::CommandLineOpen(Some(
                            "searcharticles".to_owned(),
                        ))))?;
                }

                C::SelectNextUnread if handle_command => {
                    self.select_next_unread()?;
                }

                C::ArticleListSearch(query) => {
                    *self.filter_state.article_search_query_mut() = Some(query.clone());
                    self.view_data.update(
                        self.config.clone(),
                        &self.model_data,
                        &self.filter_state,
                        handle_command,
                    ); // manual here for highlighting only
                    self.search_next(false, false)?;
                }

                C::SearchNext if handle_command => {
                    self.search_next(true, false)?;
                }

                C::SearchPrevious if handle_command => {
                    self.search_next(true, true)?;
                }

                C::ArticleListFilterSet(article_adhoc_filter) => {
                    self.filter_state
                        .on_new_article_adhoc_filter(article_adhoc_filter.clone(), false);
                    model_needs_update = true;
                }

                C::ArticleListFilterSetSticky(article_adhoc_filter) => {
                    self.filter_state
                        .on_new_article_adhoc_filter(article_adhoc_filter.clone(), true);
                    model_needs_update = true;
                }

                C::ArticleListFilterApply => {
                    *self.filter_state.apply_article_adhoc_filter_mut() = true;
                    model_needs_update = true;
                }

                C::ArticleListFilterClear => {
                    *self.filter_state.apply_article_adhoc_filter_mut() = false;
                    *self.filter_state.sticky_adhoc_filter_mut() = false;
                    model_needs_update = true;
                }

                C::ArticleListSort(sort_order) => {
                    *self.filter_state.adhoc_sort_order_mut() = Some(sort_order.to_owned());
                    model_needs_update = true;
                }

                C::ArticleListSortReverse => {
                    *self.filter_state.reverse_sort_order_mut() =
                        !*self.filter_state.reverse_sort_order_mut();
                    model_needs_update = true;
                }

                C::ArticleListSortClear => {
                    self.filter_state.clear_sort_order();
                    model_needs_update = true;
                }

                C::ArticleListQuery(query) => {
                    self.filter_state.on_new_article_filter(query.into());
                    model_needs_update = true;
                }

                C::Refresh => {
                    model_needs_update = true;
                }

                _ => {}
            }
        }

        if let Message::Event(event) = message {
            use Event::*;
            match event {
                ArticlesSelected(augmented_article_filter) => {
                    self.filter_state
                        .on_new_article_filter(augmented_article_filter.clone());
                    current_article = None;
                    self.select_index_and_send_message(Some(0))?;
                    model_needs_update = true;
                }

                ApplicationStateChanged(state) => {
                    self.is_focused = *state == AppState::ArticleSelection;
                    view_needs_update = true;
                }

                MouseArticleClick(row_offset) => {
                    // Select the article at the clicked row offset
                    let offset = self.view_data.get_table_state_mut().offset();
                    let target_index = offset + *row_offset as usize;
                    if target_index < self.model_data.articles().len() {
                        self.view_data
                            .get_table_state_mut()
                            .select(Some(target_index));
                        self.select_index_and_send_message(None)?;
                    }
                }

                MouseScrollDown(Panel::ArticleList) => {
                    let offset = self.view_data.get_table_state_mut().offset_mut();
                    let max = self.model_data.articles().len().saturating_sub(1);
                    *offset = (*offset).saturating_add(1).min(max);
                }

                MouseScrollUp(Panel::ArticleList) => {
                    let offset = self.view_data.get_table_state_mut().offset_mut();
                    *offset = (*offset).saturating_sub(1);
                }

                event if event.caused_model_update() => model_needs_update = true,

                _ => {}
            }
        }

        // update state where needed
        if model_needs_update {
            self.model_data.update(&self.filter_state).await?;
        }

        if model_needs_update || view_needs_update {
            self.view_data.update(
                self.config.clone(),
                &self.model_data,
                &self.filter_state,
                self.is_focused,
            );
            self.restore_sensible_selection(current_article.as_ref())?;
            self.message_sender
                .send(Message::Command(Command::Redraw))?;
        }

        Ok(())
    }
}
