// https://github.com/TheAngryByrd/FsLibLog/blob/64f118ae8df2f2944ef69758052cb3b148b87e79/src/FsLibLog/FsLibLog.fs

namespace FsAutoComplete.Logging

open System.Text.RegularExpressions


[<AutoOpen>]
module Types =
  open System

  type LogLevel =
    | Trace = 0
    | Debug = 1
    | Info = 2
    | Warn = 3
    | Error = 4
    | Fatal = 5

  /// An optional message thunk.
  ///
  /// - If <see cref="T:Microsoft.FSharp.Core.Option<_>.None">None</see> is provided, this typically signals to the logger to do a isEnabled check.
  /// - If <see cref="T:Microsoft.FSharp.Core.Option<_>.Some">Some</see> is provided, this signals the logger to log.
  type MessageThunk = (unit -> string) option

  /// The signature of a log message function
  type Logger = LogLevel -> MessageThunk -> exn option -> obj array -> bool
  type MappedContext = string -> obj -> bool -> IDisposable

  /// Type representing a Log
  [<NoEquality; NoComparison>]
  type Log =
    { LogLevel: LogLevel
      Message: MessageThunk
      Exception: exn option
      Parameters: obj list
      AdditionalNamedParameters: ((string * obj * bool) list) }

    static member StartLogLevel(logLevel: LogLevel) =
      { LogLevel = logLevel
        Message = None
        Exception = None
        Parameters = List.empty
        AdditionalNamedParameters = List.empty }

  /// An interface wrapper for a<see cref="T:FsLibLog.Types.Logger">Logger</see>. Useful when using depedency injection frameworks.
  type ILog =
    abstract member Log: Logger
    abstract member MappedContext: MappedContext

  [<AutoOpen>]
  module Inner =
    open System.Collections.Generic

    /// <summary>
    /// DisposableStack on Dispose will call dispose on items appended to its stack in Last In First Out.
    /// </summary>
    type DisposableStack() =
      let stack = Stack<IDisposable>()

      interface IDisposable with
        member __.Dispose() =
          while stack.Count > 0 do
            stack.Pop().Dispose()

      member __.Push(item: IDisposable) = stack.Push item

      member __.Push(items: IDisposable list) = items |> List.iter stack.Push

      static member Create(items: IDisposable list) =
        let ds = new DisposableStack()
        ds.Push items
        ds

    type ILog with

      /// <summary>
      /// Logs a log
      /// </summary>
      /// <param name="log">The type representing a log message to be logged</param>
      /// <returns><see langword="true"/> if the log message was logged</returns>
      member logger.fromLog(log: Log) =
        use __ =
          log.AdditionalNamedParameters
          |> List.map (fun (key, value, destructure) -> logger.MappedContext key value destructure)
          // This stack is important, it causes us to unwind as if you have multiple uses in a row
          |> DisposableStack.Create

        log.Parameters
        |> List.toArray
        |> logger.Log log.LogLevel log.Message log.Exception

      /// <summary>
      /// Logs a fatal log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      /// <returns><see langword="true"/>  if the log message was logged</returns>
      member logger.fatal'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Fatal |> logConfig |> logger.fromLog

      /// <summary>
      /// Logs a fatal log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      member logger.fatal(logConfig: Log -> Log) = logger.fatal' logConfig |> ignore

      /// <summary>
      /// Logs an error log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      /// <returns><see langword="true"/>  if the log message was logged</returns>
      member logger.error'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Error |> logConfig |> logger.fromLog

      /// <summary>
      /// Logs an error log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      member logger.error(logConfig: Log -> Log) = logger.error' logConfig |> ignore

      /// <summary>
      /// Logs a warn log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      /// <returns><see langword="true"/>  if the log message was logged</returns>
      member logger.warn'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Warn |> logConfig |> logger.fromLog

      /// <summary>
      /// Logs a warn log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      member logger.warn(logConfig: Log -> Log) = logger.warn' logConfig |> ignore

      /// <summary>
      /// Logs an info log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      /// <returns><see langword="true"/>  if the log message was logged</returns>
      member logger.info'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Info |> logConfig |> logger.fromLog

      /// <summary>
      /// Logs an info log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      member logger.info(logConfig: Log -> Log) = logger.info' logConfig |> ignore

      /// <summary>
      /// Logs a debug log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      /// <returns><see langword="true"/>  if the log message was logged</returns>
      member logger.debug'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Debug |> logConfig |> logger.fromLog

      /// <summary>
      /// Logs a debug log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      member logger.debug(logConfig: Log -> Log) = logger.debug' logConfig |> ignore

      /// <summary>
      /// Logs a trace log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      /// <returns><see langword="true"/>  if the log message was logged</returns>
      member logger.trace'(logConfig: Log -> Log) = Log.StartLogLevel LogLevel.Trace |> logConfig |> logger.fromLog

      /// <summary>
      /// Logs a trace log message given a log configurer.
      /// </summary>
      /// <param name="logConfig">A function to configure a log</param>
      member logger.trace(logConfig: Log -> Log) = logger.trace' logConfig |> ignore


  /// An interface for retrieving a concrete logger such as Serilog, Nlog, etc.
  type ILogProvider =
    abstract member GetLogger: string -> Logger
    abstract member OpenNestedContext: string -> IDisposable
    abstract member OpenMappedContext: string -> obj -> bool -> IDisposable

  module Log =

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a message.
    /// </summary>
    /// <param name="message">The message to set for the log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let setMessage (message: string) (log: Log) =
      { log with
          Message = Some(fun () -> message) }

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a message thunk.  Useful for "expensive" string construction scenarios.
    /// </summary>
    /// <param name="messageThunk">The function that generates a message to add to a Log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let setMessageThunk (messageThunk: unit -> string) (log: Log) = { log with Message = Some messageThunk }

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a parameter.
    /// </summary>
    /// <param name="param">The value to add to the log</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let addParameter (param: 'a) (log: Log) =
      { log with
          Parameters = List.append log.Parameters [ (box param) ] }

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a list of parameters.
    /// </summary>
    /// <param name="params">The values to add to the log, in the form of an `obj list`.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let addParameters (``params``: obj list) (log: Log) =
      let ``params`` = ``params`` |> List.map box

      { log with
          Parameters = log.Parameters @ ``params`` }


    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with additional named parameters for context. This helper adds more context to a log.
    /// This DOES NOT affect the parameters set for a message template.
    /// This is the same calling OpenMappedContext right before logging.
    /// </summary>
    /// <param name="key">The key of the parameter to add to the log.</param>
    /// <param name="value">The value of the parameter to add to the log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let addContext (key: string) (value: obj) (log: Log) =
      { log with
          AdditionalNamedParameters = List.append log.AdditionalNamedParameters [ key, (box value), false ] }


    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with additional named parameters for context. This helper adds more context to a log.
    /// This DOES NOT affect the parameters set for a message template.
    /// This is the same calling OpenMappedContext right before logging.
    /// This destructures an object rather than calling `ToString()` on it.
    /// WARNING: Destructring can be expensive.
    /// </summary>
    /// <param name="key">The key of the parameter to add to the log.</param>
    /// <param name="value">The value of the parameter to add to the log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let addContextDestructured (key: string) (value: obj) (log: Log) =
      { log with
          AdditionalNamedParameters = List.append log.AdditionalNamedParameters [ key, (box value), true ] }


    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with an <see cref="T:System.Exception">exn</see>. Handles nulls.
    /// </summary>
    /// <param name="exception">The exception to add to the log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let addException (``exception``: exn) (log: Log) =
      { log with
          Exception = Option.ofObj ``exception`` }

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with an <see cref="T:System.Exception">exn</see>. Handles nulls.
    /// </summary>
    /// <param name="exception">The exception to add to the log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let addExn (``exception``: exn) (log: Log) = addException ``exception`` log

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a given <see cref="T:FsLibLog.Types.LogLevel">LogLevel</see>
    /// </summary>
    /// <param name="logLevel">The level to set for the log.</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let setLogLevel (logLevel: LogLevel) (log: Log) = { log with LogLevel = logLevel }

    let private formatterRegex =
      Regex(@"(?<!{){(?<number>\d+)(?<columnFormat>:(?<format>[^}]+))?}(?!})", RegexOptions.Compiled)

    let private isAnObject value = Convert.GetTypeCode(value) = TypeCode.Object

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a given interpolated string. This will generate a message template from a special syntax within the interpolation. The syntax for the interplated string is <code> $"I want to log {myVariable:MyLogVariableName}". </code>
    ///
    /// This would be equivalent of calling <code>(setMessage "I want to log {MyLogVariableName}" >> addContextDestructured "MyLogVariable" myVariable)</code>
    /// </summary>
    /// <param name="message">An interpolated string</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let setMessageInterpolated (message: FormattableString) (log: Log) =
      let mutable messageFormat = message.Format

      let args =
        formatterRegex.Matches(messageFormat)
        |> Seq.cast<Match>
        |> Seq.map (fun m ->
          let number = Int32.Parse(m.Groups.["number"].Value)
          let formatGroup = m.Groups.["format"]
          let propertyValue = message.GetArgument(number)
          let propertyName = formatGroup.Value
          let columnFormatGroup = m.Groups.["columnFormat"]
          propertyName, propertyValue, columnFormatGroup.Index, columnFormatGroup.Length)
      // Reverse the args so we won't change the indexes earlier in the string
      args
      |> Seq.rev
      |> Seq.iter (fun (_, _, removeStart, removeLength) ->
        if removeLength > 0 then
          messageFormat <- messageFormat.Remove(removeStart, removeLength))

      let namedArgs =
        args |> Seq.map (fun (name, _, _, _) -> box $"{{{name}}}") |> Seq.toArray

      messageFormat <- messageFormat.Replace("{{", "{{{{").Replace("}}", "}}}}")
      // Replace numbered args with named args from regex match
      messageFormat <- String.Format(messageFormat, args = namedArgs)

      let addContexts args (log: Log) =
        let addArgsToContext =
          (id, args)
          ||> Seq.fold (fun state (name, value, _, _) ->
            let contextAdder =
              if value |> isAnObject then
                addContextDestructured
              else
                addContext

            state >> contextAdder name value)

        addArgsToContext log

      log |> setMessage messageFormat |> addContexts args

    /// <summary>
    /// Amends a <see cref="T:FsLibLog.Types.Log">Log</see> with a given interpolated string. This will generate a message template from a special syntax within the interpolation. The syntax for the interplated string is <code> $"I want to log {myVariable:MyLogVariableName}". </code>
    ///
    /// This would be equivalent of calling <code>(setMessage "I want to log {MyLogVariableName}" >> addContextDestructured "MyLogVariable" myVariable)</code>
    /// </summary>
    /// <param name="message">An interpolated string</param>
    /// <param name="log">The log to amend.</param>
    /// <returns>The amended log.</returns>
    let setMessageI (message: FormattableString) (log: Log) = setMessageInterpolated message log

