part of ntp;

/// This class represents a NTP message, as specified in RFC 2030.  The message
/// format is compatible with all versions of NTP and SNTP.
///
/// This class does not support the optional authentication protocol, and
/// ignores the key ID and message digest fields.
///
/// For convenience, this class exposes message values as native Java types, not
/// the NTP-specified data formats.  For example, timestamps are
/// stored as doubles (as opposed to the NTP unsigned 64-bit fixed point
/// format).
///
/// However, the constructor NtpMessage(byte[]) and the method toByteArray()
/// allow the import and export of the raw NTP message format.
///
/// This code is copyright (c) Adam Buckley 2004
///
/// This program is free software; you can redistribute it and/or modify it
/// under the terms of the GNU General Public License as published by the Free
/// Software Foundation; either version 2 of the License, or (at your option)
/// any later version.  A HTML version of the GNU General Public License can be
/// seen at http://www.gnu.org/licenses/gpl.html
///
/// This program is distributed in the hope that it will be useful, but WITHOUT
/// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
/// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
/// more details.
///
///
/// Comments for member variables are taken from RFC2030 by David Mills,
/// University of Delaware.
///
/// Number format conversion code in NtpMessage(byte[] array) and toByteArray()
/// inspired by http://www.pps.jussieu.fr/~jch/enseignement/reseaux/
/// NTPMessage.java which is copyright (c) 2003 by Juliusz Chroboczek
///
/// @author Adam Buckley
/// Rewritten in dart by: Luka Knezic 2018
class _NTPMessage {
  /// Constructs a NtpMessage in client -> server mode, and sets the
  /// transmit timestamp to the current time.
  ///
  /// If byte array (raw NTP packet) is passed to constructor then the
  /// data is filled from a raw NTP packet.
  _NTPMessage([List<int>? array]) {
    if (array != null) {
      _leapIndicator = array[0] >> 6 & 0x3;
      _version = array[0] >> 3 & 0x7;
      _mode = array[0] & 0x7;
      _stratum = unsignedByteToShort(array[1]);
      _pollInterval = array[2];
      _precision = array[3];

      _rootDelay = ((array[4] * 256) +
              unsignedByteToShort(array[5]) +
              (unsignedByteToShort(array[6]) / 256) +
              (unsignedByteToShort(array[7]) / 65536))
          .toInt();

      _rootDispersion = ((unsignedByteToShort(array[8]) * 256) +
              unsignedByteToShort(array[9]) +
              (unsignedByteToShort(array[10]) / 256) +
              (unsignedByteToShort(array[11]) / 65536))
          .toInt();

      _referenceIdentifier[0] = array[12];
      _referenceIdentifier[1] = array[13];
      _referenceIdentifier[2] = array[14];
      _referenceIdentifier[3] = array[15];

      _referenceTimestamp = decodeTimestamp(array, 16);
      _originateTimestamp = decodeTimestamp(array, 24);
      _receiveTimestamp = decodeTimestamp(array, 32);
      _transmitTimestamp = decodeTimestamp(array, 40);
    } else {
      final DateTime time = DateTime.now().toLocal();
      _mode = 3;
      _transmitTimestamp = (time.millisecondsSinceEpoch / 1000.0) + timeToUtc;
    }
  }

  final double timeToUtc = 2208988800.0;

  /// This is a two-bit code warning of an impending leap second to be
  /// inserted/deleted in the last minute of the current day.  It's values
  /// may be as follows:
  /// Value     Meaning
  /// -----     -------
  /// 0         no warning
  /// 1         last minute has 61 seconds
  /// 2         last minute has 59 seconds
  /// 3         alarm condition (clock not synchronized)
  int _leapIndicator = 0;

  /// This value indicates the NTP/SNTP version number.  The version number
  /// is 3 for Version 3 (IPv4 only) and 4 for Version 4 (IPv4, IPv6 and OSI).
  /// If necessary to distinguish between IPv4, IPv6 and OSI, the
  /// encapsulating context must be inspected.
  int _version = 3;

  /// This value indicates the mode, with values defined as follows:
  /// Mode     Meaning
  /// ----     -------
  /// 0        reserved
  /// 1        symmetric active
  /// 2        symmetric passive
  /// 3        client
  /// 4        server
  /// 5        broadcast
  /// 6        reserved for NTP control message
  /// 7        reserved for private use
  ///
  /// In unicast and anycast modes, the client sets this field to 3 (client)
  /// in the request and the server sets it to 4 (server) in the reply. In
  /// multicast mode, the server sets this field to 5 (broadcast).
  int _mode = 0;

  /// This value indicates the stratum level of the local clock, with values
  /// defined as follows:
  /// Stratum  Meaning
  /// ----------------------------------------------
  /// 0        unspecified or unavailable
  /// 1        primary reference (e.g., radio clock)
  /// 2-15     secondary reference (via NTP or SNTP)
  /// 16-255   reserved
  int _stratum = 0;

