//! Cooklang parser
//!
//! Grammar:
//! ```txt
//! recipe     = Newline* (line line_end)* line? Eof
//! line       = metadata | section | step
//! line_end   = soft_break | Newline+
//! soft_break = Newline !Newline
//!
//! metadata   = MetadataStart meta_key Colon meta_val
//! meta_key   = (!(Colon | Newline) ANY)*
//! meta_value = (!Newline ANY)*
//!
//! section    = Eq+ (section_name Eq*)
//! sect_name  = (!Eq ANY)*
//!
//! step       = TextStep? (component | ANY)*
//!
//! component  = c_kind modifiers? c_body note?
//! c_kind     = At | Hash | Tilde
//! c_body     = c_close | c_long | Word
//! c_long     = c_l_name c_alias? c_close
//! c_l_name   = (!(Newline | OpenBrace | Or) ANY)*
//! c_alias    = Or c_l_name
//! c_close    = OpenBrace Whitespace? Quantity? Whitespace? CloseBrace
//!
//! modifiers  = modifier+
//! modifier   = (At (OpenParen Eq? Tilde? Int CloseParen)?) | And | Plus | Minus | Question
//!
//! note       = OpenParen (!CloseParen ANY)* CloseParen
//!
//! quantity   = sc_lock? num_val Whitespace !unit_sep unit
//!            | val (unit_sep unit)?
//! sc_lock    = Whitespace Eq Whitespace
//!
//! unit       = (!CloseBrace ANY)*
//!
//! unit_sep   = Whitespace Percent Whitespace
//!
//! val        = num_val | text_val
//! text_val   = (Word | Whitespace)*
//! num_val    = mixed_num | frac | range | num
//! mixed_num  = Int Whitespace frac
//! frac       = Int Whitespace Slash Whitespace Int
//! range      = num Whitespace Minus Whitespace Num
//! num        = Float | Int
//!
//!
//! ANY        = { Any token }
//! ```
//! This is more of a guideline, there may be edge cases that this grammar does
//! not cover but the pareser does.

mod block_parser;
mod frontmatter;
mod metadata;
mod model;
mod quantity;
mod section;
mod step;
mod text_block;
mod token_stream;

pub use model::*;
pub use quantity::ParsedQuantity;

use std::collections::VecDeque;

use crate::{
    error::SourceDiag,
    lexer::T,
    located::Located,
    parser::{
        metadata::metadata_entry, section::section, step::parse_step, text_block::parse_text_block,
    },
    span::Span,
    text::Text,
    Extensions,
};

pub(crate) use block_parser::BlockParser;
use token_stream::{Token, TokenStream};

