//===--------------------------- OSLogFloatFormatting.swift ------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===------------------------------------------------------------------------------===//

// This file defines types and functions for specifying formatting of
// floating-point typed interpolations passed to the os log APIs.

@frozen
public struct OSLogFloatFormatting {
  /// When set, a `+` will be printed for all non-negative floats.
  @usableFromInline
  internal var explicitPositiveSign: Bool

  /// Whether to use uppercase letters to represent numerals greater than 9
  /// (default is to use lowercase). This applies to hexadecimal digits, NaN, Inf,
  /// the symbols E and X used to denote exponent and hex format.
  @usableFromInline
  internal var uppercase: Bool

  // Note: includePrefix is not supported for FloatFormatting. The format specifier %a
  // always prints a prefix, %efg don't need one.

  /// Number of digits to display following the radix point. Hex notation does not accept
  /// a precision. For non-hex notations, precision can be a dynamic value. The default
  /// precision is 6 for non-hex notations.
  @usableFromInline
  internal var precision: (() -> Int)?

  @usableFromInline
  internal enum Notation {
    /// Hexadecimal formatting.
    case hex

    /// fprintf's `%f` formatting.
    ///
    /// Prints all digits before the radix point, and `precision` digits following
    /// the radix point. If `precision` is zero, the radix point is omitted.
    ///
    /// Note that very large floating-point values may print quite a lot of digits
    /// when using this format, even if `precision` is zero--up to hundreds for
    /// `Double`, and thousands for `Float80`. Note also that this format is
    /// very likely to print non-zero values as all-zero. If these are a concern, use
    /// `.exponential` or `.hybrid` instead.
    ///
    /// Systems may impose an upper bound on the number of digits that are
    /// supported following the radix point.
    case fixed

    /// fprintf's `%e` formatting.
    ///
    /// Prints the number in the form [-]d.ddd...dde±dd, with `precision` significant
    /// digits following the radix point. Systems may impose an upper bound on the number
    /// of digits that are supported.
    case exponential

    /// fprintf's `%g` formatting.
    ///
    /// Behaves like `.fixed` when the number is scaled close to 1.0, and like
    /// `.exponential` if it has a very large or small exponent.
    case hybrid
  }

  @usableFromInline
  internal var notation: Notation

  @_transparent
  @usableFromInline
  internal init(
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false,
    precision: (() -> Int)?,
    notation: Notation
  ) {
    self.explicitPositiveSign = explicitPositiveSign
    self.uppercase = uppercase
    self.precision = precision
    self.notation = notation
  }

  /// Displays an interpolated floating-point value in fprintf's `%f` format with
  /// default precision.
  ///
  /// Prints all digits before the radix point, and 6 digits following the radix point.
  /// Note also that this format is very likely to print non-zero values as all-zero.
  ///
  /// Note that very large floating-point values may print quite a lot of digits
  /// when using this format --up to hundreds for `Double`. Note also that this
  /// format is very likely to print non-zero values as all-zero. If these are a concern,
  /// use `.exponential` or `.hybrid` instead.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var fixed: OSLogFloatFormatting { .fixed() }