  /// This value indicates the maximum interval between successive messages,
  /// in seconds to the nearest power of two. The values that can appear in
  /// this field presently range from 4 (16 s) to 14 (16284 s); however, most
  /// applications use only the sub-range 6 (64 s) to 10 (1024 s).
  int _pollInterval = 0;

  /// This value indicates the precision of the local clock, in seconds to
  /// the nearest power of two.  The values that normally appear in this field
  /// range from -6 for mains-frequency clocks to -20 for microsecond clocks
  /// found in some workstations.
  int _precision = 0;

  /// This value indicates the total roundtrip delay to the primary reference
  /// source, in seconds.  Note that this variable can take on both positive
  /// and negative values, depending on the relative time and frequency
  /// offsets. The values that normally appear in this field range from
  /// negative values of a few milliseconds to positive values of several
  /// hundred milliseconds.
  int _rootDelay = 0;

  /// This value indicates the nominal error relative to the primary reference
  /// source, in seconds.  The values  that normally appear in this field
  /// range from 0 to several hundred milliseconds.
  int _rootDispersion = 0;

  /// This is a 4-byte array identifying the particular reference source.
  /// In the case of NTP Version 3 or Version 4 stratum-0 (unspecified) or
  /// stratum-1 (primary) servers, this is a four-character ASCII string, left
  /// justified and zero padded to 32 bits. In NTP Version 3 secondary
  /// servers, this is the 32-bit IPv4 address of the reference source. In NTP
  /// Version 4 secondary servers, this is the low order 32 bits of the latest
  /// transmit timestamp of the reference source. NTP primary (stratum 1)
  /// servers should set this field to a code identifying the external
  /// reference source according to the following list. If the external
  /// reference is one of those listed, the associated code should be used.
  /// Codes for sources not listed can be contrived as appropriate.
  /// Code     External Reference Source
  /// ----     -------------------------
  /// LOCL     uncalibrated local clock used as a primary reference for
  /// a subnet without external means of synchronization
  /// PPS      atomic clock or other pulse-per-second source
  /// individually calibrated to national standards
  /// ACTS     NIST dialup modem service
  /// USNO     USNO modem service
  /// PTB      PTB (Germany) modem service
  /// TDF      Allouis (France) Radio 164 kHz
  /// DCF      Mainflingen (Germany) Radio 77.5 kHz
  /// MSF      Rugby (UK) Radio 60 kHz
  /// WWV      Ft. Collins (US) Radio 2.5, 5, 10, 15, 20 MHz
  /// WWVB     Boulder (US) Radio 60 kHz
  /// WWVH     Kaui Hawaii (US) Radio 2.5, 5, 10, 15 MHz
  /// CHU      Ottawa (Canada) Radio 3330, 7335, 14670 kHz
  /// LORC     LORAN-C radionavigation system
  /// OMEG     OMEGA radionavigation system
  /// GPS      Global Positioning Service
  /// GOES     Geostationary Orbit Environment Satellite
  final List<int> _referenceIdentifier = <int>[0, 0, 0, 0];

  /// This is the time at which the local clock was last set or corrected, in
  /// seconds since 00:00 1-Jan-1900.
  double _referenceTimestamp = 0.0;

  /// This is the time at which the request departed the client for the
  /// server, in seconds since 00:00 1-Jan-1900.
  double _originateTimestamp = 0.0;

  /// This is the time at which the request arrived at the server, in seconds
  /// since 00:00 1-Jan-1900.
  double _receiveTimestamp = 0.0;

  /// This is the time at which the reply departed the server for the client,
  /// in seconds since 00:00 1-Jan-1900.
  double _transmitTimestamp = 0.0;

  double get referenceTimestamp => _referenceTimestamp;
  double get originateTimestamp => _originateTimestamp;
  double get receiveTimestamp => _receiveTimestamp;
  double get transmitTimestamp => _transmitTimestamp;

