use std::sync::OnceLock;

use texform_core::ast::Ast;
use texform_core::parse::{
    AllowedMode, CommandItem, CommandKind, ContextItem, DelimiterControlItem, EnvironmentItem,
    ParseConfig, ParseContext, ParseContextBuilder,
};
use texform_interface::syntax_node::{Argument, ArgumentValue, ContentMode, SyntaxNode};
use texform_knowledge::specs::load_package_specs_from_str;

pub(crate) fn serialize_node(node: &SyntaxNode) -> String {
    let ast = Ast::from_syntax_root(node);
    texform_core::serialize::serialize(&ast)
}

pub(crate) fn parse(
    src: &str,
    reject_unknown: bool,
) -> Result<(SyntaxNode, chumsky::span::SimpleSpan), Vec<String>> {
    let config = if reject_unknown {
        ParseConfig::STRICT
    } else {
        ParseConfig::LENIENT
    };
    let output = test_context().parse(src, &config);
    if output.diagnostics.is_empty() {
        let result = output
            .try_into_document()
            .expect("parse succeeded without diagnostics but produced no document")
            .0;
        let root_span = result
            .root()
            .span()
            .expect("parsed document root should have a span");
        Ok((
            result.to_syntax(),
            chumsky::span::SimpleSpan::from(root_span.start..root_span.end),
        ))
    } else {
        Err(output
            .diagnostics
            .into_iter()
            .map(|diag| diag.message)
            .collect())
    }
}

pub(crate) fn linebreak_test_items() -> [ContextItem; 2] {
    [
        command_item("\\", CommandKind::Prefix, AllowedMode::Both, "!s !o:L").into(),
        command_item("newline", CommandKind::Prefix, AllowedMode::Both, "!s !o:L").into(),
    ]
}

pub(crate) fn test_context() -> ParseContext {
    static BASE_CTX: OnceLock<ParseContext> = OnceLock::new();
    BASE_CTX
        .get_or_init(|| {
            let mut builder = ParseContextBuilder::empty().packages(&["base"]);
            for item in shared_test_items() {
                builder = builder.insert_item(item.clone());
            }
            for item in linebreak_test_items() {
                builder = builder.insert_item(item);
            }
            builder.build().expect("shared test items should be valid")
        })
        .clone()
}

pub(crate) fn test_context_with_items<I, T>(items: I) -> ParseContext
where
    I: IntoIterator<Item = T>,
    T: Into<ContextItem>,
{
    let mut builder = ParseContextBuilder::empty().packages(&["base"]);
    for item in shared_test_items() {
        builder = builder.insert_item(item.clone());
    }
    for item in linebreak_test_items() {
        builder = builder.insert_item(item);
    }
    for item in items {
        builder = builder.insert_item(item);
    }
    builder
        .build()
        .expect("test items should have valid xparse specs")
}

