// -*- Rust -*-

// Copyright 2023 Silvio Mayolo
//
// This file is part of GDLisp.
//
// GDLisp is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GDLisp is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with GDLisp. If not, see <https://www.gnu.org/licenses/>.

use std::str::FromStr;
use crate::sxp::ast::AST;
use crate::sxp::string;
use crate::sxp::syntax;
use crate::pipeline::source::SourceOffset;

grammar;

Int: i32 = <s:INT> => i32::from_str(s).unwrap();
Float: f32 = <s:FLOAT> => f32::from_str(s).unwrap();
String: String = <s:STRING> => {
  let t = &s[1..s.bytes().count()-1];
  string::parse_escapes(t).unwrap()
};
Symbol: String = <s:SYMBOL> => String::from(s);
NodePath: String = {
  <s:PATH> => {
    String::from(&s[1..])
  },
  <s:QUOTED_PATH> => {
    let t = &s[2..s.bytes().count()-1];
    string::parse_escapes(t).unwrap()
  },
};

match {
  r"\s*" => {},
  r";[^\n\r]*[\n\r]?" => {}, // Line comments (starting with semicolon)
  r"#\|([^|]|\|[^#])*\|#" => {}, // Block comments (nesting is not currently allowed)
  "#t", "#f",
  r"[+-]?[0-9]+" => INT,
  r"[+-]?[0-9]+((\.[0-9]+)([eE][+-]?[0-9]+)?|(\.[0-9]+)?([eE][+-]?[0-9]+))" => FLOAT,
  r#""([^\\"]|\\.)*""# => STRING,
  r"\$[A-Za-z0-9_~+=\-\\!$%^&*<>/?]+" => PATH,
  r#"\$"([^\\"]|\\.)*""# => QUOTED_PATH,
  "(", ")", ".", "[", "]", "'", "#'", "`", ",", ":", "V{", "{", "}", ",.", "@",
} else {
  // A symbol consists of the following, in order:
  //
  // 1. A starting character
  //
  // 2. An initial segment of zero or more subsequent characters.
  //
  // 3. A sequence of zero or more qualifiers, where a qualifier is a
  // dot followed by one or more subsequent characters.
  //
  // The starting character can be an ASCII letter, underscore, tilde,
  // plus, equal sign, minus, backslash, forward slash, exclamation
  // mark, percent, caret, ampersand, star, less than, greater than,
  // question mark, or any Unicode character in the categories L, Mn,
  // Nl, No, S, Pc, Pd, Po.
  //
  // The subsequent characters can be any starting character, an ASCII
  // number, or any Unicode character in the category N.
  r"[A-Za-z_~+=\-\\!%^&*<>/?[\p{L}\p{Mn}\p{No}\p{Nl}\p{S}\p{Pc}\p{Pd}\p{Po}--\p{Ascii}]][A-Za-z0-9_~+=\-\\!$%^&*<>/?[\p{L}\p{Mn}\p{N}\p{S}\p{Pc}\p{Pd}\p{Po}--\p{Ascii}]]*(\.[A-Za-z0-9_~+=\-\\!$%^&*<>/?[\p{L}\p{Mn}\p{N}\p{S}\p{Pc}\p{Pd}\p{Po}--\p{Ascii}]]+)*" => SYMBOL,
}

pub AST: AST = {
  <ast: LiteralOrNestedAST> => ast,
  <pos:@L> "'" <body: AST> => syntax::quote(body, SourceOffset(pos)),
  <pos:@L> "#'" <body: AST> => syntax::function(body, SourceOffset(pos)),
  <pos:@L> "`" <body: AST> => syntax::quasiquote(body, SourceOffset(pos)),
  <pos:@L> "," <body: AST> => syntax::unquote(body, SourceOffset(pos)),
  <pos:@L> ",." <body: AST> => syntax::unquote_spliced(body, SourceOffset(pos)),
}

pub SomeAST: AST = {
  <pos:@L> <one: AST> => AST::cons(one, AST::nil(SourceOffset(pos)), SourceOffset(pos)),
  <pos:@L> <first: AST> <rest: SomeAST> => AST::cons(first, rest, SourceOffset(pos)),
}

// This is here to fix an ambiguity with the grammar. Specifically, I
// want ' a : b to parse as (quote (access-slot a b)), not
// (access-slot (quote a) b). There is never a use case where the
// latter would be helpful, as (quote a) has no GDScript-only slots on
// it. The same applies to function and quasiquote. unquote is a bit
// more nebulous, as there are use cases for either possible parse of
// ,a:b, but I'm treating it the same as the other three for now.
pub LiteralOrNestedAST: AST = {
  <pos:@L> "#t" => AST::from_value(true, SourceOffset(pos)),
  <pos:@L> "#f" => AST::from_value(false, SourceOffset(pos)),
  <pos:@L> <i: Int> => AST::from_value(i, SourceOffset(pos)),
  <pos:@L> <f: Float> => AST::from_value(f, SourceOffset(pos)),
  <pos:@L> <s: String> => AST::string(s, SourceOffset(pos)), // TODO Escaping
  <pos:@L> <s: Symbol> => AST::symbol(s, SourceOffset(pos)),
  <pos:@L> "(" ")" => AST::nil(SourceOffset(pos)),
  <pos:@L> "(" <car: AST> <cdr: Cdr> ")" => AST::cons(car, cdr, SourceOffset(pos)),
  <pos:@L> "[" <contents: ArrayContents> "]" => syntax::array(contents, SourceOffset(pos)),
  <pos:@L> "{" <contents: DictContents> "}" => syntax::dict(contents, SourceOffset(pos)),
  <pos:@L> "V{" <x: AST> <y: AST> "}" => syntax::vector2(x, y, SourceOffset(pos)),
  <pos:@L> "V{" <x: AST> <y: AST> <z: AST> "}" => syntax::vector3(x, y, z, SourceOffset(pos)),
  <pos:@L> <body: LiteralOrNestedAST> ":" <rpos:@L> <name: Symbol> => syntax::access_slot(body, AST::symbol(name, SourceOffset(rpos)), SourceOffset(pos)),
  <pos:@L> <body: LiteralOrNestedAST> ":" <rpos:@L> <path: NodePath> => syntax::get_node_on(body, AST::string(path, SourceOffset(pos)), SourceOffset(pos)),
  <pos:@L> <path: NodePath> => syntax::get_node_on(AST::symbol("self", SourceOffset(pos)), AST::string(path, SourceOffset(pos)), SourceOffset(pos)),
  <pos:@L> "@" <rpos:@L> <name: Symbol> => syntax::access_slot(AST::symbol("self", SourceOffset(pos)), AST::symbol(name, SourceOffset(rpos)), SourceOffset(pos)),
}

Cdr: AST = {
  <pos:@L> => AST::nil(SourceOffset(pos)),
  "." <cdr: AST> => cdr,
  <pos:@L> <cadr: AST> <cddr: Cdr> => {
    AST::cons(cadr, cddr, SourceOffset(pos))
  }
}

ArrayContents: AST = {
  <pos:@L> => AST::nil(SourceOffset(pos)),
  <pos:@L> <first: AST> <rest: ArrayContents> => {
    AST::cons(first, rest, SourceOffset(pos))
  }
}

DictContents: AST = {
  <pos:@L> => AST::nil(SourceOffset(pos)),
  <pos:@L> <first_key: AST> <mpos:@L> <first_value: AST> <rest: DictContents> => {
    AST::cons(first_key, AST::cons(first_value, rest, SourceOffset(mpos)), SourceOffset(pos))
  }
}
