// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

pub use crate::backend_proto::{
    deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto,
    DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
};
use crate::decks::{Deck, DeckID};
use crate::{
    card::{Card, CardID, CardQueue, CardType},
    collection::Collection,
    config::SchedulerVersion,
    err::Result,
    prelude::AnkiError,
    search::SortMode,
    timestamp::TimestampSecs,
    types::Usn,
};

impl Card {
    pub(crate) fn restore_queue_from_type(&mut self) {
        self.queue = match self.ctype {
            CardType::Learn | CardType::Relearn => {
                if self.due > 1_000_000_000 {
                    // unix timestamp
                    CardQueue::Learn
                } else {
                    // day number
                    CardQueue::DayLearn
                }
            }
            CardType::New => CardQueue::New,
            CardType::Review => CardQueue::Review,
        }
    }

    pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) {
        // filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero
        if self.original_due != 0 {
            println!("bug: odue was set");
            return;
        }

        self.original_deck_id = self.deck_id;
        self.deck_id = ctx.target_deck;

        self.original_due = self.due;

        if ctx.scheduler == SchedulerVersion::V1 {
            if self.ctype == CardType::Review && self.due <= ctx.today as i32 {
                // review cards that are due are left in the review queue
            } else {
                // new + non-due go into new queue
                self.queue = CardQueue::New;
            }
            if self.due != 0 {
                self.due = position;
            }
        } else {
            // if rescheduling is disabled, all cards go in the review queue
            if !ctx.config.reschedule {
                self.queue = CardQueue::Review;
            }
            // fixme: can we unify this with v1 scheduler in the future?
            // https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745
            if self.due > 0 {
                self.due = position;
            }
        }
    }

    /// Restores to the original deck and clears original_due.
    /// This does not update the queue or type, so should only be used as
    /// part of an operation that adjusts those separately.
    pub(crate) fn remove_from_filtered_deck_before_reschedule(&mut self) {
        if self.original_deck_id.0 != 0 {
            self.deck_id = self.original_deck_id;
            self.original_deck_id.0 = 0;
            self.original_due = 0;
        }
    }

    pub(crate) fn original_or_current_deck_id(&self) -> DeckID {
        if self.original_deck_id.0 > 0 {
            self.original_deck_id
        } else {
            self.deck_id
        }
    }

    pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
        if self.original_deck_id.0 == 0 {
            // not in a filtered deck
            return;
        }

        self.deck_id = self.original_deck_id;
        self.original_deck_id.0 = 0;

        match sched {
            SchedulerVersion::V1 => {
                self.due = self.original_due;
                self.queue = match self.ctype {
                    CardType::New => CardQueue::New,
                    CardType::Learn => CardQueue::New,
                    CardType::Review => CardQueue::Review,
                    // not applicable in v1, should not happen
                    CardType::Relearn => {
                        println!("did not expect relearn type in v1 for card {}", self.id);
                        CardQueue::New
                    }
                };
                if self.ctype == CardType::Learn {
                    self.ctype = CardType::New;
                }
            }
            SchedulerVersion::V2 => {
                // original_due is cleared if card answered in filtered deck
                if self.original_due > 0 {
                    self.due = self.original_due;
                }

                if (self.queue as i8) >= 0 {
                    self.restore_queue_from_type();
                }
            }
        }

        self.original_due = 0;
    }
}

impl Deck {
    pub fn new_filtered() -> Deck {
        let mut filt = FilteredDeck::default();
        filt.search_terms.push(FilteredSearchTerm {
            search: "".into(),
            limit: 100,
            order: 0,
        });
        filt.preview_delay = 10;
        filt.reschedule = true;
        Deck {
            id: DeckID(0),
            name: "".into(),
            mtime_secs: TimestampSecs(0),
            usn: Usn(0),
            common: DeckCommon {
                study_collapsed: true,
                browser_collapsed: true,
                ..Default::default()
            },
            kind: DeckKind::Filtered(filt),
        }
    }

    pub(crate) fn is_filtered(&self) -> bool {
        matches!(self.kind, DeckKind::Filtered(_))
    }
}

