/* 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.transform;

import dk.dma.ais.binary.SixbitException;
import dk.dma.ais.message.AisBinaryMessage;
import dk.dma.ais.message.AisMessage;
import dk.dma.ais.message.AisMessage5;
import dk.dma.ais.message.AisStaticCommon;
import dk.dma.ais.message.binary.AisApplicationMessage;
import dk.dma.ais.packet.AisPacket;
import dk.dma.ais.proprietary.IProprietaryTag;
import dk.dma.ais.sentence.CommentBlock;
import dk.dma.ais.sentence.Vdm;
import dk.dma.enav.model.Country;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

/**
 * Transformer that anonymizes ais packets
 */
@ThreadSafe
public class AnonymousTransformer implements IAisPacketTransformer {

    private static final Logger LOG = LoggerFactory.getLogger(AnonymousTransformer.class);

    /**
     * List of available MID's
     */
    private static final ArrayList<Integer> MID_LIST = new ArrayList<>(Country.getMidMap().keySet());

    /**
     * List of random names
     */
    private static final ArrayList<String> NAME_LIST = new ArrayList<>();

    /**
     * Location of random names file
     */
    private static final String LOCATION = AnonymousTransformer.class.getPackage().getName().replace(".", "/")
            + "/names.txt";

    /**
     * Load names and make random order
     */
    static {
        URL url = ClassLoader.getSystemResource(LOCATION);
        if (url == null) {
            url = Thread.currentThread().getContextClassLoader().getResource(LOCATION);
        }
        if (url == null) {
            throw new Error("Could not locate " + LOCATION + " on classpath");
        }

        try {
            InputStreamReader in = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8);
            BufferedReader reader = new BufferedReader(in);
            String line;
            Set<String> names = new HashSet<>();
            while ((line = reader.readLine()) != null) {
                names.add(line.trim().toUpperCase());
            }
            NAME_LIST.addAll(names);
            Collections.shuffle(NAME_LIST);
        } catch (IOException e) {
            throw new Error("Failed to load random names list: " + e.getMessage());
        }
    }

    /**
     * Map from MMSI to anonymized data
     */
    @GuardedBy("this")
    private final Map<Integer, AnonData> anonDataMap = new HashMap<>();

    /**
     * Counter to keep track of MMSI to anonymous MMSI map
     */
    @GuardedBy("this")
    private int counter;

    /**
     * Random number generator
     */
    private final Random rand;

    /**
     * Instantiates a new Anonymous transformer.
     */
    public AnonymousTransformer() {
        rand = new Random(System.currentTimeMillis());
    }

    @Override
    public AisPacket transform(AisPacket packet) {
        // Get AIS message or discard
        AisMessage message = packet.tryGetAisMessage();
        if (message == null) {
            return null;
        }

        // Get anonymized data for target
        AnonData anonData = getAnonData(message);

        // Transformation for all mesasges
        anonymize(message, anonData);

        // Transformation for general static data
        if (message instanceof AisStaticCommon) {
            anonymize((AisStaticCommon) message, anonData);
        }

        // Transformation for class A static data
        if (message instanceof AisMessage5) {
            anonymize((AisMessage5) message, anonData);
        }

        return createPacket(message, packet);
    }

    private synchronized AnonData getAnonData(AisMessage message) {
        // Try to get from map
        AnonData anonData = anonDataMap.get(message.getUserId());
        if (anonData != null) {
            return anonData;
        }
        // Make anonymized data
        String name = makeName(counter);
        int mmsi = makeMmsi(counter++);
        String callsign = makeCallsign();
        String destination = makeDestination();
        int imoNo = counter;

        anonData = new AnonData(mmsi, name, imoNo, callsign, destination);
        anonDataMap.put(message.getUserId(), anonData);
        return anonData;
    }

    /**
     * Make MMSI from random country and counter
     * 
     * @param id
     * @return
     */
    private int makeMmsi(int id) {
        return MID_LIST.get(rand.nextInt(MID_LIST.size())) * 1000000 + id;
    }

    /**
     * Get random name from list of names
     * 
     * @return
     */
    private String makeName(int id) {
        return NAME_LIST.get(id % NAME_LIST.size());
    }

    /**
     * Make random callsign
     * 
     * @return
     */
    private String makeCallsign() {
        return UUID.randomUUID().toString().substring(0, 5).toUpperCase();
    }

    /**
     * Make random destination
     */
    private String makeDestination() {
        return "N/A";
    }

    /**
     * Do transformation basic for all AIS messages
     * 
     * @param message
     */
    private void anonymize(AisMessage message, AnonData anonData) {
        message.setUserId(anonData.getMmsi());
    }

    /**
     * 
     * @param posMessage
     */
    private void anonymize(AisStaticCommon message, AnonData anonData) {
        // Change name
        message.setName(anonData.getName());
        // Callsign
        message.setCallsign(anonData.getCallsign());
    }

    private void anonymize(AisMessage5 message, AnonData anonData) {
        // IMO
        message.setImo(anonData.getImoNo());
        // Dest
        message.setDest(anonData.getDestination());
    }

    /**
     * Make a new packet from old packet and
     * 
     * @param message
     * @param packet
     * @return
     */
    private AisPacket createPacket(AisMessage message, AisPacket packet) {
        List<String> lines = new ArrayList<>(4);
        int sequence = message.getVdm().getSequence();

        // Handle binary message
        if (message instanceof AisBinaryMessage) {
            try {
                AisApplicationMessage appMessage = ((AisBinaryMessage) message).getApplicationMessage();
                if (appMessage == null) {
                    return null;
                }
            } catch (SixbitException e) {
                return null;
            }
        }

        String[] sentences;
        try {
            sentences = Vdm.createSentences(message, sequence);
        } catch (UnsupportedOperationException e) {
            LOG.debug("Encoding not supported by message type " + message.getMsgId());
            return null;
        } catch (SixbitException e) {
            LOG.error("Failed to encode AIS message: " + message + ": " + e.getMessage());
            return null;
        }

        // Add proprietary tags (or not?)
        if (message.getTags() != null) {
            for (IProprietaryTag tag : message.getTags()) {
                lines.add(tag.getSentence());
            }
        }

        // Add comment blocks
        CommentBlock cb = message.getVdm().getCommentBlock();
        if (cb != null) {
            lines.add(cb.encode());
        }

        // Add sentences
        for (String sentence : sentences) {
            lines.add(sentence);
        }

        return AisPacket.from(StringUtils.join(lines, "\r\n"));
    }

    /**
     * Class holding anonymized data for target
     */
    private class AnonData {
        private final int mmsi;
        private final String name;
        private final int imoNo;
        private final String callsign;
        private final String destination;

        /**
         * Instantiates a new Anon data.
         *
         * @param mmsi        the mmsi
         * @param name        the name
         * @param imoNo       the imo no
         * @param callsign    the callsign
         * @param destination the destination
         */
        public AnonData(int mmsi, String name, int imoNo, String callsign, String destination) {
            this.mmsi = mmsi;
            this.name = name;
            this.imoNo = imoNo;
            this.callsign = callsign;
            this.destination = destination;
        }

        /**
         * Gets mmsi.
         *
         * @return the mmsi
         */
        public int getMmsi() {
            return mmsi;
        }

        /**
         * Gets name.
         *
         * @return the name
         */
        public String getName() {
            return name;
        }

        /**
         * Gets imo no.
         *
         * @return the imo no
         */
        public int getImoNo() {
            return imoNo;
        }

        /**
         * Gets callsign.
         *
         * @return the callsign
         */
        public String getCallsign() {
            return callsign;
        }

        /**
         * Gets destination.
         *
         * @return the destination
         */
        public synchronized String getDestination() {
            return destination;
        }

    }

}