  /// This method constructs the data bytes of a raw NTP packet.
  List<int> toByteArray() {
    final List<int> rawNtp = List.filled(48, 0);

    rawNtp[0] = _leapIndicator << 6 | _version << 3 | _mode;
    rawNtp[1] = _stratum;
    rawNtp[2] = _pollInterval;
    rawNtp[3] = _precision;

    /// root delay is a signed 16.16-bit FP, in Java an int is 32-bits
    final int l = _rootDelay * 65536;
    rawNtp[4] = l >> 24 & 0xFF;
    rawNtp[5] = l >> 16 & 0xFF;
    rawNtp[6] = l >> 8 & 0xFF;
    rawNtp[7] = l & 0xFF;

    /// root dispersion is an unsigned 16.16-bit FP, in Java there are no
    /// unsigned primitive types, so we use a long which is 64-bits
    final int ul = _rootDispersion * 65536;
    rawNtp[8] = ul >> 24 & 0xFF;
    rawNtp[9] = ul >> 16 & 0xFF;
    rawNtp[10] = ul >> 8 & 0xFF;
    rawNtp[11] = ul & 0xFF;

    rawNtp[12] = _referenceIdentifier[0];
    rawNtp[13] = _referenceIdentifier[1];
    rawNtp[14] = _referenceIdentifier[2];
    rawNtp[15] = _referenceIdentifier[3];

    encodeTimestamp(rawNtp, 16, _referenceTimestamp);
    encodeTimestamp(rawNtp, 24, _originateTimestamp);
    encodeTimestamp(rawNtp, 32, _receiveTimestamp);
    encodeTimestamp(rawNtp, 40, _transmitTimestamp);

    return rawNtp;
  }

  /// Converts an unsigned byte to a short.  By default, Java assumes that
  /// a byte is signed.
  int unsignedByteToShort(int i) {
    if ((i & 0x80) == 0x80)
      return 128 + (i & 0x7f);
    else
      return i;
  }

  /// Will read 8 bytes of a message beginning at <code>pointer</code>
  /// and return it as a double, according to the NTP 64-bit timestamp
  /// format.
  double decodeTimestamp(List<int> array, int pointer) {
    double r = 0.0;

    for (int i = 0; i < 8; i++) {
      r += unsignedByteToShort(array[pointer + i]) * pow(2.0, (3 - i) * 8);
    }

    return r;
  }

  /// Encodes a timestamp in the specified position in the message
  void encodeTimestamp(List<int> array, int pointer, double timestamp) {
    /// Converts a double into a 64-bit fixed point
    for (int i = 0; i < 8; i++) {
      /// 2^24, 2^16, 2^8, .. 2^-32
      final num base = pow(2.0, (3 - i) * 8);

      /// Capture byte value
      array[pointer + i] = timestamp ~/ base;

      /// Subtract captured value from remaining total
      timestamp = timestamp - (unsignedByteToShort(array[pointer + i]) * base);
    }

    /// From RFC 2030: It is advisable to fill the non-significant
    /// low order bits of the timestamp with a random, unbiased
    /// bit-string, both to avoid systematic round-off errors and as
    /// a means of loop detection and replay detection.
    array[7] = Random().nextInt(255);
  }

  @override
  String toString() {
    return 'Leap indicator: $_leapIndicator\n'
        'Version: $_version \n'
        'Mode: $_mode\n'
        'Stratum: $_stratum\n'
        'Poll: $_pollInterval\n'
        'Precision: $_precision\n'
        'Root delay: ${_rootDelay * 1000.0} ms\n'
        'Root dispersion: ${_rootDispersion * 1000.0}ms\n'
        'Reference identifier: ${referenceIdentifierToString(_referenceIdentifier, _stratum, _version)}\n'
        'Reference timestamp: ${timestampToString(_referenceTimestamp)}\n'
        'Originate timestamp: ${timestampToString(_originateTimestamp)}\n'
        'Receive timestamp:   ${timestampToString(_receiveTimestamp)}\n'
        'Transmit timestamp:  ${timestampToString(_transmitTimestamp)}';
  }

  String timestampToString(double timestamp) {
    if (timestamp == 0) {
      return '0';
    }

    final double utc = timestamp - timeToUtc;
    final double ms = utc * 1000.0;

    return DateTime.fromMillisecondsSinceEpoch(ms.toInt()).toString();
  }

  String referenceIdentifierToString(List<int> ref, int stratum, int version) {
    /// From the RFC 2030:
    /// In the case of NTP Version 3 or Version 4 stratum-0 (unspecified)
    /// or stratum-1 (primary) servers, this is a four-character ASCII
    /// string, left justified and zero padded to 32 bits.
    if (stratum == 0 || stratum == 1) {
      return ref.toString();
    }

    /// In NTP Version 3 secondary servers, this is the 32-bit IPv4
    /// address of the reference source.
    else if (version == 3) {
      return '${unsignedByteToShort(ref[0])}.${unsignedByteToShort(ref[1])}.'
          '${unsignedByteToShort(ref[2])}.${unsignedByteToShort(ref[3])}';
    }

    /// In NTP Version 4 secondary servers, this is the low order 32 bits
    /// of the latest transmit timestamp of the reference source.
    else if (version == 4) {
      return '${unsignedByteToShort(ref[0]) / 256.0 + unsignedByteToShort(ref[1]) / 65536.0 + unsignedByteToShort(ref[2]) / 16777216.0 + unsignedByteToShort(ref[3]) / 4294967296.0}';
    }

    return '';
  }
}
