// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use super::document::parse_qualified_name;
use super::prelude::*;

#[cfg_attr(test, parser_test)]
/// ```test,Expression
/// something
/// "something"
/// 0.3
/// 42
/// 42px
/// #aabbcc
/// (something)
/// (something).something
/// @image-url("something")
/// @image_url("something")
/// some_id.some_property
/// function_call()
/// function_call(hello, world)
/// cond ? first : second
/// call_cond() ? first : second
/// (nested()) ? (ok) : (other.ko)
/// 4 + 4
/// 4 + 8 * 7 / 5 + 3 - 7 - 7 * 8
/// -0.3px + 0.3px - 3.pt+3pt
/// aa == cc && bb && (xxx || fff) && 3 + aaa == bbb
/// [array]
/// array[index]
/// {object:42}
/// "foo".bar.something().something.xx({a: 1.foo}.a)
/// (x) => x > 0
/// ```
pub fn parse_expression(p: &mut impl Parser) -> bool {
    p.peek(); // consume the whitespace so they aren't part of the Expression node
    parse_expression_helper(p, OperatorPrecedence::Default)
}

#[derive(Eq, PartialEq, Ord, PartialOrd)]
#[repr(u8)]
enum OperatorPrecedence {
    /// ` ?: `
    Default,
    /// `||`, `&&`
    Logical,
    /// `==` `!=` `>=` `<=` `<` `>`
    Equality,
    /// `+ -`
    Add,
    /// `* /`
    Mul,
    Unary,
}

