package jp.sourceforge.shovel.logic.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.seasar.framework.container.S2Container;

import jp.sourceforge.shovel.GrowlPriorityType;
import jp.sourceforge.shovel.GrowlType;
import jp.sourceforge.shovel.entity.IGrowlPacket;
import jp.sourceforge.shovel.exception.ApplicationException;
import jp.sourceforge.shovel.io.ByteBuilder;
import jp.sourceforge.shovel.logic.IGrowlLogic;

public class GrowlLogicImpl implements IGrowlLogic {
    final static int UDP_PORT = 9887;
    final static byte PROTOCOL_VERSION = 1;
    final static String ENCODING = "UTF-8";

    public IGrowlPacket createPacket(String application, GrowlType growlType, String password) {
        IGrowlPacket packet = (IGrowlPacket)getContainer().getComponent(IGrowlPacket.class);
        
        packet.setApplication(application);
        packet.setPassword(password);
        packet.setGrowlType(growlType);
        
        return packet;
    }
    public IGrowlPacket createPacket(String application, GrowlType growlType, String notification,
            String title, String description, GrowlPriorityType priorityType, boolean sticky, String password) {
        IGrowlPacket packet = (IGrowlPacket)getContainer().getComponent(IGrowlPacket.class);
        
        packet.setApplication(application);
        packet.setNotification(notification);
        packet.setPassword(password);
        packet.setGrowlType(growlType);
        packet.setTitle(title);
        packet.setDescription(description);
        packet.setPriorityType(priorityType);
        packet.setSticky(sticky);
        
        return packet;
    }
    
    byte[] payload(ByteBuilder bb, IGrowlPacket packet) throws ApplicationException {
        InputStream in = null;
        ApplicationException re = null;
        try {
            ByteBuilder fb = new ByteBuilder();
            fb.Append(bb.GetBytes());
            
            String algorithm = null;
            switch (packet.getGrowlType()) {
            case NOTIFICATION:
            case REGISTRATION:
                algorithm = "MD5";
                break;
            case NOTIFICATION_SHA256:
            case REGISTRATION_SHA256:
                algorithm = "SHA-256";
                break;
            }
            
            if (algorithm != null) {
                ByteBuilder bpb = new ByteBuilder();
                bpb.Append(bb.GetBytes());
                bpb.Append(packet.getPassword());
                
                in = new ByteArrayInputStream(bpb.GetBytes());
                MessageDigest digest = MessageDigest.getInstance(algorithm);
                byte[] bytes = new byte[4096];
                int length = 0;
                while ((length = in.read(bytes, 0, bytes.length)) >= 0) {
                    digest.update(bytes, 0, length);
                }
                byte[] checksum = digest.digest();
                
                fb.Append(checksum);
            }
            
            return fb.GetBytes();
        } catch (NoSuchAlgorithmException e) {
            re = new ApplicationException("");
            throw re;
        } catch (IOException e) {
            re = new ApplicationException("");
            throw re;
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                if (re == null) {
                    throw new ApplicationException("");
                }
                throw re;
            }
        }
    }
    
    byte[] payloadRegistration(IGrowlPacket packet) throws ApplicationException {
        ByteBuilder nb = new ByteBuilder();
        ByteBuilder db = new ByteBuilder();
        int index = 0;
        byte notificationCount = 0;
        byte enabledNotificationCount = 0;
        
        Map<String, Boolean> notificationMap = packet.getNotifications();
        Set<Entry<String, Boolean>> notificationEntries = notificationMap.entrySet();
        
        try {
            for (Entry<String, Boolean> notificationEntry : notificationEntries) {
                byte[] notification = notificationEntry.getKey().getBytes(ENCODING);
                nb.Append(notification.length);
                nb.Append(notification);
    
                notificationCount++;
                if (notificationEntry.getValue()) {
                    db.Append((byte)index);
                    enabledNotificationCount++;
                }
                index++;
            }
            
            byte[] application = packet.getApplication().getBytes(ENCODING);
            
            ByteBuilder bb = new ByteBuilder();
            bb.Append(PROTOCOL_VERSION);
            bb.Append(packet.getGrowlType().getId());
            bb.Append(application.length);
            bb.Append(notificationCount);
            bb.Append(enabledNotificationCount);
            bb.Append(application);
            bb.Append(nb.GetBytes());
            bb.Append(db.GetBytes());

            return payload(bb, packet);
        } catch (UnsupportedEncodingException e) {
            throw new ApplicationException("");
        }
    }
    
    byte[] payloadNotification(IGrowlPacket packet) throws ApplicationException {
        int flags = ConvertPriorityFlags(packet.getPriorityType());
        if (packet.isSticky()) {
            flags |= 1;
        }
        
        ByteBuilder bb = new ByteBuilder();
        try {
            byte[] notification = packet.getNotification().getBytes(ENCODING);
            byte[] title = packet.getTitle().getBytes(ENCODING);
            byte[] description = packet.getDescription().getBytes(ENCODING);
            byte[] application = packet.getApplication().getBytes(ENCODING);
            
            bb.Append(PROTOCOL_VERSION);
            bb.Append(packet.getGrowlType().getId());
            bb.Append(flags);
            bb.Append(notification.length);
            bb.Append(title.length);
            bb.Append(description.length);
            bb.Append(application.length);
            bb.Append(notification);
            bb.Append(title);
            bb.Append(description);
            bb.Append(application);
        } catch (UnsupportedEncodingException e) {
            throw new ApplicationException("");
        }
        
        return payload(bb, packet);
    }
    
    static final int ConvertPriorityFlags(GrowlPriorityType priorityType) {
        int flags = (((int)priorityType.getId()) & 7) * 2;
        if (priorityType.getId() < 0) {
            flags |= 8;
        }
        return flags;
    }
    
    public void sendPacket(String host, IGrowlPacket packet) throws ApplicationException {
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket();
            byte[] bytes = null;
            switch (packet.getGrowlType()) {
            case REGISTRATION:
            case REGISTRATION_SHA256:
            case REGISTRATION_NOAUTH:
                bytes = payloadRegistration(packet);
                break;
            case NOTIFICATION:
            case NOTIFICATION_SHA256:
            case NOTIFICATION_NOAUTH:
                bytes = payloadNotification(packet);
                break;
            default:
                throw new ApplicationException("");
            }
            
            socket.send(new DatagramPacket(bytes,
                                           bytes.length,
                                           InetAddress.getByName(host),
                                           UDP_PORT));
        } catch (SocketException e) {
            throw new ApplicationException("");
        } catch (UnknownHostException e) {
            throw new ApplicationException("");
        } catch (IOException e) {
            throw new ApplicationException("");
        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }
    
    /////
    
    S2Container container_;
    
    public void setContainer(S2Container container) {
        container_ = container;
    }
    S2Container getContainer() {
        return container_;
    }
}
