(** Bombs-Must-Detonate: FSM Syntax Extension Compiler @author Brian Go *)


open Ast
exception Fsm_error of string


let raise_fsm_error strlist = raise (Fsm_error (String.concat "" strlist))
let concat s = String.concat "" s

(** A node in the state graph used for connectivity checking *)

type state_node = int * int list * bool ref 
          (** id, target id list, visited flag *)


(** State Graph *)

let node_list = ref []

(** State Id -> State name association *)

let state_env = ref []

(** Returns the state name given an id *)

let get_state_name id = 
  if List.mem_assoc id !state_env then 
    List.assoc id !state_env
  else
    string_of_int id

(** Reverses key/value pairs of an association list *)

let rec reverse_assoc lst = match lst with
    (x,y)::rest -> (y,x)::(reverse_assoc rest)
  | [] -> []

(** Retrieves the state_node object with the given id from the given graph *)

let rec get_state_helper node_list search_id = match node_list with
    (id,targets,visited)::rest when id = search_id -> (id,targets,visited)
  | _::rest -> get_state_helper rest search_id
  | [] -> raise_fsm_error ["State machine error: state ";
                           get_state_name search_id;
                           " has an incoming transition but is not defined."]

(** Retrieves the state_node object with the given id from the globally defined graph *)

let get_state search_id = get_state_helper !node_list search_id

(** Visit all nodes stemming from nodes in node_list *)

let rec visit_graph node_list = 
  match node_list with
      (id,targets,visited)::rest ->
        if !visited then
          visit_graph rest
        else 
          let _ = visited := true in
          let _ = visit_graph (List.map get_state targets) in
            visit_graph rest
    | [] -> ()


(** Check if the graph node_list with root(s) start_nodes is connected *)

let check_connected start_nodes node_list =
  let _ = visit_graph start_nodes in
  let rec helper node_list = match node_list with
      (id,_,visited)::rest -> 
        if !visited then 
          helper rest 
        else 
          raise_fsm_error ["State machine error: state graph is disconnected.  State ";
                           get_state_name id;
                           " is not reachable."]
    | [] -> ()
  in 
    helper node_list


(** Used by make_id() *)

let cur_id = ref (-1);;
(** Returns a unique integer every call *)

let make_id () = let _ = cur_id := !cur_id + 1 in !cur_id

(** Returns the first element of any 2-tuple *)

let rec get_first lst = match lst with
    (x,y)::rest -> x::(get_first rest)
  | [] -> []

(** Gets a string list of state names from a list of state_nodes *)

let rec get_state_names statelist = match statelist with
    (name,_,_)::rest ->
      name::(get_state_names rest)
  | [] -> []

(** Appends the first string to the second *)

let append_string app_str str = 
  concat [str;app_str]

