// Copyright (c) ZeroC, Inc.

use crate::ast::node::Node;
use crate::grammar::*;
use crate::parsers::slice::tokens::*;
use crate::parsers::slice::grammar::*;
use crate::parsers::slice::parser::Parser;
use crate::slice_file::Span;
use crate::utils::ptr_util::{OwnedPtr, WeakPtr};

// Specify the signature of the parser's entry function.
grammar<'input, 'a>(parser: &mut Parser<'a>);

extern {
    // Specify the types that the parser should use for location tracking and error emission.
    type Location = crate::slice_file::Location;
    type Error = crate::parsers::slice::tokens::Error;

    // Link the names of terminal tokens with their actual token types. Ex: `identifier => TokenKind::Identifier`
    // says that wherever we use `identifier` in the grammar, it actually represents a `TokenKind::Identifier`.
    // Identifiers must match the names we use in the grammar rules, and values must match enumerators in `tokens.rs`.
    enum TokenKind<'input> {
        identifier => TokenKind::Identifier(<&'input str>),

        string_literal => TokenKind::StringLiteral(<&'input str>),
        integer_literal => TokenKind::IntegerLiteral(<&'input str>),

        doc_comment => TokenKind::DocComment(<&'input str>),

        // Definition keywords
        module_keyword => TokenKind::ModuleKeyword,
        struct_keyword => TokenKind::StructKeyword,
        exception_keyword => TokenKind::ExceptionKeyword,
        class_keyword => TokenKind::ClassKeyword,
        interface_keyword => TokenKind::InterfaceKeyword,
        enum_keyword => TokenKind::EnumKeyword,
        custom_keyword => TokenKind::CustomKeyword,
        type_alias_keyword => TokenKind::TypeAliasKeyword,
        result_keyword => TokenKind::ResultKeyword,

        // Collection keywords
        sequence_keyword => TokenKind::SequenceKeyword,
        dictionary_keyword => TokenKind::DictionaryKeyword,

        // Primitive type keywords
        bool_keyword => TokenKind::BoolKeyword,
        int8_keyword => TokenKind::Int8Keyword,
        uint8_keyword => TokenKind::UInt8Keyword,
        int16_keyword => TokenKind::Int16Keyword,
        uint16_keyword => TokenKind::UInt16Keyword,
        int32_keyword => TokenKind::Int32Keyword,
        uint32_keyword => TokenKind::UInt32Keyword,
        varint32_keyword => TokenKind::VarInt32Keyword,
        varuint32_keyword => TokenKind::VarUInt32Keyword,
        int64_keyword => TokenKind::Int64Keyword,
        uint64_keyword => TokenKind::UInt64Keyword,
        varint62_keyword => TokenKind::VarInt62Keyword,
        varuint62_keyword => TokenKind::VarUInt62Keyword,
        float32_keyword => TokenKind::Float32Keyword,
        float64_keyword => TokenKind::Float64Keyword,
        string_keyword => TokenKind::StringKeyword,
        any_class_keyword => TokenKind::AnyClassKeyword,

        // Other keywords
        compact_keyword => TokenKind::CompactKeyword,
        idempotent_keyword => TokenKind::IdempotentKeyword,
        mode_keyword => TokenKind::ModeKeyword,
        stream_keyword => TokenKind::StreamKeyword,
        tag_keyword => TokenKind::TagKeyword,
        throws_keyword => TokenKind::ThrowsKeyword,
        unchecked_keyword => TokenKind::UncheckedKeyword,

        // Brackets
        "(" => TokenKind::LeftParenthesis,
        ")" => TokenKind::RightParenthesis,
        "[" => TokenKind::LeftBracket,
        "]" => TokenKind::RightBracket,
        "[[" => TokenKind::DoubleLeftBracket,
        "]]" => TokenKind::DoubleRightBracket,
        "{" => TokenKind::LeftBrace,
        "}" => TokenKind::RightBrace,
        "<" => TokenKind::LeftChevron,
        ">" => TokenKind::RightChevron,

        // Symbols
        "," => TokenKind::Comma,
        ":" => TokenKind::Colon,
        "::" => TokenKind::DoubleColon,
        "=" => TokenKind::Equals,
        "?" => TokenKind::QuestionMark,
        "->" => TokenKind::Arrow,
        "-" => TokenKind::Minus,
    }
}

// Grammar Rules

// TODO we can probably allow module to come before or after the compilation mode now.
pub SliceFile: (Option<FileCompilationMode>, Vec<WeakPtr<Attribute>>, Option<OwnedPtr<Module>>, Vec<Definition>) = {
    <sfp: SliceFilePrelude> <m: Module?> <ds: Definition*> => (sfp.0, sfp.1, m, ds),
}

SliceFilePrelude: (Option<FileCompilationMode>, Vec<WeakPtr<Attribute>>) = {
    => (None, Vec::new()),
    <sfp: SliceFilePrelude> <fe: FileCompilationMode> => handle_file_compilation_mode(parser, sfp, fe),
    <mut sfp: SliceFilePrelude> <fa: FileAttribute> => {
        sfp.1.push(fa);
        sfp
    },
}

FileCompilationMode: FileCompilationMode = {
    <l: @L> mode_keyword "=" <i: Identifier> <r: @R> => {
        construct_file_compilation_mode(parser, i, Span::new(l, r, parser.file_name))
    },
}

Module: OwnedPtr<Module> = {
    <p: Prelude> <l: @L> module_keyword <i: RelativeIdentifier> <r: @R> => {
        construct_module(parser, p, i, Span::new(l, r, parser.file_name))
    },
}

Definition: Definition = {
    Struct => Definition::Struct(parser.ast.add_named_element(<>)),
    Exception => Definition::Exception(parser.ast.add_named_element(<>)),
    Class => Definition::Class(parser.ast.add_named_element(<>)),
    Interface => Definition::Interface(parser.ast.add_named_element(<>)),
    Enum => Definition::Enum(parser.ast.add_named_element(<>)),
    CustomType => Definition::CustomType(parser.ast.add_named_element(<>)),
    TypeAlias => Definition::TypeAlias(parser.ast.add_named_element(<>)),
}

Struct: OwnedPtr<Struct> = {
    <p: Prelude> <l1: @L> <ck: compact_keyword?> <l2: @L> struct_keyword <i: ContainerIdentifier> <r: @R> "{" <dms: UndelimitedList<Field>> "}" ContainerEnd => {
        let l = if ck.is_some() { l1 } else { l2 };
        construct_struct(parser, p, ck.is_some(), i, dms, Span::new(l, r, parser.file_name))
    },
}

Exception: OwnedPtr<Exception> = {
    <p: Prelude> <l: @L> exception_keyword <i: ContainerIdentifier> <r: @R> <tr: (":" <TypeRef>)?> "{" <dms: UndelimitedList<Field>> "}" ContainerEnd => {
        construct_exception(parser, p, i, tr, dms, Span::new(l, r, parser.file_name))
    },
}

Class: OwnedPtr<Class> = {
    <p: Prelude> <l: @L> class_keyword <i: ContainerIdentifier> <r1: @R> <ci: CompactId?> <r2: @R> <tr: (":" <TypeRef>)?> "{" <dms: UndelimitedList<Field>> "}" ContainerEnd => {
        let r = if ci.is_some() { r2 } else { r1 };
        construct_class(parser, p, i, ci, tr, dms, Span::new(l, r, parser.file_name))
    },
}

Field: OwnedPtr<Field> = {
    <p: Prelude> <l1: @L> <t: Tag?> <l2: @L> <i: Identifier> ":" <tr: TypeRef> <r: @R> => {
        let l = if t.is_some() { l1 } else { l2 };
        construct_field(parser, p, i, t, tr, Span::new(l, r, parser.file_name))
    },
}

Interface: OwnedPtr<Interface> = {
    <p: Prelude> <l: @L> interface_keyword <i: ContainerIdentifier> <r: @R> <trs: (":" <NonEmptyCommaList<TypeRef>>)?> "{" <os: Operation*> "}" ContainerEnd => {
        construct_interface(parser, p, i, trs, os, Span::new(l, r, parser.file_name))
    },
}

Operation: OwnedPtr<Operation> = {
    <p: Prelude> <l1: @L> <ik: idempotent_keyword?> <l2: @L> <i: ContainerIdentifier> "(" <ps: UndelimitedList<Parameter>> ")" <rt: ReturnType?> <r1: @R> <es: ExceptionSpecification?> <r2: @R> ContainerEnd => {
        let l = if ik.is_some() { l1 } else { l2 };
        let r = if es.is_some() { r2 } else { r1 };
        construct_operation(parser, p, ik.is_some(), i, ps, rt, es, Span::new(l, r, parser.file_name))
    },
}

Parameter: OwnedPtr<Parameter> = {
    <p: Prelude> <l1: @L> <t: Tag?> <l2: @L> <i: Identifier> ":" <s: stream_keyword?> <tr: TypeRef> <r: @R> => {
        let l = if t.is_some() { l1 } else { l2 };
        construct_parameter(parser, p, i, t, s.is_some(), tr, Span::new(l, r, parser.file_name))
    },
}

ReturnType: Vec<OwnedPtr<Parameter>> = {
    "->" <l: @L> <t: Tag?> <s: stream_keyword?> <tr: TypeRef> <r: @R> => {
        construct_single_return_type(parser, t, s.is_some(), tr, Span::new(l, r, parser.file_name))
    },
    "->" <l: @L> "(" <ps: UndelimitedList<Parameter>> ")" <r: @R> => {
        check_return_tuple(parser, &ps, Span::new(l, r, parser.file_name));
        ps
    },
}

ExceptionSpecification: Vec<TypeRef> = {
    throws_keyword <TypeRef> => vec![<>],
    throws_keyword "(" <NonEmptyCommaList<TypeRef>> ")" => <>,
}

Enum: OwnedPtr<Enum> = {
    <p: Prelude> <l1: @L> <ck: compact_keyword?> <uk: unchecked_keyword?> <l2: @L> enum_keyword <i: ContainerIdentifier> <r: @R> <tr: (":" <TypeRef>)?> "{" <es: UndelimitedList<Enumerator>> "}" ContainerEnd => {
        let l = if ck.is_some() || uk.is_some() { l1 } else { l2 };
        construct_enum(parser, p, ck.is_some(), uk.is_some(), i, tr, es, Span::new(l, r, parser.file_name))
    },
}

Enumerator: OwnedPtr<Enumerator> = {
    <p: Prelude> <l: @L> <i: ContainerIdentifier> <afs: ("(" <UndelimitedList<Field>> ")")?> <si: ("=" <SignedInteger>)?> <r: @R> ContainerEnd => {
        construct_enumerator(parser, p, i, afs, si, Span::new(l, r, parser.file_name))
    },
}

CustomType: OwnedPtr<CustomType> = {
    <p: Prelude> <l: @L> custom_keyword <i: Identifier> <r: @R> => {
        construct_custom_type(parser, p, i, Span::new(l, r, parser.file_name))
    },
}

TypeAlias: OwnedPtr<TypeAlias> = {
    <p: Prelude> <l: @L> type_alias_keyword <i: Identifier> <r: @R> "=" <tr: TypeRef> => {
        construct_type_alias(parser, p, i, tr, Span::new(l, r, parser.file_name))
    },
}

Result: OwnedPtr<ResultType> = {
    result_keyword "<" <success_type: TypeRef> "," <failure_type: TypeRef> ">" => {
        OwnedPtr::new(ResultType { success_type, failure_type })
    },
}

Sequence: OwnedPtr<Sequence> = {
    sequence_keyword "<" <element_type: TypeRef> ">" => {
        OwnedPtr::new(Sequence { element_type })
    },
}

Dictionary: OwnedPtr<Dictionary> = {
    dictionary_keyword "<" <key_type: TypeRef> "," <value_type: TypeRef> ">" => {
        OwnedPtr::new(Dictionary { key_type, value_type })
    },
}

Primitive: Primitive = {
    bool_keyword => Primitive::Bool,
    int8_keyword => Primitive::Int8,
    uint8_keyword => Primitive::UInt8,
    int16_keyword => Primitive::Int16,
    uint16_keyword => Primitive::UInt16,
    int32_keyword => Primitive::Int32,
    uint32_keyword => Primitive::UInt32,
    varint32_keyword => Primitive::VarInt32,
    varuint32_keyword => Primitive::VarUInt32,
    int64_keyword => Primitive::Int64,
    uint64_keyword => Primitive::UInt64,
    varint62_keyword => Primitive::VarInt62,
    varuint62_keyword => Primitive::VarUInt62,
    float32_keyword => Primitive::Float32,
    float64_keyword => Primitive::Float64,
    string_keyword => Primitive::String,
    any_class_keyword => Primitive::AnyClass,
}

TypeRef: TypeRef = {
    <l: @L> <las: LocalAttribute*> <trd: TypeRefDefinition> <o: "?"?> <r: @R> => {
        construct_type_ref(parser, las, trd, o.is_some(), Span::new(l, r, parser.file_name))
    },
}

TypeRefDefinition: TypeRefDefinition = {
    Primitive => primitive_to_type_ref_definition(parser, <>),
    Result => anonymous_type_to_type_ref_definition(parser, <>),
    Sequence => anonymous_type_to_type_ref_definition(parser, <>),
    Dictionary => anonymous_type_to_type_ref_definition(parser, <>),
    RelativeIdentifier => construct_unpatched_type_ref_definition(<>),
    GlobalIdentifier => construct_unpatched_type_ref_definition(<>),
}

FileAttribute = "[[" <Attribute> "]]";

LocalAttribute = "[" <Attribute> "]";

Attribute: WeakPtr<Attribute> = {
    <l: @L> <rsi: RelativeIdentifier> <aas: ("(" <CommaList<AttributeArgument>> ")")?> <r: @R> => {
        construct_attribute(parser, rsi, aas, Span::new(l, r, parser.file_name))
    },
}

AttributeArgument: String = {
    <sl: string_literal> => unescape_string_literal(sl),
    <i: identifier> => i.to_owned(),
}

Identifier: Identifier = {
    <l: @L> <i: identifier> <r: @R> => {
        Identifier { value: i.to_owned(), span: Span::new(l, r, parser.file_name) }
    },
}

RelativeIdentifier: Identifier = {
    <l: @L> <i: identifier> <mut v: ("::" <identifier>)*> <r: @R> => {
        v.insert(0, i);
        Identifier { value: v.join("::"), span: Span::new(l, r, parser.file_name) }
    },
}

GlobalIdentifier: Identifier = {
    <l: @L> <mut v: ("::" <identifier>)+> <r: @R> => {
        v.insert(0, ""); // Gives a leading "::" when we `join`.
        Identifier { value: v.join("::"), span: Span::new(l, r, parser.file_name) }
    },
}

Integer: Integer<i128> = {
    <l: @L> <i: integer_literal> <r: @R> => {
        try_parse_integer(parser, i, Span::new(l, r, parser.file_name))
    },
}

SignedInteger: Integer<i128> = {
    <i: Integer> => i,
    <l: @L> "-" <mut i: Integer> => Integer {
        value: -i.value,
        span: Span { start: l, ..i.span },
    },
}

Tag: Integer<u32> = {
    tag_keyword "(" <i: SignedInteger> ")" => {
        parse_tag_value(parser, i)
    },
}

CompactId: Integer<u32> = {
    "(" <i: SignedInteger> ")" => {
        parse_compact_id_value(parser, i)
    },
}

Prelude: (Vec<(&'input str, Span)>, Vec<WeakPtr<Attribute>>) = {
    => (Vec::new(), Vec::new()),
    <mut prelude: Prelude> <l: @L> <comment: doc_comment> <r: @R> => {
        prelude.0.push((comment, Span::new(l, r, parser.file_name)));
        prelude
    },
    <mut prelude: Prelude> <attribute: LocalAttribute> => {
        prelude.1.push(attribute);
        prelude
    },
}

// Utility Rules

// A comma separated list of 1 or more elements, with an optional trailing comma.
NonEmptyCommaList<T>: Vec<T> = {
    <element: T> <mut vector: ("," <T>)*> ","? => {
        vector.insert(0, element);
        vector
    },
}

// A comma separated list of 0 or more elements, with an optional trailing comma.
CommaList<T>: Vec<T> = {
    NonEmptyCommaList<T> => <>,
    => Vec::new(),
}

// A list of 0 or more elements with no required separators.
// A single comma can optionally be placed after each element (including a trailing comma),
// but these are ignored by the compiler and only for user-readability.
UndelimitedList<T>: Vec<T> = {
    (<T> ","?)* => <>,
}

ContainerIdentifier: Identifier = {
    Identifier => {
        parser.current_scope.push_scope(&<>.value);
        <>
    },
}

ContainerEnd: () = {
    => parser.current_scope.pop_scope(),
}