pub(crate) fn shared_test_items() -> &'static [ContextItem] {
    static ITEMS: OnceLock<Vec<ContextItem>> = OnceLock::new();
    ITEMS.get_or_init(|| {
        let specs = load_package_specs_from_str(
            r#"
commands:
  - name: frac
    kind: prefix
    allowed_mode: math
    argspec: 'm m'
  - name: sqrt
    kind: prefix
    allowed_mode: math
    argspec: 'o m'
  - name: text
    kind: prefix
    allowed_mode: math
    argspec: 'm:T'
  - name: alpha
    kind: prefix
    allowed_mode: math
    argspec: ''
  - name: beta
    kind: prefix
    allowed_mode: math
    argspec: ''
  - name: gamma
    kind: prefix
    allowed_mode: math
    argspec: ''
  - name: delim
    kind: prefix
    allowed_mode: math
    argspec: 'm:D'
  - name: hspace
    kind: prefix
    allowed_mode: both
    argspec: 'm:L'
  - name: romannumeral
    kind: prefix
    allowed_mode: both
    argspec: 'm:I'
  - name: includegraphics
    kind: prefix
    allowed_mode: both
    argspec: 'o:K m:T'
  - name: label
    kind: prefix
    allowed_mode: both
    argspec: 'm:N'
  - name: over
    kind: infix
    allowed_mode: math
    argspec: ''
  - name: choose
    kind: infix
    allowed_mode: math
    argspec: ''
  - name: bfseries
    kind: declarative
    allowed_mode: both
    argspec: ''
  - name: qty
    kind: prefix
    allowed_mode: math
    argspec: 'd<(,)><[,]><{,}><|,|>'
  - name: pqty
    kind: prefix
    allowed_mode: math
    argspec: 's r{}'
  - name: abs
    kind: prefix
    allowed_mode: math
    argspec: 's r{}'
  - name: eval
    kind: prefix
    allowed_mode: math
    argspec: 's d<(,|><[,|><{,}>'
  - name: mqty
    kind: prefix
    allowed_mode: math
    argspec: 's d<(,)><[,]><{,}><|,|>'
  - name: dd
    kind: prefix
    allowed_mode: math
    argspec: 'o d<(,)><{,}>'
  - name: dv
    kind: prefix
    allowed_mode: math
    argspec: 's o m g'
  - name: pdv
    kind: prefix
    allowed_mode: math
    argspec: 's o m g g'
  - name: braket
    kind: prefix
    allowed_mode: math
    argspec: 's m g'
  - name: exp
    kind: prefix
    allowed_mode: math
    argspec: ''

environments:
  - name: matrix
    allowed_mode: math
    argspec: ''
    body_mode: math
  - name: align
    allowed_mode: math
    argspec: ''
    body_mode: math
  - name: align*
    allowed_mode: math
    argspec: ''
    body_mode: math

delimiters:
  - name: "."
    is_control_sequence: false
    allowed_mode: math
    unicode_value: ""
    attributes: {}
  - name: "("
    is_control_sequence: false
    allowed_mode: math
    unicode_value: "("
    attributes: {}
  - name: ")"
    is_control_sequence: false
    allowed_mode: math
    unicode_value: ")"
    attributes: {}
  - name: "["
    is_control_sequence: false
    allowed_mode: math
    unicode_value: "["
    attributes: {}
  - name: "]"
    is_control_sequence: false
    allowed_mode: math
    unicode_value: "]"
    attributes: {}
  - name: "|"
    is_control_sequence: false
    allowed_mode: math
    unicode_value: "|"
    attributes: {}
  - name: "<"
    is_control_sequence: false
    allowed_mode: math
    unicode_value: "<"
    attributes: {}
  - name: ">"
    is_control_sequence: false
    allowed_mode: math
    unicode_value: ">"
    attributes: {}
  - name: "/"
    is_control_sequence: false
    allowed_mode: math
    unicode_value: "/"
    attributes: {}
  - name: langle
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⟨"
    attributes: {}
  - name: rangle
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⟩"
    attributes: {}
  - name: "{"
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "{"
    attributes: {}
  - name: "}"
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "}"
    attributes: {}
  - name: lfloor
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⌊"
    attributes: {}
  - name: rfloor
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⌋"
    attributes: {}
  - name: lceil
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⌈"
    attributes: {}
  - name: rceil
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⌉"
    attributes: {}
  - name: lvert
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "|"
    attributes: {}
  - name: rvert
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "|"
    attributes: {}
  - name: lVert
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "‖"
    attributes: {}
  - name: rVert
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "‖"
    attributes: {}
  - name: lgroup
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⟮"
    attributes: {}
  - name: rgroup
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⟯"
    attributes: {}
  - name: lmoustache
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⎰"
    attributes: {}
  - name: rmoustache
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "⎱"
    attributes: {}
  - name: "|"
    is_control_sequence: true
    allowed_mode: math
    unicode_value: "‖"
    attributes: {}
"#,
            "inline-test",
        );

        let mut items = Vec::new();
        for command in specs.commands {
            items.push(
                CommandItem::new(
                    command.name,
                    command.kind,
                    command.allowed_mode,
                    command.argspec.source,
                )
                .with_tags(command.tags)
                .into(),
            );
        }
        for environment in specs.environments {
            items.push(
                EnvironmentItem::new(
                    environment.name,
                    environment.allowed_mode,
                    environment.body_mode,
                    environment.argspec.source,
                )
                .with_tags(environment.tags)
                .into(),
            );
        }
        for delimiter in specs.delimiters {
            if delimiter.is_control_sequence {
                items.push(DelimiterControlItem::new(delimiter.name).into());
            }
        }
        items
    })
}