(** Checks if the id is associated with some name in the environment. If it is, returns the name, and then unchanged environment. If it isn't, creates a new ID, adds it to the environment, and returns them *)

let get_id name env = 
  if List.mem_assoc name env then
    List.assoc name env, env
  else
    let id = make_id() in
      id, (name,id)::env

(** Turns a string into a AST variable identifier *)

let varify str = SynVarIdentifier (SynVarName str)

(** Compiles the callback list of a state *)

let rec compile_callbacks callbacks state_name = 
  match callbacks with 
      cur::rest ->
        let field, foo = 
          (match cur with
               SynFsmOnInit s -> "onInit", s 
                    (** Not actually a callback. Called when the state is viisted. *)

             | SynFsmOnMoveRequest s -> "onMoveRequest", s
             | SynFsmOnTeammateDeath s -> "onTeammateDeath", s
             | SynFsmOnBombDetonate s -> "onBombDetonate", s
             | SynFsmOnDeath s -> "onDeath", s)
        in
          (if field = "onInit" then 
               (** Process onInit differently *)

             [ SynVarAssign
                 (SynVarAssignment
                    (SynStructOrEnumValue
                       (SynVarName state_name,
                        "init",
                        ref VIdUnspecified),
                     SynVarIdentifier
                       (SynVarName foo))) ]
           else
             [ SynVarAssign
                 (SynVarAssignment
                    ((SynStructOrEnumValue
                        (SynVarName (concat [state_name;"_callbacks"]),
                         field, ref VIdUnspecified)),
                     (SynVarIdentifier
                        (SynVarName foo)))) ])
          @ compile_callbacks rest state_name
    | [] -> []

(** Compile a transition target of a state. A transition target is a state name with a weight. If there are multiple targets then the individual weight divided by the total weight is the probability that target state is visited next. *)

let compile_transition_target (targ_name,targ_weight) transition_name env = 
  let targ_id, new_env = 
    if List.mem_assoc targ_name env then
      List.assoc targ_name env, env
    else 
      let id = make_id() in
        id, (targ_name,id)::env
  in
  let tuple_name = concat [transition_name;"_target_";string_of_int (make_id())] in
    [ 
     (** declare transition target tuple *)

      SynVarDeclare
        (SynVarDeclareNoInit
           (SynEnumOrStructType
              ("fsmTransitionTarget", ref []),
            tuple_name));
      
      (** set target id *)

      SynVarAssign
        (SynVarAssignment
           (SynStructOrEnumValue
              (SynVarName tuple_name,
               "id",
               ref VIdUnspecified),
            SynValue
              (SynIntValue targ_id)));
      
      (** set the target weight *)

      SynVarAssign
        (SynVarAssignment
           (SynStructOrEnumValue
              (SynVarName tuple_name,
               "weight",
               ref VIdUnspecified),
            SynValue
              (SynFloatValue targ_weight)));
      
      (** add tuple to transition list *)

      SynVarAssign
        (SynVarModify
           (SynStructOrEnumValue
              (SynVarName transition_name,
               "targets",
               ref VIdUnspecified),
            SynBinopCons,
            SynVarIdentifier
              (SynVarName tuple_name))) ], new_env, targ_id
           

(** Compiles all transition targets of a given state. See compile_transition_target *)

let rec compile_transition_targets targs transition_name env = match targs with
    cur::rest -> 
      let cur_result, cur_env, targ_id = compile_transition_target cur transition_name env in 
      let rest_result, rest_env, targ_ids = compile_transition_targets rest transition_name cur_env in
        cur_result @ rest_result, rest_env, targ_id::targ_ids
  | [] -> [], env, []
   

(** Compiles a given transition in a state. A transition is a list of predicates which act on the fsmStateInformation object (see lib_fsm.bmd), all of which much be satisfied to visit (non-deterministaically) one of the transition targets. *)

let compile_transition (preds,targs) state_name env = 
  let name = concat [state_name;"_transition_";string_of_int (make_id())] in
  let predvars = List.map varify preds in
  let target_result, target_env, target_ids = compile_transition_targets targs name env in
    [ 
     (** declare transition *)

      SynVarDeclare
        (SynVarDeclareNoInit
           (SynEnumOrStructType ("fsmTransition",ref []),
            name));
      
      (** assign predicate list *)

      SynVarAssign
        (SynVarAssignment
           (SynStructOrEnumValue
              (SynVarName name,
               "predicates",
               ref VIdUnspecified),
            (SynListValueProducer
               (SynListList predvars))))]
      
      (** compile target list *)

    @ target_result
      
      (** add to transition list *)

    @ [ SynVarAssign
          (SynVarModify
             (SynStructOrEnumValue
                (SynVarName state_name,
                 "transitionList",
                 ref VIdUnspecified),
              SynBinopCons,
              (SynVarIdentifier
                 (SynVarName name)))) ], target_env, target_ids


(** Compiles all transitions of a given state. See compile_transition *)
      
let rec compile_transitions transitions state_name env = 
  match transitions with
      cur::rest -> 
        let cur_result, cur_env, targ_ids = compile_transition cur state_name env in
        let rest_result, rest_env, id_list = compile_transitions rest state_name cur_env in
          cur_result @ rest_result, rest_env, targ_ids @ id_list
    | [] -> [], env, []


(** Compiles all states. A state has an initialization function called every time it is visited, a set of game event callback functions, and transitions to other states. The mechanism of state transition is described in lib_fsm.bmd. *)

let rec compile_states_helper state_list env = match state_list with
    (name, callback_list, transition_list)::rest ->
      let id,new_env = get_id name env in
      let transition_code, transition_env, transition_ids = compile_transitions transition_list name new_env in
      let _ = node_list := (id,transition_ids,ref false)::!node_list in
        [ 
         (** declare state *)

          SynVarDeclare
            (SynVarDeclareNoInit
               (SynEnumOrStructType ("fsmState",ref []),
                name));
          
          (** assign id *)

          SynVarAssign 
            (SynVarAssignment
               (SynStructOrEnumValue
                  (SynVarName name,"id",ref VIdUnspecified),
                SynValue
                  (SynIntValue id)));
          
          (** declare callback set *)

          SynVarDeclare
            (SynVarDeclareNoInit
               (SynEnumOrStructType ("callbackSet",ref []),
                concat [name;"_callbacks"]))]
        @ 
         (** compile callbacks *)

          compile_callbacks callback_list name
        @ [ 
          (** assign callback set *)

          SynVarAssign
            (SynVarAssignment
               (SynStructOrEnumValue
                  (SynVarName name,"callbacks",ref VIdUnspecified),
                SynVarIdentifier
                  (SynVarName (concat [name;"_callbacks"])))) ]
        @ transition_code
        @ compile_states_helper rest transition_env
  | [] -> let _ = state_env := reverse_assoc env in []

(** Produces code to set the state list of the finite state machine to the given state names *)
           
let compile_set_statelist state_names fsm_name =
  let state_name_vars = List.map varify state_names in
    [ SynVarAssign
        (SynVarAssignment
           (SynStructOrEnumValue
              (SynVarName "ret",
               "states",
               ref VIdUnspecified),
            SynListValueProducer
              (SynListList state_name_vars))) ]

(** Compiles the state list and sets the initial state of the FSM. See compile_states_helper. *)

let compile_states state_list fsm_name =
  
  (** Compile all states *)
 
  let states = compile_states_helper state_list [] in
  match state_list with 
      (init_name,_,_)::rest ->
        states
          
          (** Set the initial state *)

        @ [SynVarAssign
             (SynVarAssignment
                (SynStructOrEnumValue
                   (SynVarName "ret","currentState",ref VIdUnspecified),
                 SynVarIdentifier
                   (SynVarName init_name)))]
          
          (** Make the state list and set the fsm field *)

        @ (compile_set_statelist (get_state_names state_list) fsm_name)
    | [] -> raise_fsm_error ["State machine error: No states specified for state machine."]


(** Compiles each state machine described in the AST, leaving other syntax elements untouched. *)

let rec compile_fsm src_prog = 
    match src_prog with
        (SynStateMachine (name,init,state_list))::rest ->
          let _ = node_list := [] in
          let machine_code = 
            
            (** Declare fsm, set the init function *)

            [SynVarDeclare 
               (SynVarDeclareNoInit 
              (SynEnumOrStructType ("finiteStateMachine",ref []),
               "ret"));
             SynVarAssign
               (SynVarAssignment 
                  (SynStructOrEnumValue 
                     (SynVarName "ret","initialize",ref VIdUnspecified),
                   SynVarIdentifier
                     (SynVarName init)))]
              
              (** Compile the states *)

            @ compile_states state_list name
              
              (** Return the state machine *)

            @ [SynReturnStatement 
                 (SynValueReturn 
                    (SynVarIdentifier
                       (SynVarName "ret")))]
          in
          let machine_function = (SynFunctionDefine ((SynEnumOrStructType ("finiteStateMachine",ref []), name, []), machine_code)) in
          let start_node = match List.rev !node_list with
              start::rest -> start
            | [] -> raise_fsm_error ["State mahine error: no states specified."]
          in
          let _ = check_connected [start_node] !node_list in
            (SynInclude (ref (IncludeFileName "lib_fsm.bmd")))::(machine_function::(compile_fsm rest))
      | cur::rest -> cur::(compile_fsm rest)
      | [] -> []