package org.apache.commons.net.ntp;
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.net.DatagramPacket;

/***
 * Implementation of NtpV3Packet with methods converting Java objects to/from
 * the Network Time Protocol (NTP) data message header format described in RFC-1305.
 *
 * @author Naz Irizarry, MITRE Corp
 * @author Jason Mathews, MITRE Corp
 *
 * @version $Revision: 1299238 $
 */
public class NtpV3Impl implements NtpV3Packet
{

    private static final int MODE_INDEX = 0;
    private static final int MODE_SHIFT = 0;

    private static final int VERSION_INDEX = 0;
    private static final int VERSION_SHIFT = 3;

    private static final int LI_INDEX = 0;
    private static final int LI_SHIFT = 6;

    private static final int STRATUM_INDEX = 1;
    private static final int POLL_INDEX = 2;
    private static final int PRECISION_INDEX = 3;

    private static final int ROOT_DELAY_INDEX = 4;
    private static final int ROOT_DISPERSION_INDEX = 8;
    private static final int REFERENCE_ID_INDEX = 12;

    private static final int REFERENCE_TIMESTAMP_INDEX = 16;
    private static final int ORIGINATE_TIMESTAMP_INDEX = 24;
    private static final int RECEIVE_TIMESTAMP_INDEX = 32;
    private static final int TRANSMIT_TIMESTAMP_INDEX = 40;

//    private static final int KEY_IDENTIFIER_INDEX = 48;
//    private static final int MESSAGE_DIGEST = 54; /* len 16 bytes */

    private final byte[] buf = new byte[48];

    private volatile DatagramPacket dp;

    /** Creates a new instance of NtpV3Impl */
    public NtpV3Impl()
    {
    }

    /***
     * Returns mode as defined in RFC-1305 which is a 3-bit integer
     * whose value is indicated by the MODE_xxx parameters.
     *
     * @return mode as defined in RFC-1305.
     */
    public int getMode()
    {
        return (ui(buf[MODE_INDEX]) >> MODE_SHIFT) & 0x7;
    }

    /***
     * Return human-readable name of message mode type as described in
     * RFC 1305.
     * @return mode name as string.
     */
    public String getModeName()
    {
        return NtpUtils.getModeName(getMode());
    }

    /***
     * Set mode as defined in RFC-1305.
     * @param mode
     */
    public void setMode(int mode)
    {
        buf[MODE_INDEX] = (byte) (buf[MODE_INDEX] & 0xF8 | mode & 0x7);
    }

    /***
     * Returns leap indicator as defined in RFC-1305 which is a two-bit code:
     *  0=no warning
     *  1=last minute has 61 seconds
     *  2=last minute has 59 seconds
     *  3=alarm condition (clock not synchronized)
     *
     * @return leap indicator as defined in RFC-1305.
     */
    public int getLeapIndicator()
    {
        return (ui(buf[LI_INDEX]) >> LI_SHIFT) & 0x3;
    }

    /***
     * Set leap indicator as defined in RFC-1305.
     * @param li leap indicator.
     */
    public void setLeapIndicator(int li)
    {
        buf[LI_INDEX] = (byte) (buf[LI_INDEX] & 0x3F | ((li & 0x3) << LI_SHIFT));
    }

    /***
     * Returns poll interval as defined in RFC-1305, which is an eight-bit
     * signed integer indicating the maximum interval between successive
     * messages, in seconds to the nearest power of two (e.g. value of six
     * indicates an interval of 64 seconds. The values that can appear in
     * this field range from NTP_MINPOLL to NTP_MAXPOLL inclusive.
     *
     * @return poll interval as defined in RFC-1305.
     */
    public int getPoll()
    {
        return buf[POLL_INDEX];
    }

    /***
     * Set poll interval as defined in RFC-1305.
     *
     * @param poll poll interval.
     */
    public void setPoll(int poll)
    {
        buf[POLL_INDEX] = (byte) (poll & 0xFF);
    }