  /// Displays an interpolated floating-point value in fprintf's `%f` format with
  /// specified precision, and optional sign and case.
  ///
  /// Prints all digits before the radix point, and `precision` digits following
  /// the radix point. If `precision` is zero, the radix point is omitted.
  ///
  /// Note that very large floating-point values may print quite a lot of digits
  /// when using this format, even if `precision` is zero--up to hundreds for
  /// `Double`. Note also that this format is very likely to print non-zero values as
  /// all-zero. If these are a concern, use `.exponential` or `.hybrid` instead.
  ///
  /// Systems may impose an upper bound on the number of digits that are
  /// supported following the radix point.
  ///
  /// All parameters to this function except `precision` must be boolean literals.
  ///
  /// - Parameters:
  ///   - precision: Number of digits to display after the radix point.
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func fixed(
    precision: @escaping @autoclosure () -> Int,
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: precision,
      notation: .fixed
    )
  }

  /// Displays an interpolated floating-point value in fprintf's `%f` format with
  /// default precision, and optional sign and case.
  ///
  /// Prints all digits before the radix point, and 6 digits following the radix point.
  /// Note also that this format is very likely to print non-zero values as all-zero.
  ///
  /// Note that very large floating-point values may print quite a lot of digits
  /// when using this format, even if `precision` is zero--up to hundreds for
  /// `Double`. Note also that this format is very likely to print non-zero values as
  /// all-zero. If these are a concern, use `.exponential` or `.hybrid` instead.
  ///
  /// Systems may impose an upper bound on the number of digits that are
  /// supported following the radix point.
  ///
  /// All parameters to this function must be boolean literals.
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func fixed(
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: nil,
      notation: .fixed
    )
  }

  /// Displays an interpolated floating-point value in hexadecimal format.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var hex: OSLogFloatFormatting { .hex() }

  /// Displays an interpolated floating-point value in hexadecimal format with
  /// optional sign and case.
  ///
  /// All parameters to this function must be boolean literals.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func hex(
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: nil,
      notation: .hex
    )
  }

  /// Displays an interpolated floating-point value in fprintf's `%e` format.
  ///
  /// Prints the number in the form [-]d.ddd...dde±dd.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var exponential: OSLogFloatFormatting { .exponential() }

  /// Displays an interpolated floating-point value in fprintf's `%e` format with
  /// specified precision, and optional sign and case.
  ///
  /// Prints the number in the form [-]d.ddd...dde±dd, with `precision` significant
  /// digits following the radix point. Systems may impose an upper bound on the number
  /// of digits that are supported.
  ///
  /// All parameters except `precision` must be boolean literals.
  ///
  /// - Parameters:
  ///   - precision: Number of digits to display after the radix point.
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func exponential(
    precision: @escaping @autoclosure () -> Int,
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: precision,
      notation: .exponential
    )
  }

  /// Displays an interpolated floating-point value in fprintf's `%e` format with
  /// an optional sign and case.
  ///
  /// Prints the number in the form [-]d.ddd...dde±dd.
  ///
  /// All parameters to this function must be boolean literals.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func exponential(
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: nil,
      notation: .exponential
    )
  }

  /// Displays an interpolated floating-point value in fprintf's `%g` format.
  ///
  /// Behaves like `.fixed` when the number is scaled close to 1.0, and like
  /// `.exponential` if it has a very large or small exponent.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var hybrid: OSLogFloatFormatting { .hybrid() }

  /// Displays an interpolated floating-point value in fprintf's `%g` format with the
  /// specified precision, and optional sign and case.
  ///
  /// Behaves like `.fixed` when the number is scaled close to 1.0, and like
  /// `.exponential` if it has a very large or small exponent.
  ///
  /// All parameters except `precision` must be boolean literals.
  ///
  /// - Parameters:
  ///   - precision: Number of digits to display after the radix point.
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func hybrid(
    precision: @escaping @autoclosure () -> Int,
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: precision,
      notation: .hybrid
    )
  }

  /// Displays an interpolated floating-point value in fprintf's `%g` format with
  /// optional sign and case.
  ///
  /// Behaves like `.fixed` when the number is scaled close to 1.0, and like
  /// `.exponential` if it has a very large or small exponent.
  ///
  /// All parameters to this function must be boolean literals.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - uppercase: Pass `true` to use uppercase letters or `false` to use
  ///     lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func hybrid(
    explicitPositiveSign: Bool = false,
    uppercase: Bool = false
  ) -> OSLogFloatFormatting {
    return OSLogFloatFormatting(
      explicitPositiveSign: explicitPositiveSign,
      uppercase: uppercase,
      precision: nil,
      notation: .hybrid
    )
  }
}

extension OSLogFloatFormatting {
  /// Returns a fprintf-compatible length modifier for a given argument type
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  internal static func _formatStringLengthModifier<I: FloatingPoint>(
    _ type: I.Type
  ) -> String? {
    switch type {
    //   fprintf formatters promote Float to Double
    case is Float.Type: return ""
    case is Double.Type: return ""
#if !os(Windows) && (arch(i386) || arch(x86_64))
    //   fprintf formatters use L for Float80
    case is Float80.Type: return "L"
#endif
    default: return nil
    }
  }

  /// Constructs an os_log format specifier for the given type `type`
  /// using the specified alignment `align` and privacy qualifier `privacy`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  internal func formatSpecifier<I: FloatingPoint>(
    for type: I.Type,
    align: OSLogStringAlignment,
    privacy: OSLogPrivacy
  ) -> String {
    var specification = "%"
    // Add privacy qualifier after % sign within curly braces. This is an
    // os log specific flag.
    if let privacySpecifier = privacy.privacySpecifier {
      specification += "{"
      specification += privacySpecifier
      specification += "}"
    }

    // 1. Flags
    // IEEE: `+` The result of a signed conversion shall always begin with a sign
    // ( '+' or '-' )
    if explicitPositiveSign {
      specification += "+"
    }

    // IEEE: `-` The result of the conversion shall be left-justified within the field.
    // The conversion is right-justified if this flag is not specified.
    if case .start = align.anchor {
      specification += "-"
    }

    if let _ = align.minimumColumnWidth {
      // The alignment could be a dynamic value. Therefore, use a star here and pass it
      // as an additional argument.
      specification += "*"
    }

    if let _ = precision {
      specification += ".*"
    }

    guard let lengthModifier =
      OSLogFloatFormatting._formatStringLengthModifier(type) else {
      fatalError("Float type has unknown length")
    }
    specification += lengthModifier

    // 3. Precision and conversion specifier.
    switch notation {
    case .fixed:
      specification += (uppercase ? "F" : "f")
    case .exponential:
      specification += (uppercase ? "E" : "e")
    case .hybrid:
      specification += (uppercase ? "G" : "g")
    case .hex:
      //guard type.radix == 2 else { return nil }
      specification += (uppercase ? "A" : "a")
    default:
      fatalError("Unknown float notation")
    }
    return specification
  }
}