/// Provides operators to make writing logs more streamlined.
module Operators =

  /// <summary>
  /// Amend a log with a message. Wrapper for <see cref="M:FsLibLog.Types.LogModule.setMessage">Log.setMessage</see>.
  /// </summary>
  /// <param name="message">The string of the base message.</param>
  /// <returns>A new Log instance with the specified message.</returns>
  let (!!!) message = Log.setMessage message

  /// <summary>
  /// Amends a log with a parameter. Wrapper for <see cref="M:FsLibLog.Types.LogModule.addParameter">Log.addParameter</see>.
  /// </summary>
  /// <param name="log">The Log to add the parameter to.</param>
  /// <param name="value">The value for the parameter.</param>
  /// <returns>The Log with the added parameter.</returns>
  let (>>!) log value = log >> Log.addParameter value

  /// <summary>
  /// Amends a Log with additional named parameters for context. This helper adds more context to a log.
  /// This DOES NOT affect the parameters set for a message template. This is the same calling OpenMappedContext right before logging.
  ///
  /// Wrapper for <see cref="M:FsLibLog.Types.LogModule.addContext">Log.addContext</see>.
  /// </summary>
  /// <param name="log">The log to add the parameter to.</param>
  /// <param name="key">The name for the parameter.</param>
  /// <param name="value">The value for the parameter.</param>
  /// <returns>The amended log with the parameter added.</returns>
  let (>>!-) log (key, value) = log >> Log.addContext key value

  /// <summary>
  /// Amends a Log with additional named parameters for context. This helper adds more context to a log. This DOES NOT affect the parameters set for a message template.
  /// This is the same calling OpenMappedContext right before logging. This destructures an object rather than calling ToString() on it. WARNING: Destructring can be expensive.
  ///
  /// Wrapper for <see cref="M:FsLibLog.Types.LogModule.addContextDestructured">Log.addContextDestructured</see>.
  /// </summary>
  /// <param name="log">The log to add the parameter to.</param>
  /// <param name="key">The name for the parameter.</param>
  /// <param name="value">The value for the parameter.</param>
  /// <returns>The amended log with the parameter added.</returns>
  let (>>!+) log (key, value) = log >> Log.addContextDestructured key value

  /// <summary>
  /// Amends a Log with an exn. Handles nulls.
  ///
  /// Wrapper for <see cref="M:FsLibLog.Types.LogModule.addException">Log.addException</see>.
  /// </summary>
  /// <param name="log">The log to add the parameter to.</param>
  /// <param name="e">The exception to add to the log.</param>
  /// <returns>The amended log with the parameter added.</returns>
  let (>>!!) log e = log >> Log.addException e