    /***
     * Returns precision as defined in RFC-1305 encoded as an 8-bit signed
     * integer (seconds to nearest power of two).
     * Values normally range from -6 to -20.
     *
     * @return precision as defined in RFC-1305.
     */
    public int getPrecision()
    {
        return buf[PRECISION_INDEX];
    }

    /***
     * Set precision as defined in RFC-1305.
     * @param precision
     */
    public void setPrecision(int precision)
    {
        buf[PRECISION_INDEX] = (byte) (precision & 0xFF);
    }

    /***
     * Returns NTP version number as defined in RFC-1305.
     *
     * @return NTP version number.
     */
    public int getVersion()
    {
        return (ui(buf[VERSION_INDEX]) >> VERSION_SHIFT) & 0x7;
    }

    /***
     * Set NTP version as defined in RFC-1305.
     *
     * @param version NTP version.
     */
    public void setVersion(int version)
    {
        buf[VERSION_INDEX] = (byte) (buf[VERSION_INDEX] & 0xC7 | ((version & 0x7) << VERSION_SHIFT));
    }

    /***
     * Returns Stratum as defined in RFC-1305, which indicates the stratum level
     * of the local clock, with values defined as follows: 0=unspecified,
     * 1=primary ref clock, and all others a secondary reference (via NTP).
     *
     * @return Stratum level as defined in RFC-1305.
     */
    public int getStratum()
    {
        return ui(buf[STRATUM_INDEX]);
    }

    /***
     * Set stratum level as defined in RFC-1305.
     *
     * @param stratum stratum level.
     */
    public void setStratum(int stratum)
    {
        buf[STRATUM_INDEX] = (byte) (stratum & 0xFF);
    }

    /***
     * Return root delay as defined in RFC-1305, which is the total roundtrip delay
     * to the primary reference source, in seconds. Values can take positive and
     * negative values, depending on clock precision and skew.
     *
     * @return root delay as defined in RFC-1305.
     */
    public int getRootDelay()
    {
        return getInt(ROOT_DELAY_INDEX);
    }

    /***
     * Return root delay as defined in RFC-1305 in milliseconds, which is
     * the total roundtrip delay to the primary reference source, in
     * seconds. Values can take positive and negative values, depending
     * on clock precision and skew.
     *
     * @return root delay in milliseconds
     */
    public double getRootDelayInMillisDouble()
    {
        double l = getRootDelay();
        return l / 65.536;
    }

    /***
     * Returns root dispersion as defined in RFC-1305.
     * @return root dispersion.
     */
    public int getRootDispersion()
    {
        return getInt(ROOT_DISPERSION_INDEX);
    }

    /***
     * Returns root dispersion (as defined in RFC-1305) in milliseconds.
     *
     * @return root dispersion in milliseconds
     */
    public long getRootDispersionInMillis()
    {
        long l = getRootDispersion();
        return (l * 1000) / 65536L;
    }

    /***
     * Returns root dispersion (as defined in RFC-1305) in milliseconds
     * as double precision value.
     *
     * @return root dispersion in milliseconds
     */
    public double getRootDispersionInMillisDouble()
    {
        double l = getRootDispersion();
        return l / 65.536;
    }

    /***
     * Set reference clock identifier field with 32-bit unsigned integer value.
     * See RFC-1305 for description.
     *
     * @param refId reference clock identifier.
     */
    public void setReferenceId(int refId)
    {
        for (int i = 3; i >= 0; i--) {
            buf[REFERENCE_ID_INDEX + i] = (byte) (refId & 0xff);
            refId >>>= 8; // shift right one-byte
        }
    }

    /***
     * Returns the reference id as defined in RFC-1305, which is
     * a 32-bit integer whose value is dependent on several criteria.
     *
     * @return the reference id as defined in RFC-1305.
     */
    public int getReferenceId()
    {
        return getInt(REFERENCE_ID_INDEX);
    }

