module internal StateMonad

    type Error = 
        | VarExists of string
        | VarNotFound of string
        | IndexOutOfBounds of int
        | DivisionByZero 
        | ReservedName of string           

    type Result<'a, 'b>  =
        | Success of 'a
        | Failure of 'b

    type State = { vars     : Map<string, int> list
                   word     : (char * int) list 
                   reserved : Set<string> }

    type SM<'a> = S of (State -> Result<'a * State, Error>)

    let mkState lst word reserved = 
           { vars = [Map.ofList lst];
             word = word;
             reserved = Set.ofList reserved }

    let evalSM (s : State) (S a : SM<'a>) : Result<'a, Error> =
        match a s with
        | Success (result, _) -> Success result
        | Failure error -> Failure error

    let bind (f : 'a -> SM<'b>) (S a : SM<'a>) : SM<'b> =
        S (fun s ->
              match a s with
              | Success (b, s') -> 
                match f b with 
                | S g -> g s'
              | Failure err     -> Failure err)


    let ret (v : 'a) : SM<'a> = S (fun s -> Success (v, s))
    let fail err     : SM<'a> = S (fun s -> Failure err)

    let (>>=)  x f = bind f x
    let (>>>=) x f = x >>= (fun () -> f)

    let push : SM<unit> = 
        S (fun s -> Success ((), {s with vars = Map.empty :: s.vars}))

    let pop : SM<unit> =
        (S (fun s -> Success((), { s with vars = s.vars.Tail})))

    let wordLength : SM<int> =
        S (fun s -> Success(s.word.Length, s))

    let getPos f pos = S (fun s ->
        if pos < s.word.Length && pos >= 0
        then Success(f <| s.word.Item pos, s)
        else Failure(IndexOutOfBounds pos))
    
    let characterValue (pos : int) : SM<char> =
        getPos fst pos

    let pointValue (pos : int) : SM<int> =
        getPos snd pos

    let lookup (x : string) : SM<int> = 
        let rec aux =
            function
            | []      -> None
            | m :: ms -> 
                match Map.tryFind x m with
                | Some v -> Some v
                | None   -> aux ms

        S (fun s -> 
              match aux (s.vars) with
              | Some v -> Success (v, s)
              | None   -> Failure (VarNotFound x))

    let declare (var : string) : SM<unit> =
        S (fun s ->
            match s.vars, s.reserved with
            | _, res when res.Contains var -> Failure (ReservedName var)
            | vars, _ when vars.Head.ContainsKey var -> Failure (VarExists var)
            | vars, _ -> Success ((), {s with vars = (Map.add var 0 vars.Head)::vars})
        )
    
    let update (var : string) (value : int) : SM<unit> =
        let rec aux lst = function
            | []      -> None
            | m :: ms ->
                match Map.containsKey var m with
                | true  -> Some ((List.rev lst) @ ((Map.add var value m) :: ms))
                | false -> aux (m::lst) ms

        S (fun s -> 
              match aux List.Empty s.vars with
              | Some l -> Success ((), {s with vars = l })
              | None   -> Failure (VarNotFound var))
        
    let print (str : string) (isVar: bool) : SM<unit> =
        let rec aux lst = function
        | []      -> None
        | m :: ms ->
            match Map.containsKey str m with
            | true  -> Some (m.Item str)
            | false -> aux (m::lst) ms
            
        S (fun s ->
          if isVar then
              match aux List.Empty s.vars with
              | Some i -> printf "%d" i
                          Success ((), s)
              | None   -> Failure (VarNotFound str)
          else printf "%s" str
               Success ((), s))