/* Copyright (c) 2011 Danish Maritime Authority.
 *
 * Licensed 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.
 */
package dk.dma.ais.tracker.targetTracker;

import dk.dma.ais.data.AisTarget;
import dk.dma.ais.message.AisMessage;
import dk.dma.ais.message.AisMessage24;
import dk.dma.ais.message.AisPositionMessage;
import dk.dma.ais.message.AisStaticCommon;
import dk.dma.ais.message.AisTargetType;
import dk.dma.ais.message.IVesselPositionMessage;
import dk.dma.ais.packet.AisPacket;
import dk.dma.ais.packet.AisPacketSource;
import dk.dma.ais.tracker.Target;
import dk.dma.enav.model.Country;
import dk.dma.enav.model.geometry.Position;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import static java.util.Objects.requireNonNull;

/**
 * Immutable information about a target. Whenever we receive a new message from the target. We create a new TargetInfo
 * instance via {@link #updateTarget(TargetInfo, AisPacket, AisTargetType, long, AisPacketSource, Map)}.
 *
 * @author Kasper Nielsen
 */
public class TargetInfo extends Target {
    
    /** serialVersionUID. */
    private static final long serialVersionUID = 2L;

    /**
     * The target type of the info, is never null.
     */
    final AisTargetType targetType;

    /**
     * The latest position and time stamp that was received Packet that was received.
     */
    final long positionTimestamp;
    /**
     * The Position packet.
     */
    final byte[] positionPacket;
    /**
     * The Position.
     */
    final Position position;

    /**
     * The Cog.
     */
    final float cog;
    /**
     * The Heading.
     */
    final int heading;
    /**
     * The Nav status.
     */
    final byte navStatus;
    /**
     * The Sog.
     */
    final float sog;

    /**
     * The Ais target.
     */
//Do not want serialization of mutable complex object
    transient volatile AisTarget aisTarget;
    /**
     * The Position ais packet.
     */
//further caching
    transient volatile AisPacket positionAisPacket;
    /**
     * The Static ais packet 1.
     */
    transient volatile AisPacket staticAisPacket1;
    /**
     * The Static ais packet 2.
     */
    transient volatile AisPacket staticAisPacket2;

    /**
     * The constant staticTimestamp.
     */
// The latest static info
    final long staticTimestamp;
    /**
     * The Static data 1.
     */
    final byte[] staticData1;
    /**
     * The Static data 2.
     */
    final byte[] staticData2;
    /**
     * The Static ship type.
     */
    final int staticShipType;

    /**
     * The Packet source.
     */
    final AisPacketSource packetSource;

    /**
     * Gets packet source.
     *
     * @return the packetSource
     */
    public AisPacketSource getPacketSource() {
        return packetSource;
    }

    private TargetInfo(AisPacketSource packetSource, int mmsi, AisTargetType targetType, long positionTimestamp, Position p, int heading, float cog,
            float sog, byte navStatus, byte[] positionPacket, long staticTimestamp, byte[] staticData1,
            byte[] staticData2, int staticShipType) {
        super(mmsi);

        this.packetSource=packetSource;
                
        this.targetType = requireNonNull(targetType);

        // Position data
        this.positionTimestamp = positionTimestamp;
        this.position = p;
        this.heading = heading;
        this.cog = cog;
        this.sog = sog;
        this.navStatus = navStatus;
        this.positionPacket = positionPacket;

        // Static Data
        this.staticTimestamp = staticTimestamp;
        this.staticData1 = staticData1;
        this.staticData2 = staticData2;
        this.staticShipType = staticShipType;
        
        // Caching for AisPackets
        //this.staticAisPacket1 = getStaticAisPacket1();
        //this.staticAisPacket2 = getStaticAisPacket2();
        //this.positionAisPacket = getPositionPacket();
        
        // Caching for getAisTarget()
        //this.aisTarget = getAisTarget();
    }

    /**
     * Returns the target type of the target.
     *
     * @return target type
     */
    public AisTargetType getTargetType() {
        return targetType;
    }

    /**
     * Gets position timestamp.
     *
     * @return the position timestamp
     */
    public long getPositionTimestamp() {
        return positionTimestamp;
    }

    /**
     * Gets position.
     *
     * @return the position
     */
    public Position getPosition() {
        return position;
    }

    /**
     * Gets cog.
     *
     * @return the cog
     */
    public float getCog() {
        return cog;
    }

    /**
     * Gets heading.
     *
     * @return the heading
     */
    public int getHeading() {
        return heading;
    }

    /**
     * Gets nav status.
     *
     * @return the nav status
     */
    public byte getNavStatus() {
        return navStatus;
    }

    /**
     * Gets sog.
     *
     * @return the sog
     */
    public float getSog() {
        return sog;
    }

    /**
     * Gets static timestamp.
     *
     * @return the static timestamp
     */
    public long getStaticTimestamp() {
        return staticTimestamp;
    }

