package org.etsi.its.adapter.layers;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.TimeoutException;

import org.etsi.adapter.TERFactory;
import org.etsi.common.ByteHelper;
import org.etsi.its.adapter.IManagementLayers;
import org.etsi.its.adapter.PcapMultiplexer;

import com.commsignia.v2x.client.ITSApplication;
import com.commsignia.v2x.client.MessageSet;
import com.commsignia.v2x.client.exception.ClientException;
import com.commsignia.v2x.client.model.BTPType;
import com.commsignia.v2x.client.model.InjectData;
import com.commsignia.v2x.client.model.InjectData.Builder;
import com.commsignia.v2x.client.model.InjectData.Type;
import com.commsignia.v2x.client.model.dev.DeviceInfoResponse;
import com.commsignia.v2x.client.model.dev.FacilityModule;

public class CommsigniaLayer extends Layer implements IEthernetSpecific {
    
    private static final byte[] DeviceMacAddress = new byte[] { (byte)0x70, (byte)0xb3, (byte)0xd5, (byte)0xf2, (byte)0xa1, (byte)0xe3 };
    private static final String TargetHost = "172.17.15.38";
    private static final int TargetPort = 7942; 
    private static final int SourcePort = 7943; 
    private static final int ItsAid = 5; 
    private static final int TxPowerDbm = -10; // Max value: -33dBm, RSU: -30dBm, Lab: -10dBm
    private static String pcapFilter = null;
    