fn parse_expression_helper(p: &mut impl Parser, precedence: OperatorPrecedence) -> bool {
    let mut p = p.start_node(SyntaxKind::Expression);
    let checkpoint = p.checkpoint();
    let mut possible_range = false;
    match p.nth(0).kind() {
        SyntaxKind::Identifier => {
            parse_qualified_name(&mut *p);
        }
        SyntaxKind::StringLiteral => {
            if p.nth(0).as_str().ends_with('{') {
                parse_template_string(&mut *p)
            } else {
                p.consume()
            }
        }
        SyntaxKind::NumberLiteral => {
            if p.nth(0).as_str().ends_with('.') {
                possible_range = true;
            }
            p.consume()
        }
        SyntaxKind::ColorLiteral => p.consume(),
        SyntaxKind::LParent => {
            if p.nth(1).kind() == SyntaxKind::Identifier
                && p.nth(2).kind() == SyntaxKind::RParent
                && p.nth(3).kind() == SyntaxKind::FatArrow
            {
                parse_closure(&mut *p);
            } else {
                p.consume();
                parse_expression(&mut *p);
                p.expect(SyntaxKind::RParent);
            }
        }
        SyntaxKind::LBracket => parse_array(&mut *p),
        SyntaxKind::LBrace => parse_object_notation(&mut *p),
        SyntaxKind::Plus | SyntaxKind::Minus | SyntaxKind::Bang => {
            let mut p = p.start_node(SyntaxKind::UnaryOpExpression);
            p.consume();
            parse_expression_helper(&mut *p, OperatorPrecedence::Unary);
        }
        SyntaxKind::At => {
            parse_at_keyword(&mut *p);
        }
        _ => {
            p.error("invalid expression");
            return false;
        }
    }

    loop {
        match p.nth(0).kind() {
            SyntaxKind::Dot => {
                {
                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
                }
                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::MemberAccess);
                p.consume(); // '.'
                if possible_range && p.peek().kind() == SyntaxKind::NumberLiteral {
                    let error = format!(
                        "Parse error. Range expressions are not supported in Slint. You can use an integer as a model to repeat something multiple time. Eg: `for i in {} : ...`",
                        p.peek().as_str()
                    );
                    p.error(error);
                    p.consume();
                    return false;
                }
                if !p.expect(SyntaxKind::Identifier) {
                    return false;
                }
            }
            SyntaxKind::LParent => {
                {
                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
                }
                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::FunctionCallExpression);
                parse_function_arguments(&mut *p);
            }
            SyntaxKind::LBracket => {
                {
                    let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
                }
                let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::IndexExpression);
                p.expect(SyntaxKind::LBracket);
                parse_expression(&mut *p);
                p.expect(SyntaxKind::RBracket);
            }
            _ => break,
        }
        possible_range = false;
    }

    if precedence >= OperatorPrecedence::Mul {
        return true;
    }

    while matches!(p.nth(0).kind(), SyntaxKind::Star | SyntaxKind::Div) {
        {
            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
        }
        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
        p.consume();
        parse_expression_helper(&mut *p, OperatorPrecedence::Mul);
    }

    if p.nth(0).kind() == SyntaxKind::Percent {
        p.error("Unexpected '%'. For the unit, it should be attached to the number. If you're looking for the modulo operator, use the 'Math.mod(x, y)' function");
        p.consume();
        return false;
    }

    if precedence >= OperatorPrecedence::Add {
        return true;
    }

    while matches!(p.nth(0).kind(), SyntaxKind::Plus | SyntaxKind::Minus) {
        {
            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
        }
        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
        p.consume();
        parse_expression_helper(&mut *p, OperatorPrecedence::Add);
    }

    if precedence > OperatorPrecedence::Equality {
        return true;
    }

    if matches!(
        p.nth(0).kind(),
        SyntaxKind::LessEqual
            | SyntaxKind::GreaterEqual
            | SyntaxKind::EqualEqual
            | SyntaxKind::NotEqual
            | SyntaxKind::LAngle
            | SyntaxKind::RAngle
    ) {
        if precedence == OperatorPrecedence::Equality {
            p.error("Use parentheses to disambiguate equality expression on the same level");
        }

        {
            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
        }
        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
        p.consume();
        parse_expression_helper(&mut *p, OperatorPrecedence::Equality);
    }

    if precedence >= OperatorPrecedence::Logical {
        return true;
    }

    let mut prev_logical_op = None;
    while matches!(p.nth(0).kind(), SyntaxKind::AndAnd | SyntaxKind::OrOr) {
        if let Some(prev) = prev_logical_op {
            if prev != p.nth(0).kind() {
                p.error("Use parentheses to disambiguate between && and ||");
                prev_logical_op = None;
            }
        } else {
            prev_logical_op = Some(p.nth(0).kind());
        }

        {
            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
        }
        let mut p = p.start_node_at(checkpoint.clone(), SyntaxKind::BinaryExpression);
        p.consume();
        parse_expression_helper(&mut *p, OperatorPrecedence::Logical);
    }

    if p.nth(0).kind() == SyntaxKind::Question {
        {
            let _ = p.start_node_at(checkpoint.clone(), SyntaxKind::Expression);
        }
        let mut p = p.start_node_at(checkpoint, SyntaxKind::ConditionalExpression);
        p.consume();
        parse_expression(&mut *p);
        p.expect(SyntaxKind::Colon);
        parse_expression(&mut *p);
    }
    true
}

#[cfg_attr(test, parser_test)]
/// ```test
/// (x) => x > 0
/// (y) => y == 42
/// (z) => true
/// ```
fn parse_closure(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::Closure);

    p.expect(SyntaxKind::LParent);

    {
        let mut p = p.start_node(SyntaxKind::DeclaredIdentifier);
        p.expect(SyntaxKind::Identifier);
    }

    p.expect(SyntaxKind::RParent);

    p.expect(SyntaxKind::FatArrow);

    parse_expression(&mut *p);
}