    /**
     * Gets static ship type.
     *
     * @return the static ship type
     */
    public int getStaticShipType() {
        return staticShipType;
    }

    /**
     * Gets ais target.
     *
     * @return the ais target
     */
    public AisTarget getAisTarget() {
        AisTarget t = aisTarget;
        return t == null ?aisTarget = TargetInfoToAisTarget.generateAisTarget(this) : t;
    }

    /**
     * Returns the country of the vessel based on its MMSI number.
     *
     * @return the country of the vessel based on its MMSI number
     */
    public Country getCountry() {
        return Country.getCountryForMmsi(getMmsi());
    }

    /**
     * Returns the latest received position packet. Or <code>null</code> if no position has been received from the
     * vessel.
     *
     * @return the latest received position packet
     */
    public AisPacket getPositionPacket() {
        byte[] positionPacket = this.positionPacket;
        if (positionPacket != null) {
            AisPacket positionAisPacket = this.positionAisPacket;
            return positionAisPacket == null ? positionAisPacket = AisPacket.fromByteArray(positionPacket): positionAisPacket;
        }
        return null;
    }

    /**
     * Returns the number of static packets we have received. Either return 0 if we have received 0 packets. 1 if we
     * have received a single packet. Or 2 we if have received two packets (AisMessage type 24).
     *
     * @return the number of static packets we have received
     */
    public int getStaticCount() {
        return staticData1 == null ? 0 : staticData2 == null ? 1 : 2;
    }

    /**
     * Returns any static packets we have received from the target.
     *
     * @return the ais packet [ ]
     */
    public AisPacket[] getStaticPackets() {
        if (staticData1 == null) {
            return new AisPacket[0];
        } else if (staticData2 == null) {
            return new AisPacket[] { getStaticAisPacket1() };
        } else {
            return new AisPacket[] { getStaticAisPacket1(), getStaticAisPacket2() };
        }
    }

    /**
     * Get packets ais packet [ ].
     *
     * @return the ais packet [ ]
     */
    public AisPacket[] getPackets() {
        if (!hasPositionInfo()) {
            return getStaticPackets();
        } 
        
        Collection<AisPacket> packets = new ArrayList<>();
        
        for (AisPacket p: getStaticPackets()) {
            packets.add(p);
        }
        
        packets.add(getPositionPacket());
        
        return packets.toArray(new AisPacket[packets.size()]);
    }

    /**
     * Gets static ais packet 1.
     *
     * @return the static ais packet 1
     */
    public AisPacket getStaticAisPacket1() {
        byte[] staticData1 = this.staticData1;
        if (staticData1 != null) {
            AisPacket staticAisPacket1 = this.staticAisPacket1;
            return staticAisPacket1 == null ? staticAisPacket1 = AisPacket.fromByteArray(staticData1): staticAisPacket1;
        }
        return null;
    }

    /**
     * Gets static ais packet 2.
     *
     * @return the static ais packet 2
     */
    public AisPacket getStaticAisPacket2() {
        byte[] staticData2 = this.staticData2;
        if (staticData2 != null) {
            AisPacket staticAisPacket2 = this.staticAisPacket2;
            return staticAisPacket2 == null ? staticAisPacket2 = AisPacket.fromByteArray(staticData2): staticAisPacket2;
        }
        return null;
    }

    /**
     * Returns true if we have positional information.
     *
     * @return true if we have positional information
     */
    public boolean hasPositionInfo() {
        return positionPacket != null;
    }

    /**
     * Returns true if we have static information.
     *
     * @return true if we have static information
     */
    public boolean hasStaticInfo() {
        return staticData1 != null;
    }

    /**
     * Merge target info.
     *
     * @param other the other
     * @return the target info
     */
    TargetInfo merge(TargetInfo other) {
        if (positionTimestamp >= other.positionTimestamp && staticTimestamp >= other.staticTimestamp) {
            return this;
        } else if (other.positionTimestamp >= positionTimestamp && other.staticTimestamp >= staticTimestamp) {
            return other;
        }
        return positionTimestamp >= other.positionTimestamp ? mergeWithStaticFrom(other) : other
                .mergeWithStaticFrom(this);
    }

    /**
     * Copies the static information from the specified target into a new target. Maintaining the position information
     * of the current target.
     *
     * @param other the other
     * @return the new target
     */
    TargetInfo mergeWithStaticFrom(TargetInfo other) {
        //assert packetSource == other.packetSource
        return new TargetInfo(packetSource, getMmsi(), targetType, positionTimestamp, position, heading, cog, sog, navStatus,
                positionPacket, other.staticTimestamp, other.staticData1, other.staticData2, other.staticShipType);
    }

