package org.deepsymmetry.beatlink;

import java.net.DatagramPacket;
import java.net.InetAddress;

/**
 * Represents a device status update seen on a DJ Link network.
 *
 * @author James Elliott
 */
public abstract class DeviceUpdate {

    /**
     * The address from which this device update was received.
     */
    final InetAddress address;

    /**
     * When this update was received.
     */
    @SuppressWarnings("WeakerAccess")
    final long timestamp;

    /**
     * The name of the device sending the update.
     */
    final String deviceName;

    /**
     * The player/device number sending the update.
     */
    final int deviceNumber;

    /**
     * The packet data containing the device update.
     */
    final byte[] packetBytes;

    /**
     * Does this appear to come from a pre-nexus CDJ?
     */
    final boolean preNexusCdj;

    /**
     * Constructor sets all the immutable interpreted fields based on the packet content.
     *
     * @param packet the device update packet that was received
     * @param name the type of packet that is being processed, in case a problem needs to be reported
     * @param length the expected length of the packet
     */
    @SuppressWarnings("WeakerAccess")
    public DeviceUpdate(DatagramPacket packet, String name, int length) {
        timestamp = System.nanoTime();
        if (packet.getLength() != length) {
            throw new IllegalArgumentException(name + " packet must be " + length + " bytes long");
        }
        address = packet.getAddress();
        packetBytes = new byte[packet.getLength()];
        System.arraycopy(packet.getData(), 0, packetBytes, 0, packet.getLength());
        deviceName = new String(packetBytes, 11, 20).trim();
        preNexusCdj = deviceName.startsWith("CDJ") && deviceName.endsWith("0");
        deviceNumber = Util.unsign(packetBytes[33]);
    }

    /**
     * Get the address of the device from which this update was seen.
     *
     * @return the network address from which the update was sent
     */
    public InetAddress getAddress() {
        return address;
    }

    /**
     * Get the timestamp recording when the device update was received.
     *
     * @return the nanosecond timestamp at which we received this update
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * Get the name reported by the device sending the update.
     *
     * @return the device name
     */
    public String getDeviceName() {
        return deviceName;
    }

    /**
     * Check whether this packet seems to have come from a CDJ older
     * than the original Nexus series (which means, for example, that
     * beat numbers will not be available, so the {@link org.deepsymmetry.beatlink.data.TimeFinder}
     * can't work with it.
     *
     * @return {@code true} if the device name starts with "CDJ" and ends with "0".
     */
    public boolean isPreNexusCdj() {
        return preNexusCdj;
    }

    /**
     * Get the player/device number reporting the update.
     *
     * @return the player number found in the update packet
     */
    public int getDeviceNumber() {
        return deviceNumber;
    }

    /**
     * Get the raw data bytes of the device update packet.
     *
     * @return the data sent by the device to update its status
     */
    public byte[] getPacketBytes() {
        byte[] result = new byte[packetBytes.length];
        System.arraycopy(packetBytes, 0, result, 0, packetBytes.length);
        return result;
    }

    /**
     * Get the device pitch at the time of the update. This is an integer ranging from 0 to 2097152, which corresponds
     * to a range between completely stopping playback to playing at twice normal tempo. The equivalent percentage
     * value can be obtained by passing the pitch to {@link Util#pitchToPercentage(long)}, and the corresponding
     * fractional scaling value by passing it to {@link Util#pitchToMultiplier(long)}. Mixers always report a pitch
     * of +0%, so tempo changes are purely reflected in the BPM value.
     *
     * @return the raw effective device pitch at the time of the update
     */
    public abstract int getPitch();

    /**
     * Get the playback BPM at the time of the update. This is an integer representing the BPM times 100, so a track
     * running at 120.5 BPM would be represented by the value 12050. Mixers always report a pitch of +0%, so tempo
     * changes are purely reflected in the BPM value.
     *
     * <p>When the CDJ has just started up and no track has been loaded, it will report a BPM of 65535.</p>
     *
     * @return the track BPM to two decimal places multiplied by 100
     */
    public abstract int getBpm();

    /**
     * Is this device reporting itself to be the current tempo master?
     *
     * @return {@code true} if the device that sent this update is the master
     * @throws  IllegalStateException if called with a {@link Beat} and the {@link VirtualCdj} is not running, since
     *          that is needed to find the latest status update from the device which sent the beat packet.
     */
    public abstract boolean isTempoMaster();

    /**
     * Is this device reporting itself synced to the current tempo master?
     *
     * @return {@code true} if the device that sent this update is synced
     * @throws  IllegalStateException if called with a {@link Beat} and the {@link VirtualCdj} is not running, since
     *          that is needed to find the latest status update from the device which sent the beat packet.
     */
    public abstract boolean isSynced();

    /**
     * If this packet indicates the device in the process of yielding the tempo master role to another player,
     * this will hold the device number of that player, otherwise it will be {@code null}.
     *
     * @return the device number, if any, this update is yielding the tempo master role to
     */
    public abstract Integer getDeviceMasterIsBeingYieldedTo();

    /**
     * Get the effective tempo reflected by this update, which reflects both its track BPM and pitch as needed.
     *
     * @return the beats per minute this device is reporting
     */
    public abstract double getEffectiveTempo();

    /**
     * Get the position within a measure of music at which the most recent beat fell (a value from 1 to 4, where 1 represents
     * the down beat). This value will be accurate for players when the track was properly configured within rekordbox
     * (and if the music follows a standard House 4/4 time signature). The mixer makes no effort to synchronize
     * down beats with players, however, so this value is meaningless when coming from the mixer. The usefulness of
     * this value can be checked with {@link #isBeatWithinBarMeaningful()}.
     *
     * @return the beat number within the current measure of music
     */
    public abstract int getBeatWithinBar();

    /**
     * Returns {@code true} if this update is coming from a device where {@link #getBeatWithinBar()} can reasonably
     * be expected to have musical significance, because it respects the way a track was configured within rekordbox.
     *
     * @return true for status packets from players, false for status packets from mixers
     */
    @SuppressWarnings("WeakerAccess")
    public abstract boolean isBeatWithinBarMeaningful();

    @Override
    public String toString() {
        return "DeviceUpdate[deviceNumber:" + deviceNumber +
                ", deviceName:" + deviceName + ", address:" + address.getHostAddress() +
                ", timestamp:" + timestamp + ", beatWithinBar:" + getBeatWithinBar() +
                ", isBeatWithinBarMeaningful: " + isBeatWithinBarMeaningful() + ", effectiveTempo:" +
                getEffectiveTempo() + ", isTempoMaster:" + isTempoMaster();
    }
}