/*
 * 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 org.icepush.Configuration;
import org.icepush.NotificationProvider;
import org.icepush.OutOfBandNotifier;
import org.icepush.PushNotification;
import org.icepush.servlet.ServletContextConfiguration;
import org.icepush.util.ExtensionRegistry;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

public class GcmNotificationProvider implements NotificationProvider {
    private static final Logger log = Logger.getLogger(GcmNotificationProvider.class.getName());
    private static final String GCM_SERVER_URL = "https://android.googleapis.com/gcm/send";
    private static final String GCM_LOGIN_PROPERTY = 
            "com.icesoft.icepush.gcmlogin";
    private static final int MaximumRetries = 10;
    OutOfBandNotifier outOfBandNotifier;

    private Timer timer = new Timer("ICEpush-GCM", true);
    private String authKey;
    private int loginRetries = 0;

    public GcmNotificationProvider(String authKey) {
        this.authKey = authKey;
    }

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

    public void send(String uri, PushNotification notification) {
        //assumes URL looks like "c2dm:registrationID"
        outOfBandNotifier.trace("GcmNotificationProvider.send " + uri);
        String registrationID = uri.substring(5);
        if (registrationID == null) {
            throw new RuntimeException("Missing GCM registration ID");
        } else {
            timer.schedule( new SendMessage(registrationID, 
                    notification, 0), retryDelay(0) );
        }
    }

    private static long retryDelay(int retries) {
        return (long) Math.pow(retries * 10, 10);
    }

    private class SendMessage extends TimerTask {
        private final String registrationID;
        private final PushNotification notification;
        private final int retried;

        public SendMessage(String registrationID, PushNotification notification, int retried) {
            this.registrationID = registrationID;
            this.notification = notification;
            this.retried = retried;
        }

        private String encode(Object value)  {
            return URLEncoder.encode(String.valueOf(value));
        }

        public void run() {
            try {
                URL url = new URL(GCM_SERVER_URL);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setDoInput(true);
                connection.setDoOutput(true);
                connection.setUseCaches(false);
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Authorization", "key=" + authKey);
                //include specified Retry-After header
                if (retried > 0) {
                    connection.setRequestProperty("Retry-After", String.valueOf(retryDelay(retried)));
                }

                OutputStream os = connection.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
                Map attributes = notification.getAttributes();
                osw.write("registration_id=");
                osw.write(registrationID);
                osw.write("&");
                osw.write("collapse_key=");
                osw.write(encode(attributes.get("category")));
                osw.write("&");
                osw.write("data.ticker=");
                osw.write(encode(attributes.get("hint")));
                osw.write("&");
                osw.write("data.title=");
                osw.write(encode(notification.getSubject()));
                osw.write("&");
                osw.write("data.msg=");
                osw.write(encode(notification.getDetail()));
                osw.write("&");
                osw.write("delay_while_idle=true");
                osw.flush();
                osw.close();

                int statusCode = connection.getResponseCode();
                log.fine("GCM Notification for " + registrationID + " Response Code is  " + statusCode);
                outOfBandNotifier.trace("GCM Notification for " + registrationID + " Response Code is  " + statusCode);
                switch (statusCode) {
                    case 200:
                        break;
                    case 503:
                        //service temporarily unavailable, retry with delay
                        if (retried > MaximumRetries) {
                            throw new RuntimeException("GCM service is down");
                        } else {
                            timer.schedule( new SendMessage(registrationID,
                                    notification, retried + 1), 
                                    retryDelay(retried + 1) );
                        }
                        break;
                    case 401:
                        log.log(Level.WARNING, "Authorization key is invalid.");
                        break;
                    default:
                        log.log(Level.WARNING, "Unknown response code: " + statusCode);
                        break;
                }
            } catch (IOException e) {
                log.log(Level.WARNING, "Failed to connect", e);
            }
        }
    }

    private void shutdown() {
        timer.cancel();
    }

    public static class AutoRegister implements ServletContextListener {
        private GcmNotificationProvider provider;

        public void contextInitialized(ServletContextEvent servletContextEvent) {
            ServletContext servletContext = 
                    servletContextEvent.getServletContext();
            String authKey = System.getProperty(GCM_LOGIN_PROPERTY);
            if (null == authKey)  {
                authKey = servletContext
                        .getInitParameter(GCM_LOGIN_PROPERTY);
            }
            if (null == authKey)  {
                Configuration configuration = 
                        new ServletContextConfiguration("gcm", servletContext);
                authKey = configuration.getAttribute("authKey", null);
            }

            if (null != authKey)  {
                provider = new GcmNotificationProvider(authKey);
                ExtensionRegistry.addExtension(servletContext, 10, NotificationProvider.class.getName(), provider);
		        log.log(Level.INFO, "GCM Notification Provider is on");
            } else {
                log.log(Level.INFO, "GCM Notification Provider is off");
                log.log(Level.FINE, "reason: Missing authKey");
            }
        }

        public void contextDestroyed(ServletContextEvent servletContextEvent) {
            if (provider != null) {
                provider.shutdown();
            }
        }
    }
}