#[cfg_attr(test, parser_test)]
/// ```test
/// @image-url("/foo/bar.png")
/// @linear-gradient(0deg, blue, red)
/// @conic-gradient(blue 0deg, red 180deg)
/// @tr("foo", bar)
/// ```
fn parse_at_keyword(p: &mut impl Parser) {
    debug_assert_eq!(p.peek().kind(), SyntaxKind::At);
    match p.nth(1).as_str() {
        "image-url" | "image_url" => {
            parse_image_url(p);
        }
        "linear-gradient" | "linear_gradient" => {
            parse_gradient(p);
        }
        "radial-gradient" | "radial_gradient" => {
            parse_gradient(p);
        }
        "conic-gradient" | "conic_gradient" => {
            parse_gradient(p);
        }
        "tr" => {
            parse_tr(p);
        }
        "markdown" => {
            parse_markdown(p);
        }
        "keys" => {
            parse_keys(p);
        }
        _ => {
            p.consume();
            p.test(SyntaxKind::Identifier); // consume the identifier, so that autocomplete works
            p.error("Expected 'image-url', 'tr', 'keys', 'markdown' 'conic-gradient', 'linear-gradient', or 'radial-gradient' after '@'");
        }
    }
}

#[cfg_attr(test, parser_test)]
/// ```test,Array
/// [ a, b, c , d]
/// []
/// [a,]
/// [ [], [] ]
/// ```
fn parse_array(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::Array);
    p.expect(SyntaxKind::LBracket);

    while p.nth(0).kind() != SyntaxKind::RBracket {
        parse_expression(&mut *p);
        if !p.test(SyntaxKind::Comma) {
            break;
        }
    }
    p.expect(SyntaxKind::RBracket);
}

#[cfg_attr(test, parser_test)]
/// ```test,ObjectLiteral
/// {}
/// {a:b}
/// { a: "foo" , }
/// {a:b, c: 4 + 4, d: [a,] }
/// ```
fn parse_object_notation(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::ObjectLiteral);
    p.expect(SyntaxKind::LBrace);

    while p.nth(0).kind() != SyntaxKind::RBrace {
        let mut p = p.start_node(SyntaxKind::ObjectMember);
        p.expect(SyntaxKind::Identifier);
        p.expect(SyntaxKind::Colon);
        parse_expression(&mut *p);
        if !p.test(SyntaxKind::Comma) {
            break;
        }
    }
    p.expect(SyntaxKind::RBrace);
}

#[cfg_attr(test, parser_test)]
/// ```test
/// ()
/// (foo)
/// (foo, bar, foo)
/// (foo, bar(), xx+xx,)
/// ```
fn parse_function_arguments(p: &mut impl Parser) {
    p.expect(SyntaxKind::LParent);

    while p.nth(0).kind() != SyntaxKind::RParent {
        parse_expression(&mut *p);
        if !p.test(SyntaxKind::Comma) {
            break;
        }
    }
    p.expect(SyntaxKind::RParent);
}

#[cfg_attr(test, parser_test)]
/// ```test,StringTemplate
/// "foo\{bar}"
/// "foo\{4 + 5}foo"
/// ```
fn parse_template_string(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::StringTemplate);
    debug_assert!(p.nth(0).as_str().ends_with("\\{"));
    p.expect(SyntaxKind::StringLiteral);
    loop {
        parse_expression(&mut *p);
        let peek = p.peek();
        if peek.kind != SyntaxKind::StringLiteral || !peek.as_str().starts_with('}') {
            p.error("Error while parsing string template")
        }
        let cont = peek.as_str().ends_with('{');
        p.consume();
        if !cont {
            break;
        }
    }
}