pub(crate) fn command_item(
    name: &str,
    kind: CommandKind,
    allowed_mode: AllowedMode,
    spec: &str,
) -> CommandItem {
    CommandItem::new(name, kind, allowed_mode, spec)
}

pub(crate) fn environment_item(
    name: &str,
    allowed_mode: AllowedMode,
    body_mode: ContentMode,
    spec: &str,
) -> EnvironmentItem {
    EnvironmentItem::new(name, allowed_mode, body_mode, spec)
}

pub(crate) fn label_command_item() -> CommandItem {
    command_item("label", CommandKind::Prefix, AllowedMode::Both, "m:N")
}

pub(crate) fn expect_arg(slot: &Option<Argument>) -> &Argument {
    slot.as_ref()
        .unwrap_or_else(|| panic!("Expected argument slot to be present"))
}

pub(crate) fn unwrap_content(slot: &Option<Argument>) -> &SyntaxNode {
    match &expect_arg(slot).value {
        ArgumentValue::MathContent(node) | ArgumentValue::TextContent(node) => node,
        _ => panic!("Expected content argument"),
    }
}

pub(crate) fn assert_same_structure(with_spaces: &str, compact: &str) {
    let (result_spaces, _) = parse(with_spaces, false).unwrap();
    let (result_compact, _) = parse(compact, false).unwrap();
    assert_eq!(result_spaces, result_compact);
}

pub(crate) fn extract_first_command(node: SyntaxNode) -> (String, Vec<Option<Argument>>) {
    match node {
        SyntaxNode::Root { children, .. } => match &children[0] {
            SyntaxNode::Command { name, args, .. } => (name.clone(), args.clone()),
            other => panic!("Expected command node, got {:?}", other),
        },
        other => panic!("Expected root node, got {:?}", other),
    }
}

pub(crate) fn extract_command_args<'a>(
    node: &'a SyntaxNode,
    name: &str,
) -> Option<&'a [Option<Argument>]> {
    match node {
        SyntaxNode::Command {
            name: command_name,
            args,
            ..
        } if command_name == name => Some(args.as_slice()),
        SyntaxNode::Root { children, .. } | SyntaxNode::Group { children, .. } => children
            .iter()
            .find_map(|child| extract_command_args(child, name)),
        SyntaxNode::Command { args, .. } => args.iter().find_map(|slot| {
            slot.as_ref().and_then(|arg| match &arg.value {
                ArgumentValue::MathContent(node) | ArgumentValue::TextContent(node) => {
                    extract_command_args(node, name)
                }
                _ => None,
            })
        }),
        SyntaxNode::Declarative { args, .. } => args.iter().find_map(|slot| {
            slot.as_ref().and_then(|arg| match &arg.value {
                ArgumentValue::MathContent(node) | ArgumentValue::TextContent(node) => {
                    extract_command_args(node, name)
                }
                _ => None,
            })
        }),
        SyntaxNode::Environment { args, body, .. } => args
            .iter()
            .find_map(|slot| {
                slot.as_ref().and_then(|arg| match &arg.value {
                    ArgumentValue::MathContent(node) | ArgumentValue::TextContent(node) => {
                        extract_command_args(node, name)
                    }
                    _ => None,
                })
            })
            .or_else(|| extract_command_args(body, name)),
        SyntaxNode::Infix {
            args, left, right, ..
        } => args
            .iter()
            .find_map(|slot| {
                slot.as_ref().and_then(|arg| match &arg.value {
                    ArgumentValue::MathContent(node) | ArgumentValue::TextContent(node) => {
                        extract_command_args(node, name)
                    }
                    _ => None,
                })
            })
            .or_else(|| extract_command_args(left, name))
            .or_else(|| extract_command_args(right, name)),
        _ => None,
    }
}