module Providers =
  module SerilogProvider =
    open System
    open System.Linq.Expressions

    let getLogManagerType () = Type.GetType("Serilog.Log, Serilog")

    let isAvailable () = getLogManagerType () |> isNull |> not

    let getPushProperty () =

      let ndcContextType =
        Type.GetType("Serilog.Context.LogContext, Serilog")
        |> Option.ofObj
        |> Option.defaultWith (fun () -> Type.GetType("Serilog.Context.LogContext, Serilog.FullNetFx"))

      ()

      let pushPropertyMethod =
        ndcContextType.GetMethod("PushProperty", [| typedefof<string>; typedefof<obj>; typedefof<bool> |])

      let nameParam = Expression.Parameter(typedefof<string>, "name")

      let valueParam = Expression.Parameter(typedefof<obj>, "value")

      let destructureObjectParam =
        Expression.Parameter(typedefof<bool>, "destructureObjects")

      let pushPropertyMethodCall =
        Expression.Call(null, pushPropertyMethod, nameParam, valueParam, destructureObjectParam)

      let pushProperty =
        Expression
          .Lambda<Func<string, obj, bool, IDisposable>>(
            pushPropertyMethodCall,
            nameParam,
            valueParam,
            destructureObjectParam
          )
          .Compile()

      fun key value destructure -> pushProperty.Invoke(key, value, destructure)


    let getForContextMethodCall () =
      let logManagerType = getLogManagerType ()

      let method =
        logManagerType.GetMethod("ForContext", [| typedefof<string>; typedefof<obj>; typedefof<bool> |])

      let propertyNameParam = Expression.Parameter(typedefof<string>, "propertyName")

      let valueParam = Expression.Parameter(typedefof<obj>, "value")

      let destructureObjectsParam =
        Expression.Parameter(typedefof<bool>, "destructureObjects")

      let exrs: Expression[] =
        [| propertyNameParam; valueParam; destructureObjectsParam |]

      let methodCall = Expression.Call(null, method, exrs)

      let func =
        Expression
          .Lambda<Func<string, obj, bool, obj>>(methodCall, propertyNameParam, valueParam, destructureObjectsParam)
          .Compile()

      fun name -> func.Invoke("SourceContext", name, false)

    [<NoEquality; NoComparison>]
    type SerilogGateway =
      { Write: obj -> obj -> string -> obj[] -> unit
        WriteException: obj -> obj -> exn -> string -> obj[] -> unit
        IsEnabled: obj -> obj -> bool
        TranslateLevel: LogLevel -> obj }

      static member Create() =
        let logEventLevelType = Type.GetType("Serilog.Events.LogEventLevel, Serilog")

        if (logEventLevelType |> isNull) then
          failwith ("Type Serilog.Events.LogEventLevel was not found.")

        let debugLevel = Enum.Parse(logEventLevelType, "Debug", false)

        let errorLevel = Enum.Parse(logEventLevelType, "Error", false)

        let fatalLevel = Enum.Parse(logEventLevelType, "Fatal", false)

        let informationLevel = Enum.Parse(logEventLevelType, "Information", false)

        let verboseLevel = Enum.Parse(logEventLevelType, "Verbose", false)

        let warningLevel = Enum.Parse(logEventLevelType, "Warning", false)

        let translateLevel (level: LogLevel) =
          match level with
          | LogLevel.Fatal -> fatalLevel
          | LogLevel.Error -> errorLevel
          | LogLevel.Warn -> warningLevel
          | LogLevel.Info -> informationLevel
          | LogLevel.Debug -> debugLevel
          | LogLevel.Trace -> verboseLevel
          | _ -> debugLevel

        let loggerType = Type.GetType("Serilog.ILogger, Serilog")

        if (loggerType |> isNull) then
          failwith ("Type Serilog.ILogger was not found.")

        let isEnabledMethodInfo = loggerType.GetMethod("IsEnabled", [| logEventLevelType |])

        let instanceParam = Expression.Parameter(typedefof<obj>)

        let instanceCast = Expression.Convert(instanceParam, loggerType)

        let levelParam = Expression.Parameter(typedefof<obj>)

        let levelCast = Expression.Convert(levelParam, logEventLevelType)

        let isEnabledMethodCall =
          Expression.Call(instanceCast, isEnabledMethodInfo, levelCast)

        let isEnabled =
          Expression
            .Lambda<Func<obj, obj, bool>>(isEnabledMethodCall, instanceParam, levelParam)
            .Compile()

        let writeMethodInfo =
          loggerType.GetMethod("Write", [| logEventLevelType; typedefof<string>; typedefof<obj[]> |])

        let messageParam = Expression.Parameter(typedefof<string>)
        let propertyValuesParam = Expression.Parameter(typedefof<obj[]>)

        let writeMethodExp =
          Expression.Call(instanceCast, writeMethodInfo, levelCast, messageParam, propertyValuesParam)

        let expression =
          Expression.Lambda<Action<obj, obj, string, obj[]>>(
            writeMethodExp,
            instanceParam,
            levelParam,
            messageParam,
            propertyValuesParam
          )

        let write = expression.Compile()

        let writeExceptionMethodInfo =
          loggerType.GetMethod("Write", [| logEventLevelType; typedefof<exn>; typedefof<string>; typedefof<obj[]> |])

        let exceptionParam = Expression.Parameter(typedefof<exn>)

        let writeMethodExp =
          Expression.Call(
            instanceCast,
            writeExceptionMethodInfo,
            levelCast,
            exceptionParam,
            messageParam,
            propertyValuesParam
          )

        let writeException =
          Expression
            .Lambda<Action<obj, obj, exn, string, obj[]>>(
              writeMethodExp,
              instanceParam,
              levelParam,
              exceptionParam,
              messageParam,
              propertyValuesParam
            )
            .Compile()

        { Write =
            (fun logger level message formattedParmeters -> write.Invoke(logger, level, message, formattedParmeters))
          WriteException =
            fun logger level ex message formattedParmeters ->
              writeException.Invoke(logger, level, ex, message, formattedParmeters)
          IsEnabled = fun logger level -> isEnabled.Invoke(logger, level)
          TranslateLevel = translateLevel }

    type private SeriLogProvider() =
      let getLoggerByName = getForContextMethodCall ()
      let pushProperty = getPushProperty ()
      let serilogGatewayInit = lazy (SerilogGateway.Create())

      let writeMessage logger logLevel (messageFunc: MessageThunk) ``exception`` formatParams =
        let serilogGateway = serilogGatewayInit.Value
        let translatedValue = serilogGateway.TranslateLevel logLevel

        match messageFunc with
        | None -> serilogGateway.IsEnabled logger translatedValue
        | Some _ when serilogGateway.IsEnabled logger translatedValue |> not -> false
        | Some m ->
          match ``exception`` with
          | Some ex -> serilogGateway.WriteException logger translatedValue ex (m ()) formatParams
          | None -> serilogGateway.Write logger translatedValue (m ()) formatParams

          true

      interface ILogProvider with
        member this.GetLogger(name: string) : Logger = getLoggerByName name |> writeMessage

        member this.OpenMappedContext (key: string) (value: obj) (destructure: bool) : IDisposable =
          pushProperty key value destructure

        member this.OpenNestedContext(message: string) : IDisposable = pushProperty "NDC" message false

    let create () = SeriLogProvider() :> ILogProvider


  module MicrosoftExtensionsLoggingProvider =
    open System
    open System.Linq.Expressions
    open System.Reflection
    open System.Collections.Generic

    type ILoggerFactory = obj
    // This has to be set from usercode for this to light up
    let mutable private microsoftLoggerFactory: ILoggerFactory option = None

    let setMicrosoftLoggerFactory (factory: ILoggerFactory) = microsoftLoggerFactory <- Option.ofObj factory

    let getLogFactoryType =
      lazy (Type.GetType("Microsoft.Extensions.Logging.ILoggerFactory, Microsoft.Extensions.Logging.Abstractions"))

    let isAvailable () =
      getLogFactoryType.Value |> isNull |> not
      && microsoftLoggerFactory |> Option.isSome


    type ILogger = obj
    type LoggerName = string
    type MicrosoftLogLevel = obj
    type MessageFormat = string
    type MessageArgs = obj array

    [<NoEquality; NoComparison>]
    type LoggerFactoryGateway =
      { CreateLogger: ILoggerFactory -> LoggerName -> ILogger }

      static member Create() =
        let createLogger =
          let factoryType = getLogFactoryType.Value

          let createLoggerMethodInfo =
            factoryType.GetMethod("CreateLogger", [| typedefof<string> |])

          let instanceParam = Expression.Parameter(typedefof<ILoggerFactory>)
          let nameParam = Expression.Parameter(typedefof<string>)
          let instanceCast = Expression.Convert(instanceParam, factoryType)

          let createLoggerMethodExp =
            Expression.Call(instanceCast, createLoggerMethodInfo, nameParam)

          let createLogger =
            Expression
              .Lambda<Func<ILoggerFactory, string, ILogger>>(createLoggerMethodExp, instanceParam, nameParam)
              .Compile()

          createLogger |> FuncConvert.FromFunc

        { CreateLogger = createLogger }

    type LoggerGateway =
      { Write: ILogger -> MicrosoftLogLevel -> MessageFormat -> MessageArgs -> unit
        WriteError: ILogger -> MicrosoftLogLevel -> exn -> MessageFormat -> MessageArgs -> unit
        IsEnabled: ILogger -> MicrosoftLogLevel -> bool
        TranslateLevel: LogLevel -> MicrosoftLogLevel
        BeginScope: ILogger -> obj -> IDisposable }

      static member Create() =
        let loggerExtensions =
          Type.GetType("Microsoft.Extensions.Logging.LoggerExtensions, Microsoft.Extensions.Logging.Abstractions")

        let loggerType =
          Type.GetType("Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.Abstractions")

        let logEventLevelType =
          Type.GetType("Microsoft.Extensions.Logging.LogLevel, Microsoft.Extensions.Logging.Abstractions")

        let instanceParam = Expression.Parameter(typedefof<ILogger>)

        let instanceCast = Expression.Convert(instanceParam, loggerType)
        let levelParam = Expression.Parameter(typedefof<MicrosoftLogLevel>)

        let levelCast = Expression.Convert(levelParam, logEventLevelType)

        let isEnabled =
          let isEnabledMethodInfo = loggerType.GetMethod("IsEnabled", [| logEventLevelType |])

          let isEnabledMethodCall =
            Expression.Call(instanceCast, isEnabledMethodInfo, levelCast)


          Expression
            .Lambda<Func<ILogger, MicrosoftLogLevel, bool>>(isEnabledMethodCall, instanceParam, levelParam)
            .Compile()
          |> FuncConvert.FromFunc

        let write, writeError =
          let messageParam = Expression.Parameter(typedefof<MessageFormat>)
          let propertyValuesParam = Expression.Parameter(typedefof<MessageArgs>)

          let write =
            let writeMethodInfo =
              loggerExtensions.GetMethod(
                "Log",
                BindingFlags.Static ||| BindingFlags.Public,
                null,
                [| loggerType
                   logEventLevelType
                   typedefof<MessageFormat>
                   typedefof<MessageArgs> |],
                null
              )

            let writeMethodExp =
              Expression.Call(null, writeMethodInfo, instanceCast, levelCast, messageParam, propertyValuesParam)

            let expression =
              Expression.Lambda<Action<ILogger, MicrosoftLogLevel, MessageFormat, MessageArgs>>(
                writeMethodExp,
                instanceParam,
                levelParam,
                messageParam,
                propertyValuesParam
              )

            expression.Compile() |> FuncConvert.FromAction


          let writeError =
            let writeMethodInfo =
              loggerExtensions.GetMethod(
                "Log",
                BindingFlags.Static ||| BindingFlags.Public,
                null,
                [| loggerType
                   logEventLevelType
                   typedefof<exn>
                   typedefof<MessageFormat>
                   typedefof<MessageArgs> |],
                null

              )

            let exnParam = Expression.Parameter(typedefof<exn>)

            let writeMethodExp =
              Expression.Call(
                null,
                writeMethodInfo,
                instanceCast,
                levelCast,
                exnParam,
                messageParam,
                propertyValuesParam
              )

            let expression =
              Expression.Lambda<Action<ILogger, MicrosoftLogLevel, exn, MessageFormat, MessageArgs>>(
                writeMethodExp,
                instanceParam,
                levelParam,
                exnParam,
                messageParam,
                propertyValuesParam
              )

            expression.Compile() |> FuncConvert.FromAction

          write, writeError

        let translateLevel =

          let debugLevel = Enum.Parse(logEventLevelType, "Debug", false)

          let errorLevel = Enum.Parse(logEventLevelType, "Error", false)

          let criticalLevel = Enum.Parse(logEventLevelType, "Critical", false)

          let informationLevel = Enum.Parse(logEventLevelType, "Information", false)

          let traceLevel = Enum.Parse(logEventLevelType, "Trace", false)

          let warningLevel = Enum.Parse(logEventLevelType, "Warning", false)

          fun (level: LogLevel) ->
            match level with
            | LogLevel.Fatal -> criticalLevel
            | LogLevel.Error -> errorLevel
            | LogLevel.Warn -> warningLevel
            | LogLevel.Info -> informationLevel
            | LogLevel.Debug -> debugLevel
            | LogLevel.Trace -> traceLevel
            | _ -> debugLevel

        let beginScope =
          let beginScopeMethodInfo =
            loggerType.GetMethod("BeginScope").MakeGenericMethod(typedefof<obj>)

          let stateParam = Expression.Parameter(typedefof<obj>)

          let beginScopeMethodCall =
            Expression.Call(instanceCast, beginScopeMethodInfo, stateParam)

          Expression
            .Lambda<Func<ILogger, obj, IDisposable>>(beginScopeMethodCall, instanceParam, stateParam)
            .Compile()
          |> FuncConvert.FromFunc

        { Write = write
          WriteError = writeError
          IsEnabled = isEnabled
          TranslateLevel = translateLevel
          BeginScope = beginScope }


    type private MicrosoftProvider() =
      let factoryGateway = lazy (LoggerFactoryGateway.Create())
      let loggerGateway = lazy (LoggerGateway.Create())

      interface ILogProvider with
        member this.GetLogger(name: string) : Logger =
          match microsoftLoggerFactory with
          | None -> fun _ _ _ _ -> false
          | Some factory ->
            let logger = factoryGateway.Value.CreateLogger factory name

            fun logLevel message exn args ->
              let microsoftLevel = loggerGateway.Value.TranslateLevel logLevel

              match message with
              | Some message ->
                let message = message ()

                match exn with
                | Some ex -> loggerGateway.Value.WriteError logger microsoftLevel ex message args
                | None -> loggerGateway.Value.Write logger microsoftLevel message args

                true
              | None -> loggerGateway.Value.IsEnabled logger microsoftLevel

        member this.OpenMappedContext (key: string) (value: obj) (_destructure: bool) : IDisposable =
          match microsoftLoggerFactory with
          | None ->
            { new IDisposable with
                member x.Dispose() = () }
          | Some factory ->
            // Create bogus logger that will propagate to a real logger later
            let logger = factoryGateway.Value.CreateLogger factory (Guid.NewGuid().ToString())
            // Requires a IEnumerable<KeyValuePair> to make sense
            // https://nblumhardt.com/2016/11/ilogger-beginscope/
            [ KeyValuePair(key, value) ] |> box |> loggerGateway.Value.BeginScope logger


        member this.OpenNestedContext(message: string) : IDisposable =
          match microsoftLoggerFactory with
          | None ->
            { new IDisposable with
                member x.Dispose() = () }
          | Some factory ->
            // Create bogus logger that will propagate to a real logger later
            let logger = factoryGateway.Value.CreateLogger factory (Guid.NewGuid().ToString())

            loggerGateway.Value.BeginScope logger (box message)

    let create () = MicrosoftProvider() :> ILogProvider



