/*
 * ICESOFT COMMERCIAL SOURCE CODE LICENSE V 1.1
 *
 * The contents of this file are subject to the ICEsoft Commercial Source
 * Code License Agreement V1.1 (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.icesoft.com/license/commercial-source-v1.1.html
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * Copyright 2009-2015 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 */

package com.icesoft.icepush;

import java.io.OutputStream;
import java.io.InputStream;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.icepush.NotificationProvider;
import org.icepush.OutOfBandNotifier;
import org.icepush.PushNotification;
import org.icepush.util.ExtensionRegistry;
import java.util.logging.Level;
import java.util.logging.Logger;

public class APNSNotificationProvider implements NotificationProvider {
    private static final Logger LOGGER = Logger.getLogger(APNSNotificationProvider.class.getName());
    // hardcoded as specified by Apple
    private String HOST = "gateway.push.apple.com";
    private int PORT = 2195;
    private static String KEYSTOREPATH = 
            "/WEB-INF/apns-keystore.jks";
    private static String KEYSTOREPASS = 
            "org.icemobile.apns.keyStorePassword";
    private SSLSocketFactory sslFactory;
    OutOfBandNotifier outOfBandNotifier;

    public APNSNotificationProvider(ServletContext servletContext) {

        InputStream keyStream = servletContext
                .getResourceAsStream(KEYSTOREPATH);
        if (null != keyStream)  {
            try {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.log(Level.FINE, "Loading custom APNS keystore " + KEYSTOREPATH);
                }
                String passString = servletContext
                    .getInitParameter(KEYSTOREPASS);
                passString = System.getProperty(KEYSTOREPASS, passString);
                char[] passPhrase = "".toCharArray();
                if (null != passString)  {
                    passPhrase = passString.toCharArray();
                }
                KeyManagerFactory keyManagerFactory = null;
                try {
                    keyManagerFactory = 
                            KeyManagerFactory.getInstance("SunX509");
                } catch (Exception e)  {
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.log(Level.FINE, "No SunX509 KeyManagerFactory: " + e);
                    }
                }
                if (null == keyManagerFactory)  {
                    try {
                        keyManagerFactory = 
                                KeyManagerFactory.getInstance("IbmX509");
                    } catch (Exception e)  {
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "No IbmX509 KeyManagerFactory: " + e);
                        }
                    }
                }

                KeyStore keyStore = KeyStore.getInstance("jks");
                keyStore.load(keyStream, passPhrase);
                keyManagerFactory.init(keyStore, passPhrase);
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(keyManagerFactory.getKeyManagers(), 
                    null, null);
                sslFactory = (SSLSocketFactory) 
                        sslContext.getSocketFactory();
            } catch (Exception e)  {
                if (LOGGER.isLoggable(Level.SEVERE)) {
                    LOGGER.log(Level.SEVERE, "Unable to initialize APNS keystore: ", e);
                }
            }
        }
        if (null == sslFactory) {
            sslFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
        }
        LOGGER.log(Level.INFO, "APNS Notification Provider is on");
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "reason: SSL factory = " + sslFactory);
        }
    }

    public void registerWith(OutOfBandNotifier outOfBandNotifier) {
        outOfBandNotifier.registerProvider("apns", this);
        this.outOfBandNotifier = outOfBandNotifier;
    }

    public void send(String uri, PushNotification notification) {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "APNSNotificationProvider.send " + uri);
        }
        outOfBandNotifier.trace("APNSNotificationProvider.send " + uri);
        //Inefficient connect and close for now
        String hexToken = uri.substring("apns:".length());
        String body = notification.getSubject();
        byte[] apnsNotification = getEnhancedNotification(hexToken, 
            "{\"aps\":{\"alert\":\"" + body + "\"}}");
        try {
            SSLSocket apnsSocket = (SSLSocket) sslFactory.createSocket(HOST, PORT);
            OutputStream sout = apnsSocket.getOutputStream();
            sout.write(apnsNotification);
            sout.flush();
            sout.close();
        } catch (Exception e) {
            if (LOGGER.isLoggable(Level.SEVERE)) {
                LOGGER.log(Level.SEVERE, "Failed to send APNS Notification: ", e);
            }
            outOfBandNotifier.trace("APNSNotificationProvider.send failed: " + e);
        }
    }

    public static byte[] getSimpleNotification(
            String hexToken, String payload )  {
        byte[] payloadBytes = payload.getBytes();
        int payloadLen = payloadBytes.length;
        //command + token len + token + payload len + payload
        byte[] notiBytes = new byte[1 + 2 + 32 + 2 + payloadBytes.length];
        int i = 0;
        //command
        notiBytes[i++] = 0;
        //token len
        notiBytes[i++] = 0;
        notiBytes[i++] = 32;
        //token
        for (int j = 0; j < hexToken.length(); j += 2)  {
            byte nextByte = (byte) Integer.parseInt(hexToken.substring(j, j + 2), 16);
            notiBytes[i++] = nextByte;
        }
        //payload len assumed 16 bit
        notiBytes[i++] = (byte) (payloadLen >> 8);
        notiBytes[i++] = (byte) (payloadLen & 0xff);
        //payload
        System.arraycopy(payloadBytes, 0, notiBytes, i, payloadLen);
        
        return notiBytes;
    }


    public static byte[] getEnhancedNotification(
            String hexToken, String payload )  {
        byte[] payloadBytes = payload.getBytes();
        int payloadLen = payloadBytes.length;
        //command + identifier + expiry + token len + token + payload len + payload
        byte[] notiBytes = new byte[1 + 4 + 4 + 2 + 32 + 2 + payloadBytes.length];
        int i = 0;
        //command
        notiBytes[i++] = 1;
        //identifier
        notiBytes[i++] = 0;
        notiBytes[i++] = 6;
        notiBytes[i++] = 6;
        notiBytes[i++] = 6;
        //expiry
        notiBytes[i++] = 0;
        notiBytes[i++] = 0;
        notiBytes[i++] = 0;
        notiBytes[i++] = 0;
        //token len
        notiBytes[i++] = 0;
        notiBytes[i++] = 32;
        //token
        for (int j = 0; j < hexToken.length(); j += 2)  {
            byte nextByte = (byte) Integer.parseInt(hexToken.substring(j, j + 2), 16);
            notiBytes[i++] = nextByte;
        }
        //payload len assumed 16 bit
        notiBytes[i++] = (byte) (payloadLen >> 8);
        notiBytes[i++] = (byte) (payloadLen & 0xff);
        //payload
        System.arraycopy(payloadBytes, 0, notiBytes, i, payloadLen);
        
        return notiBytes;
    }

    public static String toHexString(byte[] bytes)  {
        StringBuffer buf = new StringBuffer();
        for (int j = 0; j < bytes.length; j++)  {
            byte b = bytes[j];
            if ((b >= 0) && (b < 0x10))  {
                //print leading zero
                buf.append("0");
            }
            buf.append(Integer.toHexString((int) b & 0xff));
        }
        return buf.toString();
    }


    public static class AutoRegister implements ServletContextListener {
        public void contextInitialized(ServletContextEvent servletContextEvent) {
            ServletContext servletContext = servletContextEvent.getServletContext();
            if (System.getProperty("javax.net.ssl.keyStore") == null) {
                LOGGER.log(Level.INFO, "APNS Notification Provider is off");
                LOGGER.log(
                    Level.FINE, "reason: Key Store was not specified.  (javax.net.ssl.keyStore)"
                );
                return;
            }
            if (System.getProperty("javax.net.ssl.keyStoreType") == null) {
                LOGGER.log(Level.INFO, "APNS Notification Provider is off");
                LOGGER.log(
                    Level.FINE, "reason: Key Store Type was not specified.  (javax.net.ssl.keyStoreType)"
                );
                return;
            }
            if (System.getProperty("javax.net.ssl.keyStorePassword") == null) {
                LOGGER.log(Level.INFO, "APNS Notification Provider is off");
                LOGGER.log(
                    Level.FINE, "reason: Key Store Password was not specified.  (javax.net.ssl.keyStorePassword)"
                );
                return;
            }
            ExtensionRegistry.addExtension(
                servletContext, 10, NotificationProvider.class.getName(), new APNSNotificationProvider(servletContext)
            );
        }

        public void contextDestroyed(ServletContextEvent servletContextEvent) {
        }
    }
}