    /***
     * Returns the reference id string. String cannot be null but
     * value is dependent on the version of the NTP spec supported
     * and stratum level. Value can be an empty string, clock type string,
     * IP address, or a hex string.
     *
     * @return the reference id string.
     */
    public String getReferenceIdString()
    {
        int version = getVersion();
        int stratum = getStratum();
        if (version == VERSION_3 || version == VERSION_4) {
            if (stratum == 0 || stratum == 1) {
                return idAsString(); // 4-character ASCII string (e.g. GPS, USNO)
            }
            // in NTPv4 servers this is latest transmit timestamp of ref source
            if (version == VERSION_4) {
                return idAsHex();
            }
        }

        // Stratum 2 and higher this is a four-octet IPv4 address
        // of the primary reference host.
        if (stratum >= 2) {
            return idAsIPAddress();
        }
        return idAsHex();
    }

    /***
     * Returns Reference id as dotted IP address.
     * @return refId as IP address string.
     */
    private String idAsIPAddress()
    {
        return ui(buf[REFERENCE_ID_INDEX]) + "." +
                ui(buf[REFERENCE_ID_INDEX + 1]) + "." +
                ui(buf[REFERENCE_ID_INDEX + 2]) + "." +
                ui(buf[REFERENCE_ID_INDEX + 3]);
    }

    private String idAsString()
    {
        StringBuilder id = new StringBuilder();
        for (int i = 0; i <= 3; i++) {
            char c = (char) buf[REFERENCE_ID_INDEX + i];
            if (c == 0) {  // 0-terminated string
                break;
            }
            id.append(c);
        }
        return id.toString();
    }

    private String idAsHex()
    {
        return Integer.toHexString(getReferenceId());
    }

    /***
     * Returns the transmit timestamp as defined in RFC-1305.
     *
     * @return the transmit timestamp as defined in RFC-1305.
     * Never returns a null object.
     */
    public TimeStamp getTransmitTimeStamp()
    {
        return getTimestamp(TRANSMIT_TIMESTAMP_INDEX);
    }

    /***
     * Set transmit time with NTP timestamp.
     * If <code>ts</code> is null then zero time is used.
     *
     * @param ts NTP timestamp
     */
    public void setTransmitTime(TimeStamp ts)
    {
        setTimestamp(TRANSMIT_TIMESTAMP_INDEX, ts);
    }

    /***
     * Set originate timestamp given NTP TimeStamp object.
     * If <code>ts</code> is null then zero time is used.
     *
     * @param ts NTP timestamp
     */
    public void setOriginateTimeStamp(TimeStamp ts)
    {
        setTimestamp(ORIGINATE_TIMESTAMP_INDEX, ts);
    }

    /***
     * Returns the originate time as defined in RFC-1305.
     *
     * @return the originate time.
     * Never returns null.
     */
    public TimeStamp getOriginateTimeStamp()
    {
        return getTimestamp(ORIGINATE_TIMESTAMP_INDEX);
    }

    /***
     * Returns the reference time as defined in RFC-1305.
     *
     * @return the reference time as <code>TimeStamp</code> object.
     * Never returns null.
     */
    public TimeStamp getReferenceTimeStamp()
    {
        return getTimestamp(REFERENCE_TIMESTAMP_INDEX);
    }

    /***
     * Set Reference time with NTP timestamp. If <code>ts</code> is null
     * then zero time is used.
     *
     * @param ts NTP timestamp
     */
    public void setReferenceTime(TimeStamp ts)
    {
        setTimestamp(REFERENCE_TIMESTAMP_INDEX, ts);
    }

    /***
     * Returns receive timestamp as defined in RFC-1305.
     *
     * @return the receive time.
     * Never returns null.
     */
    public TimeStamp getReceiveTimeStamp()
    {
        return getTimestamp(RECEIVE_TIMESTAMP_INDEX);
    }

    /***
     * Set receive timestamp given NTP TimeStamp object.
     * If <code>ts</code> is null then zero time is used.
     *
     * @param ts timestamp
     */
    public void setReceiveTimeStamp(TimeStamp ts)
    {
        setTimestamp(RECEIVE_TIMESTAMP_INDEX, ts);
    }