    /**
     * Well-known Ethernet broadcast address
     */
    public static byte[] MAC_BROADCAST = new byte[]{(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF,(byte)0xFF};
    
    /**
     * Parameter name for Link-Layer destination
     */
    public static final String LINK_LAYER_DESTINATION = "LinkLayerDestination";
    
    private Map<String, byte[]> ClientsToMacs = new HashMap<String, byte[]>();
    private Map<String, Short> ClientsToFrameTypes = new HashMap<String, Short>();
    private HashMap<String, Layer> ClientsToLayers = new HashMap<String, Layer>();
    
    /**
     * Constructor
     * @param  management   Layer management instance
     * @param  lowerStack   Lower protocol stack   
     */
    public CommsigniaLayer(IManagementLayers management, Stack<String> lowerStack) {
        super(management, lowerStack);
        
        MessageSet defaultMessageSet = MessageSet.C;
        itsApplication = new ITSApplication(ItsAid, TargetHost, TargetPort, defaultMessageSet);
        try {
            itsApplication.connect(1000);
            itsApplication.registerBlocking();
            DeviceInfoResponse deviceInfoResponse = itsApplication.requestDeviceInfoBlocking();
            System.out.println(deviceInfoResponse);
            itsApplication.setFacilityModuleStatus(FacilityModule.BSM, false);
            itsApplication.setFacilityModuleStatus(FacilityModule.CAM, false);
//            itsApplication.addEventListener(new ITSEventAdapter() {
//                @Override
//                public void onGnNotification(GNNotification notification) {
//                    ByteBuffer buffer = ByteBuffer.wrap(notification.getData());
//                    
//                    System.out.printf("GN GBC receive. GN address: %s Sequence number: %d RSSI: %d dBm\n",
//                            notification.getGNAddress(),
//                            buffer.getInt(),
//                            notification.getRssi()
//                    );
//                }
//            });
            itsApplication. gnBindBlocking(BTPType.NONE, 65535);
        } catch (TimeoutException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
    }
    
    /* (non-Javadoc)
     * @see org.etsi.its.adapter.layers.Layer#register(org.etsi.its.adapter.layers.Layer)
     */
    @Override
    public void register(Layer upperLayer) {
        TERFactory.getInstance().logDebug(">>> CommsigniaLayer.register: " + upperLayer);
        
        if(registeredUpperLayer == null) {
            super.register(upperLayer);
            
            try {
                Method getEthernetType = registeredUpperLayer.getClass().getMethod("getEthernetType", (Class<?>[])null);
                if (getEthernetType != null) {
                    upperLayerFrameType = (Short) getEthernetType.invoke(registeredUpperLayer, (Object[]) null); 
                }
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
            
            localMacAddress = management.getLinkLayerAddress();
            PcapMultiplexer.getInstance().register(this, DeviceMacAddress, this.getEthernetType());
        }
        
        if(ClientsToMacs.isEmpty()) {
            pcapFilter = "udp dst port " + SourcePort + " and ";
        } else {
            TERFactory.getInstance().logDebug("CommsigniaLayer.register: Another Client !");
            pcapFilter = pcapFilter + " and ";
        }
        // Update Filter
        String strMacAddress = String.format("%02x", localMacAddress[0]);
        for(int i=1; i < localMacAddress.length; i++) {
            strMacAddress += String.format(":%02x", localMacAddress[i]);
        }
        //udp dst port 74 && wlan src 8b:ad:f0:0d:01:02
        pcapFilter = pcapFilter + "not wlan src " + "8b:ad:f0:0d:01:03";//strMacAddress;
        // Reset filter
        PcapMultiplexer.getInstance().resetFilter(pcapFilter);
        // Register client
        ClientsToMacs.put(this.toString(), localMacAddress);
        ClientsToLayers.put(this.toString(), upperLayer);
        ClientsToFrameTypes.put(this.toString(), upperLayerFrameType);
    }
    
    /* (non-Javadoc)
     * @see org.etsi.its.adapter.layers.Layer#receive(byte[])
     */
    @Override
    public void receive(byte[] message, Map<String, Object> lowerInfo) {
        TERFactory.getInstance().logDebug(">>> CommsigniaLayer.receive: " + ByteHelper.byteArrayToString(message));
        
        ByteBuffer byteBuffer = ByteBuffer.wrap(message);
        
        // Skip C2P protocol
        byteBuffer.position(
            20 + // IP Layer:           45 00 01 1f 13 8c 00 00 80 11 6b 0b ac 11 0f 26 ff ff ff ff
            8  + // UDP Layer:          75 30 1f 07 01 0b a6 cd
            29 + // C2P Layer:          12 00 00 33 41 00 00 03 5c ac 00 02 0c 02 35 a4 e9 01 6b 49 d2 01 3f ff 00 00 7f ff 16
            4   // IEEE 802.11L Layer: 88 00 00 00 00
        );
        
        // Extract Dst
        byte[] dst = new byte[6];
        byteBuffer.get(dst, 0, dst.length);
        lowerInfo.put(EthernetLayer.LINK_LAYER_DESTINATION, dst);
        
        // Skip Src
        byteBuffer.position(byteBuffer.position() + 6);
        
        // Skip IEEE 802.11L Layer
        byteBuffer.position(byteBuffer.position() + 10);
        
        // Skip LLC header
        byteBuffer.position(byteBuffer.position() + 6);
        
        // Extract FrameType info
        byte[] rawFrameType = new byte[2];
        byteBuffer.get(rawFrameType, 0, rawFrameType.length);
        short frameType = ByteHelper.byteArrayToInt(rawFrameType).shortValue();
        
        // Extract Data
        byte[] data = new byte[byteBuffer.remaining()];
        byteBuffer.get(data, 0, byteBuffer.remaining());
        
        // Dispatch
        for (String mapKey : ClientsToMacs.keySet()) {
            if(frameType == ClientsToFrameTypes.get(mapKey)) {
                if(Arrays.equals(dst, MAC_BROADCAST) || Arrays.equals(dst, ClientsToMacs.get(mapKey))) {
                    ClientsToLayers.get(mapKey).receive(data, lowerInfo);
                }
            }
        }
        
    }
    
    /* (non-Javadoc)
     * @see org.etsi.its.adapter.layers.Layer#send(byte[], java.util.Map)
     */
    @Override
    public boolean send(byte[] message, Map<String, Object> params) {
        //TERFactory.getInstance().logDebug(">>> CommsigniaLayer.send: " + ByteHelper.byteArrayToString(message));
        
        byte[] dst = (byte[])params.get(LINK_LAYER_DESTINATION);
        if(dst == null) {
            dst = MAC_BROADCAST;
        }
            
        byte[] packet = ByteHelper.concat(
                dst,
                localMacAddress, 
                ByteHelper.intToByteArray(upperLayerFrameType, 2), 
                message
        );
        
        try {
            String dstAddress = String.format(
                "%02x:%02x:%02x:%02x:%02x:%02x", 
                dst[0], 
                dst[1], 
                dst[2], 
                dst[3], 
                dst[4], 
                dst[5]);
            String srcAddress = String.format(
                "%02x:%02x:%02x:%02x:%02x:%02x", 
                localMacAddress[0], 
                localMacAddress[1], 
                localMacAddress[2], 
                localMacAddress[3], 
                localMacAddress[4], 
                localMacAddress[5]);
            Builder build = new InjectData.Builder();
            build
                .withDstAddress(dstAddress)
                .withSrcAddress(srcAddress)
                .withType(Type.GNP)
                .withInterfaceID(2)
                .withTxPowerDbm(TxPowerDbm) 
                .withData(message);
            InjectData injectData = build.build();
            //TERFactory.getInstance().logDebug("CommsigniaLayer.send: " + ByteHelper.byteArrayToString(injectData.getData()));
            
            itsApplication.sendOnRadioBlocking(injectData);
            //TERFactory.getInstance().logDebug("<<< CommsigniaLayer.send: " + ByteHelper.byteArrayToString(packet));
            return true;//super.send(packet, params);
        } catch (ClientException e) {
            e.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        
        return false;
    }
    
    /* (non-Javadoc)
     * @see org.etsi.its.adapter.layers.Layer#unregister(org.etsi.its.adapter.layers.Layer)
     */
    @Override
    public void unregister(Layer upperLayer) {
        PcapMultiplexer.getInstance().unregister(this);
        if(ClientsToMacs.containsKey(this.toString())) {
            ClientsToMacs.remove(this.toString());
            ClientsToFrameTypes.remove(this.toString());
            ClientsToLayers.remove(this.toString());
            
            if(ClientsToMacs.isEmpty()) {
                if (itsApplication != null) {
                    try {
                        itsApplication.gnCloseBlocking(BTPType.NONE, 65535);
                        itsApplication.deregisterBlocking();
                    } catch (ClientException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    itsApplication.shutdown();
                    itsApplication = null;
                }
            }
        }
    }
    
    /* (non-Javadoc)
     * @see org.etsi.its.adapter.ports.IEthernetSpecific#getEthernetType()
     */
    @Override
    public short getEthernetType() {
        return (short)0x0800;
    }

    /**
     * Local Ethernet address 
     */
    private byte[] localMacAddress;
    
    /**
     * Upper layer's frame type
     */
    private short upperLayerFrameType;
    
    private ITSApplication itsApplication = null;
    
}
