//===----------------- OSLogIntegerFormatting.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
// integer-valued interpolations passed to the os log APIs.

@frozen
public struct OSLogIntegerFormatting {
  /// The base to use for the string representation. `radix` must be at least 2
  /// and at most 36. The default is 10.
  @usableFromInline
  internal var radix: Int

  /// When set, a `+` will be printed for all non-negative integers.
  @usableFromInline
  internal var explicitPositiveSign: Bool

  /// When set, a prefix: 0b or 0o or 0x will be added when the radix is 2, 8 or
  /// 16 respectively.
  @usableFromInline
  internal var includePrefix: Bool

  /// Whether to use uppercase letters to represent numerals
  /// greater than 9 (default is to use lowercase).
  @usableFromInline
  internal var uppercase: Bool

  /// Minimum number of digits to display. Numbers having fewer digits than
  /// minDigits will be displayed with leading zeros.
  @usableFromInline
  internal var minDigits: (() -> Int)?

  /// Initializes all stored properties.
  ///
  /// - Parameters:
  ///   - radix: The base to use for the string representation. `radix` must be
  ///     at least 2 and at most 36. The default is 10.
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers. Default is `false`.
  ///   - includePrefix: Pass `true` to add a prefix: 0b, 0o, 0x to corresponding
  ///     radices. Default is `false`.
  ///   - uppercase: Pass `true` to use uppercase letters to represent numerals
  ///     greater than 9, or `false` to use lowercase letters. The default is
  ///     `false`.
  ///   - minDigits: minimum number of digits to display. Numbers will be
  ///     prefixed with zeros if necessary to meet the minimum. The default is 1.
  @_transparent
  @usableFromInline
  internal init(
    radix: Int = 10,
    explicitPositiveSign: Bool = false,
    includePrefix: Bool = false,
    uppercase: Bool = false,
    minDigits: (() -> Int)?
  ) {
    self.radix = radix
    self.explicitPositiveSign = explicitPositiveSign
    self.includePrefix = includePrefix
    self.uppercase = uppercase
    self.minDigits = minDigits
  }