    /***
     * Return type of time packet. The values (e.g. NTP, TIME, ICMP, ...)
     * correspond to the protocol used to obtain the timing information.
     *
     * @return packet type string identifier which in this case is "NTP".
     */
    public String getType()
    {
        return "NTP";
    }

    /***
     * @return 4 bytes as 32-bit int
     */
    private int getInt(int index)
    {
        int i = ui(buf[index]) << 24 |
                ui(buf[index + 1]) << 16 |
                ui(buf[index + 2]) << 8 |
                ui(buf[index + 3]);

        return i;
    }

    /***
     * Get NTP Timestamp at specified starting index.
     *
     * @param index index into data array
     * @return TimeStamp object for 64 bits starting at index
     */
    private TimeStamp getTimestamp(int index)
    {
        return new TimeStamp(getLong(index));
    }

    /***
     * Get Long value represented by bits starting at specified index.
     *
     * @return 8 bytes as 64-bit long
     */
    private long getLong(int index)
    {
        long i = ul(buf[index]) << 56 |
                ul(buf[index + 1]) << 48 |
                ul(buf[index + 2]) << 40 |
                ul(buf[index + 3]) << 32 |
                ul(buf[index + 4]) << 24 |
                ul(buf[index + 5]) << 16 |
                ul(buf[index + 6]) << 8 |
                ul(buf[index + 7]);
        return i;
    }

    /***
     * Sets the NTP timestamp at the given array index.
     *
     * @param index index into the byte array.
     * @param t TimeStamp.
     */
    private void setTimestamp(int index, TimeStamp t)
    {
        long ntpTime = (t == null) ? 0 : t.ntpValue();
        // copy 64-bits from Long value into 8 x 8-bit bytes of array
        // one byte at a time shifting 8-bits for each position.
        for (int i = 7; i >= 0; i--) {
            buf[index + i] = (byte) (ntpTime & 0xFF);
            ntpTime >>>= 8; // shift to next byte
        }
        // buf[index] |= 0x80;  // only set if 1900 baseline....
    }

    /***
     * Returns the datagram packet with the NTP details already filled in.
     *
     * @return a datagram packet.
     */
    public synchronized DatagramPacket getDatagramPacket()
    {
        if (dp == null) {
            dp = new DatagramPacket(buf, buf.length);
            dp.setPort(NTP_PORT);
        }
        return dp;
    }

    /***
     * Set the contents of this object from source datagram packet.
     *
     * @param srcDp source DatagramPacket to copy contents from.
     */
    public void setDatagramPacket(DatagramPacket srcDp)
    {
        byte[] incomingBuf = srcDp.getData();
        int len = srcDp.getLength();
        if (len > buf.length) {
            len = buf.length;
        }

        System.arraycopy(incomingBuf, 0, buf, 0, len);
    }

    /***
     * Convert byte to unsigned integer.
     * Java only has signed types so we have to do
     * more work to get unsigned ops.
     *
     * @param b
     * @return unsigned int value of byte
     */
    protected final static int ui(byte b)
    {
        int i = b & 0xFF;
        return i;
    }

    /***
     * Convert byte to unsigned long.
     * Java only has signed types so we have to do
     * more work to get unsigned ops
     *
     * @param b
     * @return unsigned long value of byte
     */
    protected final static long ul(byte b)
    {
        long i = b & 0xFF;
        return i;
    }

    /***
     * Returns details of NTP packet as a string.
     *
     * @return details of NTP packet as a string.
     */
    @Override
    public String toString()
    {
        return "[" +
                "version:" + getVersion() +
                ", mode:" + getMode() +
                ", poll:" + getPoll() +
                ", precision:" + getPrecision() +
                ", delay:" + getRootDelay() +
                ", dispersion(ms):" + getRootDispersionInMillisDouble() +
                ", id:" + getReferenceIdString() +
                ", xmitTime:" + getTransmitTimeStamp().toDateString() +
                " ]";
    }

}