#[cfg_attr(test, parser_test)]
/// ```test,AtGradient
/// @linear-gradient(#e66465, #9198e5)
/// @linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c)
/// @linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%)
/// @linear-gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
/// @linear_gradient(217deg, rgba(255,0,0,0.8), rgba(255,0,0,0) 70.71%)
/// @radial-gradient(circle, #e66465, blue 50%, #9198e5)
/// @conic-gradient(#e66465 0deg, #9198e5 180deg, #e66465 360deg)
/// @conic-gradient(red 0deg, green 120deg, blue 240deg, red 360deg)
/// @conic-gradient(#fff 0turn, #000 0.5turn, #fff 1turn)
/// @conic_gradient(red 0rad, blue 3.14159rad, red 6.28318rad)
/// ```
fn parse_gradient(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::AtGradient);
    p.expect(SyntaxKind::At);
    debug_assert!(p.peek().as_str().ends_with("gradient"));
    p.expect(SyntaxKind::Identifier); //eg "linear-gradient"

    p.expect(SyntaxKind::LParent);

    while !p.test(SyntaxKind::RParent) {
        if !parse_expression(&mut *p) {
            return;
        }
        p.test(SyntaxKind::Comma);
    }
}

#[cfg_attr(test, parser_test)]
/// ```test,AtTr
/// @tr("foo")
/// @tr("foo{0}", bar(42))
/// @tr("context" => "ccc{}", 0)
/// @tr("xxx" => "ccc{n}" | "ddd{}" % 42, 45)
/// ```
fn parse_tr(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::AtTr);
    p.expect(SyntaxKind::At);
    debug_assert_eq!(p.peek().as_str(), "tr");
    p.expect(SyntaxKind::Identifier); //"tr"
    p.expect(SyntaxKind::LParent);

    let checkpoint = p.checkpoint();

    fn consume_literal(p: &mut impl Parser) -> bool {
        let peek = p.peek();
        if peek.kind() != SyntaxKind::StringLiteral
            || !peek.as_str().starts_with('"')
            || !peek.as_str().ends_with('"')
        {
            p.error("Expected plain string literal");
            return false;
        }
        p.expect(SyntaxKind::StringLiteral)
    }

    if !consume_literal(&mut *p) {
        return;
    }

    if p.test(SyntaxKind::FatArrow) {
        drop(p.start_node_at(checkpoint, SyntaxKind::TrContext));
        if !consume_literal(&mut *p) {
            return;
        }
    }

    if p.peek().kind() == SyntaxKind::Pipe {
        let mut p = p.start_node(SyntaxKind::TrPlural);
        p.consume();
        if !consume_literal(&mut *p) || !p.expect(SyntaxKind::Percent) {
            let _ = p.start_node(SyntaxKind::Expression);
            return;
        }
        parse_expression(&mut *p);
    }

    while p.test(SyntaxKind::Comma) {
        if !parse_expression(&mut *p) {
            break;
        }
    }
    p.expect(SyntaxKind::RParent);
}

/// ```test,AtTr
/// @markdown("foo")
/// @markdown("foo\{bar(42)} xx")
/// @markdown("foo\n" "bar")
/// ```
fn parse_markdown(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::AtMarkdown);
    p.expect(SyntaxKind::At);
    debug_assert!(p.peek().as_str().ends_with("markdown"));
    p.expect(SyntaxKind::Identifier); //eg "markdown"
    p.expect(SyntaxKind::LParent);

    let mut has_content = false;
    loop {
        let peek = p.peek();
        if peek.kind() != SyntaxKind::StringLiteral {
            break;
        }
        if peek.as_str().ends_with('{') {
            parse_template_string(&mut *p)
        } else {
            p.consume()
        }
        has_content = true;
    }

    if !has_content {
        p.error("Expected string literal");
        p.until(SyntaxKind::RParent);
        return;
    }

    if !p.expect(SyntaxKind::RParent) {
        p.until(SyntaxKind::RParent);
    }
}

