module ProtoFlex.Generator

open System
open System.IO
open System.Text
open Google.Protobuf.Reflection
open ProtoFlex.Extensions

[<Literal>]
let private disclaimer =
    "//------------------------------------------------------------------------------
//     This code was generated by the ProtoFlex tool.
//     Changes to this file will be lost when code is regenerated.
//------------------------------------------------------------------------------"

[<Literal>]
let private imports =
    "open System
open System.Collections.Generic
open System.Runtime.Serialization
open System.ServiceModel
open System.Threading.Tasks
"

[<Literal>]
let private space = "    "

let private sysTypes =
    dict
        [ ".google.protobuf.Empty", null
          ".google.protobuf.Timestamp", "DateTime"
          "google.protobuf.Duration", "TimeSpan" ]

let private split t =
    match sysTypes.TryGetValue t with
    | true, t -> t
    | _ -> t.Split '.' |> Seq.last
    |> Some
    |> Option.filter (String.IsNullOrEmpty >> not)

let rec genType (f: FieldDescriptorProto) =
    (f.label,
     match f.``type`` with
     | FieldDescriptorProto.Type.TypeDouble -> nameof float
     | FieldDescriptorProto.Type.TypeFloat -> nameof float32
     | FieldDescriptorProto.Type.TypeInt64 -> nameof int64
     | FieldDescriptorProto.Type.TypeUint64 -> nameof uint64
     | FieldDescriptorProto.Type.TypeInt32 -> nameof int
     | FieldDescriptorProto.Type.TypeFixed64 -> nameof uint64
     | FieldDescriptorProto.Type.TypeFixed32 -> nameof uint32
     | FieldDescriptorProto.Type.TypeBool -> nameof bool
     | FieldDescriptorProto.Type.TypeString -> nameof string
     | FieldDescriptorProto.Type.TypeGroup -> nameof double
     | FieldDescriptorProto.Type.TypeMessage -> split f.TypeName |> Option.defaultValue "unit"
     | FieldDescriptorProto.Type.TypeBytes -> "byte []"
     | FieldDescriptorProto.Type.TypeUint32 -> nameof uint32
     | FieldDescriptorProto.Type.TypeEnum -> split f.TypeName |> Option.defaultValue "Enum"
     | FieldDescriptorProto.Type.TypeSfixed32 -> nameof int
     | FieldDescriptorProto.Type.TypeSfixed64 -> nameof int64
     | FieldDescriptorProto.Type.TypeSint32 -> nameof int
     | FieldDescriptorProto.Type.TypeSint64 -> nameof int
     | _ -> nameof obj)
    |> function
        | FieldDescriptorProto.Label.LabelRepeated, b -> $"List<{b}>"
        | _, b -> b

let genEnumType (en: EnumDescriptorProto) (sb: StringBuilder) =
    let sb = sb.AppendLine("[<DataContract>]").AppendLine($"type {en.Name} =")

    for op in en.Values do
        sb.AppendLine $"{space}| {op.Name} = {op.Number}" |> ignore


let genMsgType (msg: DescriptorProto) (sb: StringBuilder) =
    let sb =
        sb.AppendLine("[<DataContract; CLIMutable>]").AppendLine($"type {msg.Name} =")

    msg.Fields
    |> Seq.iteri (fun i f ->
        if i = 0 then
            sb.Append $"{space}{{ "
        else
            sb.Append $"{space}  "
        |> ignore

        sb
            .AppendLine($"[<DataMember(Order = {f.Number})>]")
            .Append($"{space}  {f.Name}: {genType f}")
        |> ignore

        if i = msg.Fields.Count - 1 then
            sb.AppendLine " }"
        else
            sb.AppendLine()
        |> ignore)

let genService package (srv: ServiceDescriptorProto) (sb: StringBuilder) =
    let sb =
        sb
            .AppendLine($"[<ServiceContract(Name = \"%s{package}.{srv}\")>]")
            .AppendLine($"type I{srv.Name} =")

    for rpc in srv.Methods do
        let input = split rpc.InputType |> Option.defaultValue "unit"

        let output =
            split rpc.OutputType |> Option.map (sprintf "<%s>") |> Option.defaultValue ""

        sb.AppendLine $"{space}abstract member {rpc.Name}: {input} -> Task{output}"
        |> ignore


let genTemplate (hook: ExtensionHook) (name_space: string option) (protos: list<string * TextReader>) =
    let set = FileDescriptorSet()

    for n, r in protos do
        set.Add(n, source = r) |> ignore

    set.Process()

    match set.GetErrors() with
    | errors when (errors.Length > 0) -> Error errors
    | _ ->
        let name_space = defaultArg name_space set.Files[0].Package

        let sb =
            StringBuilder()
                .AppendLine(disclaimer)
                .AppendLine($"namespace {name_space}\n")
                .AppendLine(imports)

        for file in set.Files do
            for en in file.EnumTypes do
                genEnumType en sb
                GenEnumType en |> hook sb

            for msg in file.MessageTypes do
                if sysTypes.ContainsKey $".{file.Package}.{msg.Name}" |> not then
                    genMsgType msg sb
                    GenMsgType msg |> hook sb

            for srv in file.Services do
                genService file.Package srv sb
                GenService(file.Package, srv) |> hook sb

        Ok(string sb)