pub(crate) struct DeckFilterContext<'a> {
    pub target_deck: DeckID,
    pub config: &'a FilteredDeck,
    pub scheduler: SchedulerVersion,
    pub usn: Usn,
    pub today: u32,
}

impl Collection {
    pub fn empty_filtered_deck(&mut self, did: DeckID) -> Result<()> {
        self.transact_no_undo(|col| col.return_all_cards_in_filtered_deck(did))
    }
    pub(super) fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
        let cids = self.storage.all_cards_in_single_deck(did)?;
        self.return_cards_to_home_deck(&cids)
    }

    // Unlike the old Python code, this also marks the cards as modified.
    fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> {
        let sched = self.scheduler_version();
        let usn = self.usn()?;
        for cid in cids {
            if let Some(mut card) = self.storage.get_card(*cid)? {
                let original = card.clone();
                card.remove_from_filtered_deck_restoring_queue(sched);
                self.update_card_inner(&mut card, original, usn)?;
            }
        }
        Ok(())
    }

    // Unlike the old Python code, this also marks the cards as modified.
    pub fn rebuild_filtered_deck(&mut self, did: DeckID) -> Result<u32> {
        let deck = self.get_deck(did)?.ok_or(AnkiError::NotFound)?;
        let config = if let DeckKind::Filtered(kind) = &deck.kind {
            kind
        } else {
            return Err(AnkiError::invalid_input("not filtered"));
        };
        let ctx = DeckFilterContext {
            target_deck: did,
            config,
            scheduler: self.scheduler_version(),
            usn: self.usn()?,
            today: self.timing_today()?.days_elapsed,
        };

        self.transact_no_undo(|col| {
            col.return_all_cards_in_filtered_deck(did)?;
            col.build_filtered_deck(ctx)
        })
    }

    fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<u32> {
        let start = -100_000;
        let mut position = start;
        for term in &ctx.config.search_terms {
            position = self.move_cards_matching_term(&ctx, term, position)?;
        }

        Ok((position - start) as u32)
    }

    /// Move matching cards into filtered deck.
    /// Returns the new starting position.
    fn move_cards_matching_term(
        &mut self,
        ctx: &DeckFilterContext,
        term: &FilteredSearchTerm,
        mut position: i32,
    ) -> Result<i32> {
        let search = format!(
            "{} -is:suspended -is:buried -deck:filtered {}",
            if term.search.trim().is_empty() {
                "".to_string()
            } else {
                format!("({})", term.search)
            },
            if ctx.scheduler == SchedulerVersion::V1 {
                "-is:learn"
            } else {
                ""
            }
        );
        let order = order_and_limit_for_search(term, ctx.today);

        self.search_cards_into_table(&search, SortMode::Custom(order))?;
        for mut card in self.storage.all_searched_cards_in_search_order()? {
            let original = card.clone();
            card.move_into_filtered_deck(ctx, position);
            self.update_card_inner(&mut card, original, ctx.usn)?;
            position += 1;
        }

        Ok(position)
    }
}

fn order_and_limit_for_search(term: &FilteredSearchTerm, today: u32) -> String {
    let temp_string;
    let order = match term.order() {
        FilteredSearchOrder::OldestFirst => "(select max(id) from revlog where cid=c.id)",
        FilteredSearchOrder::Random => "random()",
        FilteredSearchOrder::IntervalsAscending => "ivl",
        FilteredSearchOrder::IntervalsDescending => "ivl desc",
        FilteredSearchOrder::Lapses => "lapses desc",
        FilteredSearchOrder::Added => "n.id",
        FilteredSearchOrder::ReverseAdded => "n.id desc",
        FilteredSearchOrder::Due => "c.due, c.ord",
        FilteredSearchOrder::DuePriority => {
            temp_string = format!(
                "
(case when queue={rev_queue} and due <= {today}
then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)",
                rev_queue = CardQueue::Review as i8,
                today = today
            );
            &temp_string
        }
    };

    format!("{} limit {}", order, term.limit)
}