#[cfg_attr(test, parser_test)]
/// ```test,AtKeys
/// @keys()
/// @keys("x")
/// @keys(Control +Shift + Alt+Meta+"A")
/// @keys(Control +Shift + Alt+Meta+Return)
/// @keys(Control +Shift? + Alt+Meta+Return)
/// @keys(Control +Shift + Alt?+Meta+Return)
/// ```
fn parse_keys(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::AtKeys);
    p.expect(SyntaxKind::At);
    debug_assert_eq!(p.peek().as_str(), "keys");
    p.expect(SyntaxKind::Identifier); //"keys"
    p.expect(SyntaxKind::LParent);

    // Parse custom syntax here...
    let mut key_count = 0_u32;

    let mut alt_count = 0_u32;
    let mut control_count = 0_u32;
    let mut shift_count = 0_u32;
    let mut meta_count = 0_u32;
    let mut ignore_shift_count = 0_u32;
    let mut ignore_alt_count = 0_u32;

    #[derive(Eq, PartialEq)]
    enum State {
        Start,
        NeedPlus,
        NeedKey,
    }
    let mut state = State::Start;

    fn bail(p: &mut crate::parser::Node<'_, impl Parser>, message: &str) {
        p.error(message);
        p.until(SyntaxKind::RParent);
    }

    loop {
        match p.peek().kind() {
            SyntaxKind::RParent => {
                assert!(key_count <= 1);
                // Trailing plus
                if state == State::NeedKey {
                    p.error("Expected another identifier or string literal");
                } else if key_count == 0
                    && (alt_count + control_count + shift_count + meta_count) > 0
                {
                    p.error("A keyboard shortcut must be empty or contain exactly one key (with modifiers)");
                }
                p.consume();
                break;
            }
            SyntaxKind::Plus => {
                if state == State::NeedPlus {
                    state = State::NeedKey;
                    p.consume();
                } else {
                    bail(
                        &mut p,
                        "Unexpected '+' in keyboard shortcut (use Plus to refer to the key)",
                    );
                    break;
                }
                continue;
            }
            SyntaxKind::Identifier | SyntaxKind::StringLiteral => {
                if state == State::NeedPlus {
                    bail(&mut p, "Expected '+' to separate parts of a keyboard shortcut");
                    break;
                }

                let token = p.peek();
                let mut consume_count = 1;
                // Modifiers must be identifiers, not string literals
                if token.kind() == SyntaxKind::Identifier {
                    let text = token.as_str();

                    let mut try_consume_question = || -> bool {
                        let next_token = p.nth(1);
                        if next_token.kind() == SyntaxKind::Question {
                            consume_count += 1;
                            true
                        } else {
                            false
                        }
                    };

                    match text {
                        "Ctrl" => {
                            bail(&mut p, "Ctrl is not in the Key namespace (Use Control instead)");
                            break;
                        }
                        "Control" => control_count += 1,
                        "Meta" => meta_count += 1,
                        "Alt" => {
                            if try_consume_question() {
                                ignore_alt_count += 1;
                            } else {
                                alt_count += 1
                            }
                        }
                        "Shift" => {
                            if try_consume_question() {
                                ignore_shift_count += 1;
                            } else {
                                shift_count += 1;
                            }
                        }
                        "AltR" | "ShiftR" | "MetaR" | "ControlR" => {
                            bail(&mut p, "Right-side modifiers are not supported");
                            break;
                        }
                        "AltGr" => {
                            bail(&mut p, "AltGr cannot be used as a modifier");
                            break;
                        }
                        "Command" | "Cmd" => {
                            bail(
                                &mut p,
                                // \x20 equals to a space (needed to avoid the trailing \ eating
                                // the indentation)
                                &format!(
                                    "{text} is not a cross-platform modifier\n\
                                    Use cross-platform modifier names instead:\n\
                                    \x20   ⌘ command -> Control\n\
                                    \x20   ⌥ option -> Alt\n\
                                    \x20   ^ control -> Meta\n\
                                    \x20   ⇧ shift -> Shift"
                                ),
                            );
                            break;
                        }
                        "Win" | "Windows" => {
                            bail(
                                &mut p,
                                &format!(
                                    "{text} is not a cross-platform modifier (Use `Meta` instead)"
                                ),
                            );
                            break;
                        }
                        _ => key_count += 1,
                    }
                } else {
                    key_count += 1;
                }

                state = State::NeedPlus;

                if [
                    alt_count,
                    control_count,
                    meta_count,
                    shift_count,
                    ignore_shift_count,
                    ignore_alt_count,
                ]
                .into_iter()
                .max()
                .unwrap_or_default()
                    > 1
                {
                    bail(&mut p, "Duplicated modifier in keyboard shortcut");
                    break;
                }
                if shift_count > 0 && ignore_shift_count > 0 {
                    bail(&mut p, "Cannot use both Shift and Shift? (remove one of them)");
                    break;
                }
                if alt_count > 0 && ignore_alt_count > 0 {
                    bail(&mut p, "Cannot use both Alt and Alt? (remove one of them)");
                    break;
                }
                if key_count > 1 {
                    bail(&mut p, "A keyboard shortcut can only contain one key (with modifiers)");
                    break;
                }

                for _ in 0..consume_count {
                    p.consume();
                }
                continue;
            }
            _ => {
                let hint = if state == State::NeedKey {
                    format!("\n(Consider using \"{}\")", p.peek().as_str())
                } else {
                    "".into()
                };
                bail(
                    &mut p,
                    &format!(
                        "Expected '+', a string literal, or an identifier in the Keys namespace{hint}"
                    ),
                );
                break;
            }
        }
    }
}