/// Events generated by [`PullParser`]
#[derive(Debug, Clone, PartialEq)]
pub enum Event<'i> {
    /// A YAML frontmatter at the top of the document.
    ///
    /// This can only be emitted as the first event and only once
    YAMLFrontMatter(Text<'i>),

    /// Metadata entry (single line block)
    Metadata { key: Text<'i>, value: Text<'i> },
    /// Section (single line block)
    Section { name: Option<Text<'i>> },
    /// Start of an element that can contain others.
    ///
    /// If this is emitted, a later [`Event::End`] of the same kind is
    /// guaranteed to be emitted. No other [`Event::Start`] will be emitted
    /// before the matching end event because `Cooklang` does not support nested
    /// blocks.
    Start(BlockKind),
    /// End of an element that can contain others.
    End(BlockKind),

    /// Text item
    Text(Text<'i>),
    /// Ingredient item
    Ingredient(Located<Ingredient<'i>>),
    /// Cookware item
    Cookware(Located<Cookware<'i>>),
    /// Timer item
    Timer(Located<Timer<'i>>),

    /// Parser error
    ///
    /// When a parser fatal error is emitted, other events (before or after) may
    /// not be correct and shall not be trusted.
    Error(SourceDiag),
    /// Parser warning
    ///
    /// Errors that are only hits to the user, but the input is correct and the
    /// other events can be trusted.
    Warning(SourceDiag),
}

/// For [`Event::Start`] and [`Event::End`]
#[derive(Debug, Clone, PartialEq)]
pub enum BlockKind {
    /// A recipe step
    Step,
    /// A text paragraph
    ///
    /// Only `Event::Text` will be emitted inside.
    Text,
}

/// Cooklang pull parser
///
/// This parser is an iterator of [`Event`]. No analysis pass is performed yet,
/// so no references, units or nothing is checked, just the structure of the
/// input.
#[derive(Debug)]
pub struct PullParser<'i, T>
where
    T: Iterator<Item = Token>,
{
    input: &'i str,
    tokens: std::iter::Peekable<T>,
    block: Vec<Token>,
    queue: VecDeque<Event<'i>>,
    extensions: Extensions,
    old_style_metadata: bool,
}

impl<'i> PullParser<'i, TokenStream<'i>> {
    /// Creates a new parser
    pub fn new(input: &'i str, extensions: Extensions) -> Self {
        if let Some(fm) = frontmatter::parse_frontmatter(input) {
            let mut events = VecDeque::new();
            events.push_back(Event::YAMLFrontMatter(Text::from_str(
                fm.yaml_text,
                fm.yaml_offset,
            )));
            let mut tokens = TokenStream::new(fm.cooklang_text);
            tokens.offset(fm.cooklang_offset);
            Self {
                input,
                tokens: tokens.peekable(),
                block: Vec::new(),
                extensions,
                queue: events,
                old_style_metadata: false,
            }
        } else {
            let tokens = TokenStream::new(input);
            Self {
                input,
                tokens: tokens.peekable(),
                block: Vec::new(),
                extensions,
                queue: VecDeque::new(),
                old_style_metadata: true,
            }
        }
    }
}

impl<'i, T> PullParser<'i, T>
where
    T: Iterator<Item = Token>,
{
    /// Transforms the parser into another [`Event`] iterator that only
    /// generates [`Event::Metadata`] blocks.
    ///
    /// Warnings and errors may be generated too.
    ///
    /// This is not just filtering, the parsing process is different and
    /// optimized to ignore everything else.
    pub fn into_meta_iter(mut self) -> impl Iterator<Item = Event<'i>> {
        std::iter::from_fn(move || self.next_metadata())
    }
}

fn is_empty_token(tok: &Token) -> bool {
    matches!(
        tok.kind,
        T![ws] | T![block comment] | T![line comment] | T![newline]
    )
}

fn is_single_line_marker(first: Option<&Token>) -> bool {
    matches!(first, Some(mt![meta | =]))
}

struct LineInfo {
    is_empty: bool,
    is_single_line: bool,
}

impl<'i, T> PullParser<'i, T>
where
    T: Iterator<Item = Token>,
{
    fn pull_line(&mut self) -> Option<LineInfo> {
        let mut is_empty = true;
        let mut no_tokens = true;
        let is_single_line = is_single_line_marker(self.tokens.peek());
        for tok in self.tokens.by_ref() {
            self.block.push(tok);
            no_tokens = false;

            if !is_empty_token(&tok) {
                is_empty = false;
            }

            if tok.kind == T![newline] {
                break;
            }
        }
        if no_tokens {
            None
        } else {
            Some(LineInfo {
                is_empty,
                is_single_line,
            })
        }
    }

    /// Advances a block. Store the tokens, newline/eof excluded.
    pub(crate) fn next_block(&mut self) -> Option<()> {
        self.block.clear();

        // start and end are used to track the "non empty" part of the block
        let mut start = 0;
        let mut end;

        let mut current_line = self.pull_line()?;

        // Eat empty lines
        while current_line.is_empty {
            start = self.block.len();
            current_line = self.pull_line()?;
        }

        // Check if more lines have to be consumed
        let multiline = !current_line.is_single_line;
        end = self.block.len();
        if multiline {
            loop {
                if is_single_line_marker(self.tokens.peek()) {
                    break;
                }
                match self.pull_line() {
                    None => break,
                    Some(line) if line.is_empty => break,
                    _ => {}
                }
                end = self.block.len();
            }
        }

        // trim trailing newline
        while let mt![newline] = self.block[end - 1] {
            if end <= start {
                break;
            }
            end -= 1;
        }
        // trim empty lines
        let trimmed_block = &self.block[start..end];
        if trimmed_block.is_empty() {
            return None;
        }

        let mut bp = BlockParser::new(trimmed_block, self.input, &mut self.queue, self.extensions);
        parse_block(&mut bp, self.old_style_metadata);
        bp.finish();

        Some(())
    }

    fn next_metadata_block(&mut self) -> Option<()> {
        if !self.old_style_metadata {
            return None;
        }
        self.block.clear();

        // eat until meta is found
        let mut last = T![newline];
        loop {
            let curr = self.tokens.peek()?.kind;
            if last == T![newline] && curr == T![meta] {
                break;
            }
            self.tokens.next();
            last = curr;
        }

        // eat until newline or end
        for tok in self.tokens.by_ref() {
            if tok.kind == T![newline] {
                break;
            }
            self.block.push(tok);
        }

        let mut bp = BlockParser::new(&self.block, self.input, &mut self.queue, self.extensions);
        if let Some(ev) = metadata_entry(&mut bp) {
            bp.event(ev);
            bp.finish(); // only finish if a metadata is parsed, as other blocks are not consumed
        }

        Some(())
    }

    pub(crate) fn next_metadata(&mut self) -> Option<Event<'i>> {
        self.queue.pop_front().or_else(|| {
            self.next_metadata_block()?;
            self.next_metadata()
        })
    }
}

impl<'i, T> Iterator for PullParser<'i, T>
where
    T: Iterator<Item = Token>,
{
    type Item = Event<'i>;

    fn next(&mut self) -> Option<Self::Item> {
        self.queue.pop_front().or_else(|| {
            self.next_block()?;
            self.next()
        })
    }
}

fn parse_block(block: &mut BlockParser, old_style_metadata: bool) {
    let meta_or_section = match block.peek() {
        T![meta] => block.with_recover(|bp| {
            metadata_entry(bp).filter(|ev| {
                let Event::Metadata { key, .. } = ev else {
                    unreachable!()
                };
                let key_t = key.text_outer_trimmed();
                let is_config_key = key_t.starts_with('[') && key_t.ends_with(']');
                let modes_active = bp.extension(Extensions::MODES);
                (is_config_key && modes_active) || old_style_metadata
            })
        }),
        T![=] => block.with_recover(section),
        _ => None,
    };

    if let Some(ev) = meta_or_section {
        block.event(ev);
    } else {
        parse_multiline_block(block);
    }
}

fn parse_multiline_block(bp: &mut BlockParser) {
    // block splitter should make sure trailing newline never reaches
    debug_assert!(bp
        .tokens()
        .last()
        .map(|t| t.kind != T![newline])
        .unwrap_or(true));

    let is_empty = bp.tokens().iter().all(|t| {
        matches!(
            t.kind,
            T![ws] | T![line comment] | T![block comment] | T![newline]
        )
    });
    if is_empty {
        bp.consume_rest();
        return;
    }

    let is_text = bp.peek() == T![>];

    if is_text {
        parse_text_block(bp);
    } else {
        parse_step(bp);
    }
}

/// get the span for a slice of tokens. panics if the slice is empty
pub(crate) fn tokens_span(tokens: &[Token]) -> Span {
    debug_assert!(!tokens.is_empty(), "tokens_span tokens empty");
    let start = tokens.first().unwrap().span.start();
    let end = tokens.last().unwrap().span.end();
    Span::new(start, end)
}

// match token type
macro_rules! mt {
    ($($reprs:tt)|*) => {
        $($crate::parser::token_stream::Token {
            kind: T![$reprs],
            ..
        })|+
    }
}
pub(crate) use mt;

macro_rules! error {
    ($msg:expr, $label:expr $(,)?) => {
        $crate::error::SourceDiag::error($msg, $label, $crate::error::Stage::Parse)
    };
}
use error;

macro_rules! warning {
    ($msg:expr, $label:expr $(,)?) => {
        $crate::error::SourceDiag::warning($msg, $label, $crate::error::Stage::Parse)
    };
}
use warning;

#[cfg(test)]
mod tests {
    use indoc::indoc;

    use super::*;
    use crate::ast::*;

    #[test]
    fn just_metadata() {
        let parser = PullParser::new(
            indoc! {r#">> entry: true
        a test @step @salt{1%mg} more text
        a test @step @salt{1%mg} more text
        a test @step @salt{1%mg} more text
        >> entry2: uwu
        a test @step @salt{1%mg} more text
        "#},
            Extensions::empty(),
        );
        let events = parser.into_meta_iter().collect::<Vec<_>>();
        assert_eq!(
            events,
            vec![
                Event::Metadata {
                    key: Text::from_str(" entry", 2),
                    value: Text::from_str(" true", 10)
                },
                Event::Metadata {
                    key: Text::from_str(" entry2", 126),
                    value: Text::from_str(" uwu", 134)
                },
            ]
        );
    }

    #[test]
    fn multiline_spaces() {
        let parser = PullParser::new(
            "  This is a step           -- comment\n and this line continues  -- another comment",
            Extensions::empty(),
        );
        let (ast, report) = build_ast(parser).into_tuple();
        let (err, warn) = report.unzip();

        // Only whitespace between line should be trimmed
        assert!(warn.is_empty());
        assert!(err.is_empty());
        assert_eq!(
            ast.unwrap().blocks,
            vec![Block::Step {
                items: vec![Item::Text({
                    let mut t = Text::empty(0);
                    t.append_str("  This is a step           ", 0);
                    t.append_fragment(crate::text::TextFragment::soft_break("\n", 37));
                    t.append_str(" and this line continues  ", 39);
                    t
                })]
            }]
        );
    }
}