module LogProvider =
  open System
  open Types
  open Providers
  open System.Diagnostics
  open Microsoft.FSharp.Quotations.Patterns

  let mutable private currentLogProvider = None

  let private knownProviders =
    [ (SerilogProvider.isAvailable, SerilogProvider.create)
      (MicrosoftExtensionsLoggingProvider.isAvailable, MicrosoftExtensionsLoggingProvider.create) ]

  /// Greedy search for first available LogProvider. Order of known providers matters.
  let private resolvedLogger =
    lazy
      (knownProviders
       |> Seq.tryFind (fun (isAvailable, _) -> isAvailable ())
       |> Option.map (fun (_, create) -> create ()))

  let private noopLogger _ _ _ _ = false

  let private noopDisposable =
    { new IDisposable with
        member __.Dispose() = () }

  /// <summary>
  /// Allows custom override when a <c>getLogger</c> function searches for a LogProvider.
  /// </summary>
  /// <param name="logProvider">The <see cref="M:FsLibLog.Types.ILogProvider"/> to set</param>
  /// <returns></returns>
  let setLoggerProvider (logProvider: ILogProvider) = currentLogProvider <- Some logProvider

  /// <summary>
  /// Gets the currently set LogProvider or attempts to find known built in providers
  /// </summary>
  /// <returns></returns>
  let getCurrentLogProvider () =
    match currentLogProvider with
    | None -> resolvedLogger.Value
    | Some p -> Some p

  /// <summary>
  /// Opens a mapped diagnostic context.  This will allow you to set additional parameters to a log given a scope.
  /// </summary>
  /// <param name="key">The name of the property.</param>
  /// <param name="value">The value of the property.</param>
  /// <param name="destructureObjects">If true, and the value is a non-primitive, non-array type, then the value will be converted to a structure; otherwise, unknown types will be converted to scalars, which are generally stored as strings. WARNING: Destructring can be expensive.</param>
  /// <returns>An IDisposable upon disposing will remove this value from a loggers scope</returns>
  let openMappedContextDestucturable (key: string) (value: obj) (destructureObjects: bool) =
    let provider = getCurrentLogProvider ()

    match provider with
    | Some p -> p.OpenMappedContext key value destructureObjects
    | None -> noopDisposable


  /// <summary>
  /// Opens a mapped diagnostic context.  This will allow you to set additional parameters to a log given a scope. Sets destructureObjects to false.
  /// </summary>
  /// <param name="key">The name of the property.</param>
  /// <param name="value">The value of the property.</param>
  /// <returns>An IDisposable upon disposing will remove this value from a loggers scope</returns>
  let openMappedContext (key: string) (value: obj) =
    //TODO: We should try to find out if the value is a primitive
    openMappedContextDestucturable key value false


  /// <summary>
  /// Opens a nested diagnostic context.  This will allow you to set additional parameters to a log given a scope.
  /// </summary>
  /// <param name="value">The value of the property</param>
  /// <returns>An IDisposable upon disposing will remove this value from a loggers scope</returns>
  let openNestedContext (value: string) =
    let provider = getCurrentLogProvider ()

    match provider with
    | Some p -> p.OpenNestedContext value
    | None -> noopDisposable

  /// <summary>
  /// Creates a logger given a <see cref="T:System.String">string</see>. This will attempt to retrieve any loggers set with <see cref="M:FsLibLog.LogProviderModule.setLoggerProvider">Log.setLoggerProvider</see>.  It will fallback to a known list of providers.
  /// </summary>
  /// <param name="name">A name to give a logger. This can help you identify the location of where the log occurred upon reviewing the logs.</param>
  /// <returns></returns>
  let getLoggerByName (name: string) =
    let loggerProvider = getCurrentLogProvider ()

    let logFunc =
      match loggerProvider with
      | Some loggerProvider -> loggerProvider.GetLogger(name)
      | None -> noopLogger

    { new ILog with
        member x.Log = logFunc
        member x.MappedContext = openMappedContextDestucturable }

  /// <summary>
  /// Creates a logger given a <see cref="T:System.Type">Type</see>.  This will attempt to retrieve any loggers set with <see cref="M:FsLibLog.LogProviderModule.setLoggerProvider">Log.setLoggerProvider</see>.  It will fallback to a known list of providers.
  /// </summary>
  /// <param name="objectType">The type to generate a logger name from. </param>
  /// <returns></returns>
  let getLoggerByType (objectType: Type) = objectType |> string |> getLoggerByName

  /// <summary>
  /// Creates a logger given a <c>'a</c> type. This will attempt to retrieve any loggers set with <see cref="M:FsLibLog.LogProviderModule.setLoggerProvider">Log.setLoggerProvider</see>.  It will fallback to a known list of providers.
  /// </summary>
  /// <typeparam name="'a">The type to generate a name from.</typeparam>
  /// <returns></returns>
  let inline getLoggerFor<'a> () = getLoggerByType (typeof<'a>)

  let rec private getModuleType =
    function
    | PropertyGet(_, propertyInfo, _) -> propertyInfo.DeclaringType
    // | Call (_, methInfo, _) -> sprintf "%s.%s" methInfo.DeclaringType.FullName methInfo.Name
    // | Lambda(_, expr) -> getModuleType expr
    // | ValueWithName(_,_,instance) -> instance
    | x -> failwithf "Expression is not a property. %A" x


  /// <summary>
  /// Creates a logger given a Quotations.Expr type. This is only useful for module level declarations. It uses the DeclaringType on the PropertyInfo of the PropertyGet.
  ///
  /// It can be utilized like:
  ///
  /// <code>
  /// let rec logger = LogProvider.getLoggerByQuotation &lt;@ logger @&gt;
  /// </code>
  ///
  /// inside a module to get the modules full qualitfied name.
  /// </summary>
  /// <param name="quotation">The quotation to generate a logger name from.</param>
  /// <returns></returns>
  let getLoggerByQuotation (quotation: Quotations.Expr) = getModuleType quotation |> getLoggerByType


type LogProvider =
  /// <summary>
  /// Creates a logger based on `Reflection.MethodBase.GetCurrentMethod().FullName` and `CallerMemberName`. This is only useful for calls within functions. Results may vary on lambda and inlined functions.
  /// </summary>
  /// <param name="memberName">Do not pass anything to this parameter to get `CallerMemberName` to work.</param>
  /// <returns></returns>
  static member inline getLoggerByFunc([<System.Runtime.CompilerServices.CallerMemberName>] ?memberName: string) =
    let mi = System.Reflection.MethodBase.GetCurrentMethod()
    // When we're in a CE we get something like `WebBackend.App+thingsToCall2@130`.
    // CallerMemberName gets us the function that actually called it
    // Splitting off + seems like the best option to get the Fully Qualified Path
    let location =
      mi.DeclaringType.FullName.Split('+') |> Seq.tryHead |> Option.defaultValue ""

    sprintf "%s.%s" location memberName.Value |> LogProvider.getLoggerByName