#[cfg_attr(test, parser_test)]
/// ```test,AtImageUrl
/// @image-url("foo.png")
/// @image-url("foo.png",)
/// @image-url("foo.png", nine-slice(1 2 3 4))
/// @image-url("foo.png", nine-slice(1))
/// ```
fn parse_image_url(p: &mut impl Parser) {
    let mut p = p.start_node(SyntaxKind::AtImageUrl);
    p.consume(); // "@"
    p.consume(); // "image-url"
    if !(p.expect(SyntaxKind::LParent)) {
        return;
    }
    let peek = p.peek();
    if peek.kind() != SyntaxKind::StringLiteral {
        p.error("@image-url must contain a plain path as a string literal");
        p.until(SyntaxKind::RParent);
        return;
    }
    if !peek.as_str().starts_with('"') || !peek.as_str().ends_with('"') {
        p.error("@image-url must contain a plain path as a string literal, without any '\\{}' expressions");
        p.until(SyntaxKind::RParent);
        return;
    }
    p.expect(SyntaxKind::StringLiteral);
    if !p.test(SyntaxKind::Comma) {
        if !p.test(SyntaxKind::RParent) {
            p.error("Expected ')' or ','");
            p.until(SyntaxKind::RParent);
        }
        return;
    }
    if p.test(SyntaxKind::RParent) {
        return;
    }
    if p.peek().as_str() != "nine-slice" {
        p.error("Expected 'nine-slice(...)' argument");
        p.until(SyntaxKind::RParent);
        return;
    }
    p.consume();
    if !p.expect(SyntaxKind::LParent) {
        p.until(SyntaxKind::RParent);
        return;
    }
    let mut count = 0;
    loop {
        match p.peek().kind() {
            SyntaxKind::RParent => {
                if count != 1 && count != 2 && count != 4 {
                    p.error("Expected 1 or 2 or 4 numbers");
                }
                p.consume();
                break;
            }
            SyntaxKind::NumberLiteral => {
                count += 1;
                p.consume();
            }
            SyntaxKind::Comma | SyntaxKind::Colon => {
                p.error("Arguments of nine-slice need to be separated by spaces");
                p.until(SyntaxKind::RParent);
                break;
            }
            _ => {
                p.error("Expected number literal or ')'");
                p.until(SyntaxKind::RParent);
                break;
            }
        }
    }
    if !p.expect(SyntaxKind::RParent) {
        p.until(SyntaxKind::RParent);
    }
}