  /// Displays an interpolated integer as a decimal number with the specified number
  /// of digits and an optional sign.
  ///
  /// The parameter `explicitPositiveSign` must be a boolean literal. The
  /// parameter `minDigits` can be an arbitrary expression.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - minDigits: minimum number of digits to display. Numbers will be
  ///     prefixed with zeros if necessary to meet the minimum.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func decimal(
    explicitPositiveSign: Bool = false,
    minDigits: @escaping @autoclosure () -> Int
  ) -> OSLogIntegerFormatting {
    return OSLogIntegerFormatting(
      radix: 10,
      explicitPositiveSign: explicitPositiveSign,
      minDigits: minDigits)
  }

  /// Displays an interpolated integer as a decimal number with an optional sign.
  ///
  /// The parameter `explicitPositiveSign` must be a boolean literal.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func decimal(
    explicitPositiveSign: Bool = false
  ) -> OSLogIntegerFormatting {
    return OSLogIntegerFormatting(
      radix: 10,
      explicitPositiveSign: explicitPositiveSign,
      minDigits: nil)
  }

  /// Displays an interpolated integer as a decimal number. This is the default format for
  /// integers.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var decimal: OSLogIntegerFormatting { .decimal() }

  /// Displays an interpolated unsigned integer as a hexadecimal number with the
  /// specified parameters. This formatting option should be used only with unsigned
  /// integers.
  ///
  /// All parameters except `minDigits` should be boolean literals. `minDigits`
  /// can be an arbitrary expression.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - includePrefix: Pass `true` to add a prefix 0x.
  ///   - uppercase: Pass `true` to use uppercase letters to represent numerals
  ///     greater than 9, or `false` to use lowercase letters. The default is `false`.
  ///   - minDigits: minimum number of digits to display. Numbers will be
  ///     prefixed with zeros if necessary to meet the minimum.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func hex(
    explicitPositiveSign: Bool = false,
    includePrefix: Bool = false,
    uppercase: Bool = false,
    minDigits: @escaping @autoclosure () -> Int
  ) -> OSLogIntegerFormatting {
    return OSLogIntegerFormatting(
      radix: 16,
      explicitPositiveSign: explicitPositiveSign,
      includePrefix: includePrefix,
      uppercase: uppercase,
      minDigits: minDigits)
  }

  /// Displays an interpolated unsigned integer as a hexadecimal number with the specified
  /// parameters. This formatting option should be used only with unsigned integers.
  ///
  /// All parameters  should be boolean literals.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - includePrefix: Pass `true` to add a prefix 0x.
  ///   - uppercase: Pass `true` to use uppercase letters to represent numerals
  ///     greater than 9, or `false` to use lowercase letters. The default is `false`.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func hex(
    explicitPositiveSign: Bool = false,
    includePrefix: Bool = false,
    uppercase: Bool = false
  ) -> OSLogIntegerFormatting {
    return OSLogIntegerFormatting(
      radix: 16,
      explicitPositiveSign: explicitPositiveSign,
      includePrefix: includePrefix,
      uppercase: uppercase,
      minDigits: nil)
  }

  /// Displays an interpolated unsigned integer as a hexadecimal number.
  /// This formatting option should be used only with unsigned integers.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var hex: OSLogIntegerFormatting { .hex() }

  /// Displays an interpolated unsigned integer as an octal number with the specified
  /// parameters. This formatting option should be used only with unsigned
  /// integers.
  ///
  /// All parameters except `minDigits` should be boolean literals. `minDigits`
  /// can be an arbitrary expression.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - includePrefix: Pass `true` to add a prefix 0o.
  ///   - uppercase: Pass `true` to use uppercase letters to represent numerals
  ///     greater than 9, or `false` to use lowercase letters. The default is `false`.
  ///   - minDigits: minimum number of digits to display. Numbers will be
  ///     prefixed with zeros if necessary to meet the minimum.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func octal(
    explicitPositiveSign: Bool = false,
    includePrefix: Bool = false,
    uppercase: Bool = false,
    minDigits: @autoclosure @escaping () -> Int
  ) -> OSLogIntegerFormatting {
    OSLogIntegerFormatting(
      radix: 8,
      explicitPositiveSign: explicitPositiveSign,
      includePrefix: includePrefix,
      uppercase: uppercase,
      minDigits: minDigits)
  }

  /// Displays an interpolated unsigned integer as an octal number with the specified parameters.
  /// This formatting option should be used only with unsigned integers.
  ///
  /// All parameters must be boolean literals.
  ///
  /// - Parameters:
  ///   - explicitPositiveSign: Pass `true` to add a + sign to non-negative
  ///     numbers.
  ///   - includePrefix: Pass `true` to add a prefix 0o.
  ///   - uppercase: Pass `true` to use uppercase letters to represent numerals
  ///     greater than 9, or `false` to use lowercase letters.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static func octal(
    explicitPositiveSign: Bool = false,
    includePrefix: Bool = false,
    uppercase: Bool = false
  ) -> OSLogIntegerFormatting {
    OSLogIntegerFormatting(
      radix: 8,
      explicitPositiveSign: explicitPositiveSign,
      includePrefix: includePrefix,
      uppercase: uppercase,
      minDigits: nil)
  }

  /// Displays an interpolated unsigned integer as an octal number.
  /// This formatting option should be used only with unsigned integers.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  public static var octal: OSLogIntegerFormatting { .octal() }
}

extension OSLogIntegerFormatting {
  /// The prefix for the radix in the Swift literal syntax.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  internal var _prefix: String {
    guard includePrefix else { return "" }
    switch radix {
    case 2: return "0b"
    case 8: return "0o"
    case 16: return "0x"
    default: return ""
    }
  }
}

extension OSLogIntegerFormatting {

