open Ast
open Ast.Binop

module S = Set.Make(String)

exception Expected of string
exception Unexpected_token of string
exception End_of_tokens

let expected t =
  raise @@ Expected t

let unexpected_token t =
  raise @@ Unexpected_token (Token.to_string t)

(* precedence table.
 * my first thought was using some sort of partially-ordered graph for
 * precedency, but infering precedence relation from the graph is hard
 * and the graph can be made to have loops, I just used plain table. *)
let precedence = [
  Add, 10;
  Sub, 10;
  Mul, 20;
  Div, 20;
  Mod, 30;
  Exp, 30;
] |> List.to_seq |> Hashtbl.of_seq

let precedence_of op =
  Hashtbl.find precedence op

type associativity =
  | Left_to_right
  | Right_to_left

let assoc_of_string = function
  | "left" -> Left_to_right
  | "right" -> Right_to_left
  | _ -> invalid_arg "assoc_of_string"

let assoc_to_string = function
  | Left_to_right -> "left"
  | Right_to_left -> "right"

let oper_assoc = [
  Exp, Right_to_left;
] |> List.to_seq |> Hashtbl.of_seq

let op_is_right_to_left op =
  let a =
    Hashtbl.find_opt oper_assoc op
    |> Option.value ~default: Left_to_right
  in
  a = Right_to_left

let operators = [
  Token.Plus, Add;
  Minus, Sub;
  Asterisk, Mul;
  Slash, Div;
  Carret, Exp;
  Percent, Mod;
] |> List.to_seq |> Hashtbl.of_seq

let token_to_op tok =
  try Hashtbl.find operators tok
  with _ -> failwith "Parser.token_to_op"

let token_is_operator tok =
  Hashtbl.mem operators tok

(* common parsers *)

let token tok seq =
  match seq () with
  | Seq.Nil -> expected @@ Token.to_string tok
  | Seq.Cons (x, seq) ->
    if x = tok then x, seq
    else expected @@ Token.to_string tok

let any_ident seq =
  match seq () with
  | Seq.Nil -> expected "ident"
  | Seq.Cons (x, seq) -> begin match x with
      | Token.Ident id -> id, seq
      | _ -> unexpected_token x
    end

let idents set seq =
  match seq () with
  | Seq.Nil ->
    let msg = "ident " ^ (S.elements set |> String.concat " or ") in
    expected msg
  | Seq.Cons (x, seq) -> begin
      match x with
      | Token.Ident id when S.mem id set -> id, seq
      | _ -> unexpected_token x
    end

let ident str seq =
  idents (S.singleton str) seq

let operator seq =
  match seq () with
  | Seq.Nil -> expected "operator"
  | Seq.Cons (x, seq) ->
    try token_to_op x, seq with
    | _ -> expected "operator"

(* parser combinators *)

let oneof fs seq =
  let rec aux = function
    | [] -> assert false
    | [f] -> f seq
    | f::fs -> (try f seq with _ -> aux fs)
  in
  aux fs

let (@>) f g = fun seq ->
  let a, seq = f seq in
  g a seq

(* expr := level
 *       | assoc
 *       | let
 *       | value binop_right
*)
let rec expr pre seq =
  seq |> oneof [
    level;
    assoc;
    let_value;
    value @> binop pre;
  ]

(* level := "level" {"get" | "set"} [op] *)
and level seq =
  let _, seq = ident "level" seq in
  let id, seq = idents (S.of_list ["get"; "set"]) seq in
  let op, seq = operator seq in
  if id = "get" then
    Get_binop_pre op, seq
  else if id = "set" then
    let v, seq = value seq in
    Set_binop_pre (op, v), seq
  else
    failwith "Parser.level"

(* assoc := "assoc" {"get" | "set"} [op] *)
and assoc seq =
  let _, seq = ident "assoc" seq in
  let id, seq = idents (S.of_list ["get"; "set"]) seq in
  let op, seq = operator seq in
  if id = "get" then
    Get_binop_aso op, seq
  else if id = "set" then
    let a, seq = idents (S.of_list ["left"; "right"]) seq in
    Set_binop_aso (op, a), seq
  else
    failwith "Parser.assoc"

(* let_value := "let" ident "=" expr *)
and let_value seq =
  let _, seq = ident "let" seq in
  let id, seq = any_ident seq in
  let _, seq = token Token.Equal seq in
  let e, seq = expr min_int seq in
  Let (id, e), seq

(* value := int | float | ( expr ) *)
and value seq =
  match seq () with
  | Seq.Nil -> raise End_of_tokens
  | Seq.Cons (x, seq) -> begin match x with
      | Token.Value x -> Value x, seq
      | Ident id -> Var id, seq
      | LParen ->
        let e, seq = expr min_int seq in
        let _, seq = token RParen seq in
        e, seq
      | _ -> unexpected_token x
    end

(* binop := binop op binop *)
and binop pre left seq =
  match seq () with
  | Seq.Nil -> left, Seq.empty
  | Seq.Cons (x, seq) -> begin match x with
      | op when token_is_operator op ->
        let op = token_to_op op in
        let op_pre = precedence_of op in
        (* op has to be calculated first *)
        if op_pre > pre
        || (op_is_right_to_left op && op_pre = pre)
        then
          let right, seq = expr op_pre seq in
          binop pre (Ast.binop left op right) seq
        else
          left, Seq.cons x seq

      | Token.RParen -> left, Seq.cons x seq
      | _ -> unexpected_token x
    end

(* parse tokens *)
let parse ts =
  let ast, rest = expr min_int ts in
  if rest () <> Seq.Nil then failwith "Parser.parse";
  ast