    /**
     * We have received a new AIS message from a target.
     *
     * @param existing   non-null if an existing target with the same MMSI number already exists
     * @param packet     the packet that was received
     * @param targetType the type of target
     * @param timestamp  the timestamp of the packet
     * @param source     the source of the packet
     * @param msg24Part0 a map of cached message type 24 static part 0 messages
     * @return a new target info
     */
    static TargetInfo updateTarget(TargetInfo existing, AisPacket packet, AisTargetType targetType, long timestamp,
            AisPacketSource source, Map<AisPacketSource, byte[]> msg24Part0) {
        AisMessage message = packet.tryGetAisMessage();// is non-null
        int mmsi = message.getUserId();
        // ATON and BS targets are easy to handle because they do not contain much other than a position
        if (targetType == AisTargetType.ATON || targetType == AisTargetType.BS) {

            // make sure it has a never timestamp
            if (existing != null && timestamp <= existing.positionTimestamp) {
                return existing;
            }

            return new TargetInfo(source, mmsi, targetType, timestamp, message.getValidPosition(), -1, -1, -1, (byte) -1,
                    packet.toByteArray(), -1, null, null, -1);
        }
        TargetInfo result = updateTargetWithPosition(existing, packet, message, mmsi, targetType, timestamp, source);
        return updateTargetWithStatic(packet, message, mmsi, targetType, timestamp, source, result, msg24Part0);
    }

    /**
     * Update target with position target info.
     *
     * @param existing   the existing
     * @param packet     the packet
     * @param message    the message
     * @param mmsi       the mmsi
     * @param targetType the target type
     * @param timestamp  the timestamp
     * @param source     the source
     * @return the target info
     */
    static TargetInfo updateTargetWithPosition(TargetInfo existing, AisPacket packet, AisMessage message, int mmsi,
            AisTargetType targetType, long timestamp, AisPacketSource source) {
        if (message instanceof IVesselPositionMessage) {
            // only update if never timestamp
            if (existing == null || timestamp >= existing.positionTimestamp) {
                // check if another targetType in which case we drop the existing one
                // we need to have this check, after having checked the timestamp
                if (existing != null && existing.targetType != targetType) {
                    existing = null;
                }
                IVesselPositionMessage ivm = (IVesselPositionMessage) message;
                Position p = message.getValidPosition();
                int heading = ivm.getTrueHeading();
                float cog = ivm.getCog();
                float sog = ivm.getSog();
                byte navStatus = message instanceof AisPositionMessage ? (byte) ((AisPositionMessage) message)
                        .getNavStatus() : (byte) -1;

                if (existing == null) {
                    return new TargetInfo(source, mmsi, targetType, timestamp, p, heading, cog, sog, navStatus,
                            packet.toByteArray(), -1, null, null, -1);
                } else {
                    return new TargetInfo(source, mmsi, targetType, timestamp, p, heading, cog, sog, navStatus,
                            packet.toByteArray(), existing.staticTimestamp, existing.staticData1, existing.staticData2,
                            existing.staticShipType);
                }
            }
        }
        return existing;
    }

    /**
     * Update target with static target info.
     *
     * @param packet     the packet
     * @param message    the message
     * @param mmsi       the mmsi
     * @param targetType the target type
     * @param timestamp  the timestamp
     * @param source     the source
     * @param existing   the existing
     * @param msg24Part0 the msg 24 part 0
     * @return the target info
     */
    static TargetInfo updateTargetWithStatic(AisPacket packet, AisMessage message, int mmsi, AisTargetType targetType,
            long timestamp, AisPacketSource source, TargetInfo existing, Map<AisPacketSource, byte[]> msg24Part0) {
        if (message instanceof AisStaticCommon) {
            // only update if never timestamp
            if (existing == null || timestamp >= existing.staticTimestamp) {
                // check if another targetType in which case we drop the existing one
                // we need to have this check, after having checked the timestamp
                if (existing != null && existing.targetType != targetType) {
                    existing = null;
                }

                AisStaticCommon c = (AisStaticCommon) message;
                byte[] static0;
                byte[] static1 = null;
                if (c instanceof AisMessage24) {
                    // AisMessage24 is split into 2 parts, if we get a part 0.
                    // Save in a hash table, where we keep it until we receive part 1
                    if (((AisMessage24) c).getPartNumber() == 0) {
                        msg24Part0.put(source, packet.toByteArray());
                        // we know that existing have not been updated by updateTargetWithPosition because
                        // AisMessage24 only contains static information, so existing=original
                        return existing; // the target is updated when we receive part 1
                    } else {
                        static0 = msg24Part0.remove(source);
                        if (static0 == null) {
                            return existing;// We do not have the first part:(
                        }
                        static1 = packet.toByteArray();
                    }
                } else {
                    static0 = packet.toByteArray();
                }

                if (existing == null) {
                    return new TargetInfo(source, mmsi, targetType, -1, null, -1, -1, -1, (byte) -1, null, timestamp, static0,
                            static1, c.getShipType());
                } else {
                    return new TargetInfo(source, mmsi, existing.targetType, existing.positionTimestamp, existing.position,
                            existing.heading, existing.cog, existing.sog, existing.navStatus, existing.positionPacket,
                            timestamp, static0, static1, c.getShipType());
                }
            }
        }
        return existing;
    }

}