  /// Returns a fprintf-compatible length modifier for a given argument type.
  @_semantics("constant_evaluable")
  @inlinable
  @_optimize(none)
  internal static func formatSpecifierLengthModifier<I: FixedWidthInteger>(
    _ type: I.Type
  ) -> String? {
    // IEEE Std 1003.1-2017, length modifiers:
    switch type {
    //   hh - d, i, o, u, x, or X conversion specifier applies to
    // (signed|unsigned) char
    case is CChar.Type: return "hh"
    case is CUnsignedChar.Type: return "hh"

    //   h  - d, i, o, u, x, or X conversion specifier applies to
    // (signed|unsigned) short
    case is CShort.Type: return "h"
    case is CUnsignedShort.Type: return "h"

    case is CInt.Type: return ""
    case is CUnsignedInt.Type: return ""

    //   l  - d, i, o, u, x, or X conversion specifier applies to
    // (signed|unsigned) long
    case is CLong.Type: return "l"
    case is CUnsignedLong.Type: return "l"

    //   ll - d, i, o, u, x, or X conversion specifier applies to
    // (signed|unsigned) long long
    case is CLongLong.Type: return "ll"
    case is CUnsignedLongLong.Type: return "ll"

    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)
  @_effects(readonly)
  internal func formatSpecifier<I: FixedWidthInteger>(
    for type: I.Type,
    align: OSLogStringAlignment,
    privacy: OSLogPrivacy
  ) -> String {
    // Based on IEEE Std 1003.1-2017
    // `d`/`i` is the only signed integral conversions allowed
    if (type.isSigned && radix != 10) {
      fatalError("Signed integers must be formatted using .decimal")
    }

    // IEEE: Each conversion specification is introduced by the '%' character
    // after which the following appear in sequence:
    //   1. Zero or more flags (in any order), which modify the meaning of the
    //      conversion specification.
    //   2. An optional minimum field width (for alignment). If the converted
    //      value has fewer bytes than the field width, it shall be padded with
    //      <space> characters by default on the left; it shall be padded on the
    //      right if the left-adjustment flag ( '-' ), is given to the
    //      field width.
    //   3. An optional precision that gives the minimum number of digits to
    //      display for the d, i, o, u, x, and X conversion specifiers.
    //   4. An optional length modifier that specifies the size of the argument.
    //   5. A conversion specifier character that indicates the type of
    //      conversion to be applied.

    // Use Swift style prefixes rather than fprintf style prefixes.
    var specification = _prefix
    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
    //
    // Use `+` flag if signed, otherwise prefix a literal `+` for unsigned.
    if explicitPositiveSign {
      // IEEE: `+` The result of a signed conversion shall always begin with a
      // sign ( '+' or '-' )
      if type.isSigned {
        specification += "+"
      } else {
        var newSpecification = "+"
        newSpecification += specification
        specification = newSpecification
      }
    }

    // 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 += "-"
    }

    // 2. Minimum field width
    // IEEE: When field width is prefixed by `0`, leading zeros (following any
    // indication of sign or base) are used to pad to the field width rather
    // than performing space padding. If the '0' and '-' flags both appear,
    // the '0' flag is ignored. If a precision is specified, the '0' flag shall
    // be ignored.
    //
    // Commentary: `0` is prefixed to field width when the user doesn't want to
    // use precision (minDigits). This allows sign and prefix characters to be
    // counted towards field width (they wouldn't be counted towards precision).
    // This is more useful for floats, where precision is digits after the radix.
    // In our case, we're already handling prefix ourselves; we choose not to
    // support this functionality. In our case, alignment always pads spaces (
    // to the left or right) until the minimum field width is met.
    if let _ = align.minimumColumnWidth {
      // The alignment could be a dynamic value. Therefore, use a star here and pass it
      // as an additional argument.
      specification += "*"
    }

    // 3. Precision

    // Default precision for integers is 1, otherwise use the requested precision.
    // The precision could be a dynamic value. Therefore, use a star here and pass it
    // as an additional argument.
    if let _ = minDigits {
      specification += ".*"
    }

    // 4. Length modifier
    guard let lengthModifier =
      OSLogIntegerFormatting.formatSpecifierLengthModifier(type) else {
      fatalError("Integer type has unknown byte length")
    }
    specification += lengthModifier

    // 5. The conversion specifier
    switch radix {
    case 10:
      specification += type.isSigned ? "d" : "u"
    case 8:
      specification += "o"
    case 16:
      specification += uppercase ? "X" : "x"
    default:
      fatalError("radix must be 10, 8 or 16")
    }
    return specification
  }
}
