(* We will inline any function that does not call itselt. *)
module Inlining

open AbSyn
open CallGraph

let mutable inlining_ctr = 0 (* for generating fresh variable names *)

let newSuffix () =
    inlining_ctr <- inlining_ctr + 1
    "_I" + string inlining_ctr

let rec inlineInExp (graph : CallGraph)
                    (prog  : TypedProg)
                    (e     : TypedExp) =
    match e with
        | Constant _ -> e
        | StringLit _ -> e
        | ArrayLit (es, t, pos) ->
            ArrayLit (List.map (inlineInExp graph prog) es, t, pos)
        | Var _ -> e
        | Plus (e1, e2, pos) ->
            Plus (inlineInExp graph prog e1,
                  inlineInExp graph prog e2, pos)
        | Minus (e1, e2, pos) ->
            Minus (inlineInExp graph prog e1,
                   inlineInExp graph prog e2, pos)
        | Equal (e1, e2, pos) ->
            Equal (inlineInExp graph prog e1,
                   inlineInExp graph prog e2, pos)
        | Less (e1, e2, pos) ->
            Less (inlineInExp graph prog e1,
                  inlineInExp graph prog e2, pos)
        | If (e1, e2, e3, pos) ->
            If (inlineInExp graph prog e1,
                inlineInExp graph prog e2,
                inlineInExp graph prog e3,
                pos)
        | Apply (fname, es, pos) ->
            if calls fname fname graph then
                (* Function is recursive - do not inline. *)
                Apply (fname, List.map (inlineInExp graph prog) es, pos)
            else (* OK - inline. *)
            inlineFuncall fname graph prog es pos
        | Let (Dec (name, e, decpos), body, pos) ->
            Let (Dec (name, inlineInExp graph prog e, decpos),
                 inlineInExp graph prog body,
                 pos)
        | Index (name, e, t, pos) ->
            Index (name, inlineInExp graph prog e, t, pos)
        | Iota (e, pos) ->
            Iota (e, pos)
        | Map (farg, e, t1, t2, pos) ->
            Map (inlineInFunArg graph prog farg,
                 inlineInExp graph prog e,
                 t1, t2, pos)
        | Filter (farg, e, t1, pos) ->
            Filter (inlineInFunArg graph prog farg,
                    inlineInExp graph prog e,
                    t1, pos)
        | Reduce (farg, e1, e2, t, pos) ->
            Reduce (inlineInFunArg graph prog farg,
                    inlineInExp graph prog e1,
                    inlineInExp graph prog e2,
                    t, pos)
        | Replicate (n, e, t, pos) ->
            Replicate (inlineInExp graph prog n,
                       inlineInExp graph prog e,
                       t, pos)
        | Scan (farg, e1, e2, t, pos) ->
            Scan (inlineInFunArg graph prog farg,
                  inlineInExp graph prog e1,
                  inlineInExp graph prog e2,
                  t, pos)
        | Times (e1, e2, pos) ->
            Times (inlineInExp graph prog e1,
                   inlineInExp graph prog e2,
                   pos)
        | Divide (e1, e2, pos) ->
            Divide (inlineInExp graph prog e1,
                    inlineInExp graph prog e2,
                    pos)
        | And (e1, e2, pos) ->
            And (inlineInExp graph prog e1,
                 inlineInExp graph prog e2,
                 pos)
        | Or (e1, e2, pos) ->
            Or (inlineInExp graph prog e1,
                inlineInExp graph prog e2,
                pos)
        | Not (e, pos) ->
            Not (inlineInExp graph prog e, pos)
        | Negate (e, pos) ->
            Negate (inlineInExp graph prog e, pos)
        | Read (t, pos) ->
            Read (t, pos)
        | Write (e, t, pos) ->
            Write (inlineInExp graph prog e, t, pos)

and inlineInFunArg (graph : CallGraph)
                   (prog  : TypedProg) = function
                       | Lambda (rettype, paramls, body, pos) ->
                           Lambda (rettype, paramls, inlineInExp graph prog body, pos)
                       | FunName fname ->
                           match List.tryFind (fun (FunDec (x, _, _, _, _)) -> x = fname) prog with
                               | None -> FunName fname
                               | Some (FunDec (_, rettype, paramls, body, pos)) ->
                                   inlineInFunArg graph prog (Lambda (rettype, paramls, body, pos))

and inlineFuncall (fname : string)
                  (graph : CallGraph)
                  (prog  : TypedProg)
                  (args  : TypedExp list)
                  (pos   : Position) =
    match List.tryFind (fun (FunDec(x, _, _, _, _)) -> x = fname) prog with
        | None -> Apply (fname, List.map ( inlineInExp graph prog) args, pos)
        | Some (FunDec (_, _, paramls, body, _)) ->
            let parNames = List.map (fun (Param (v,t)) ->  v) paramls
            // let paramBindings = List.zip parNames args (* too simplistic *)
            let uniq = newSuffix () (* can use same suffix for all pars *)
            let parNames1 = List.map (fun v -> v + uniq) parNames
            let paramBindings =
              List.zip parNames1 args @
              List.zip parNames (List.map (fun v -> Var (v,pos)) parNames1)
            let rec mkLetsAroundBody = function
                | [] -> body
                | ((paramname, arg) :: rest) ->
                    Let ( Dec ( paramname, arg, pos),
                          mkLetsAroundBody rest,
                          pos)
            inlineInExp graph prog (mkLetsAroundBody paramBindings)

let inlineInFunction (graph : CallGraph)
                     (prog : TypedProg)
                     (FunDec (fname, rettype, paramls, body, pos)) =
    FunDec (fname, rettype, paramls, inlineInExp graph prog body, pos)

let inlineOptimiseProgram (prog : TypedProg) =
    let graph = callGraph prog
    List.map (inlineInFunction graph prog) prog
