// SMSLib for Java v3
// A Java API library for sending and receiving SMS via a GSM modem
// or other supported gateways.
// Web Site: http://www.smslib.org
//
// Copyright (C) 2002-2012, Thanasis Delenikas, Athens/GREECE.
// SMSLib is distributed under the terms of the Apache License version 2.0
//
// 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 org.smslib.smsserver;

import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import org.smslib.ICallNotification;
import org.smslib.IInboundMessageNotification;
import org.smslib.IOrphanedMessageNotification;
import org.smslib.IOutboundMessageNotification;
import org.smslib.IQueueSendingNotification;
import org.smslib.InboundMessage;
import org.smslib.InboundMessage.MessageClasses;
import org.smslib.Library;
import org.smslib.Message.MessageTypes;
import org.smslib.OutboundMessage;
import org.smslib.Service;
import org.smslib.helper.Logger;
import org.smslib.smsserver.gateways.AGateway;
import org.smslib.smsserver.interfaces.Interface;
import org.smslib.smsserver.interfaces.Interface.InterfaceTypes;

/**
 * SMSServer Application.
 */
public class SMSServer
{
	Properties props;

	boolean shutdown = false;

	List<Interface<? extends Object>> infList;

	InboundNotification inboundNotification;

	OutboundNotification outboundNotification;

	CallNotification callNotification;

	QueueSendingNotification queueSendingNotification;

	OrphanedMessageNotification orphanedMessageNotification;

	InboundPollingThread inboundPollingThread;

	OutboundPollingThread outboundPollingThread;

	boolean optRunOnce = false;

	public SMSServer()
	{
		this.infList = new ArrayList<Interface<? extends Object>>();
		Runtime.getRuntime().addShutdownHook(new Shutdown());
		this.inboundNotification = new InboundNotification();
		this.outboundNotification = new OutboundNotification();
		this.callNotification = new CallNotification();
		this.queueSendingNotification = new QueueSendingNotification();
		orphanedMessageNotification = new OrphanedMessageNotification();
		this.inboundPollingThread = null;
		this.outboundPollingThread = null;
		//Service.getInstance().setInboundMessageNotification(this.inboundNotification);
		Service.getInstance().setOutboundMessageNotification(this.outboundNotification);
		Service.getInstance().setCallNotification(this.callNotification);
		Service.getInstance().setQueueSendingNotification(this.queueSendingNotification);
		Service.getInstance().setOrphanedMessageNotification(this.orphanedMessageNotification);
	}

	public List<Interface<? extends Object>> getInfList()
	{
		return this.infList;
	}

	public boolean getShutdown()
	{
		return shutdown;
	}

	public Properties getProperties()
	{
		return this.props;
	}

	class Shutdown extends Thread
	{
		@Override
		public void run()
		{
			Logger.getInstance().logInfo("SMSServer shutting down, please wait...", null, null);
			SMSServer.this.shutdown = true;
			try
			{
				stopInterfaces();
				if (Service.getInstance().getQueueManager() != null) Service.getInstance().getQueueManager().removeAllPendingMessages();
				if (Service.getInstance().getQueueManager() != null) Service.getInstance().getQueueManager().removeAllDelayedMessages();
				Service.getInstance().stopService();
				if (SMSServer.this.inboundPollingThread != null)
				{
					SMSServer.this.inboundPollingThread.interrupt();
					SMSServer.this.inboundPollingThread.join();
				}
				if (SMSServer.this.outboundPollingThread != null)
				{
					SMSServer.this.outboundPollingThread.interrupt();
					SMSServer.this.outboundPollingThread.join();
				}
			}
			catch (Exception e)
			{
				Logger.getInstance().logError("Shutdown hook error.", e, null);
				e.printStackTrace();
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void loadConfiguration() throws Exception
	{
		FileInputStream f = null;
		try
		{
			this.props = new Properties();
			if (System.getProperty("smsserver.configdir") != null) f = new FileInputStream(System.getProperty("smsserver.configdir") + "SMSServer.conf");
			else if (System.getProperty("smsserver.configfile") != null) f = new FileInputStream(System.getProperty("smsserver.configfile"));
			else f = new FileInputStream("SMSServer.conf");
			getProperties().load(f);
			if (getProperties().getProperty("smsserver.balancer", "").length() > 0)
			{
				try
				{
					Class<?> c = Class.forName((getProperties().getProperty("smsserver.balancer", "").indexOf('.') == -1 ? "org.smslib.balancing." : "") + getProperties().getProperty("smsserver.balancer", ""));
					Constructor<?> constructor = c.getConstructor();
					org.smslib.balancing.LoadBalancer balancer = (org.smslib.balancing.LoadBalancer) constructor.newInstance();
					Service.getInstance().setLoadBalancer(balancer);
					Logger.getInstance().logInfo("SMSServer: set balancer to: " + getProperties().getProperty("smsserver.balancer", ""), null, null);
				}
				catch (Exception e)
				{
					e.printStackTrace();
					Logger.getInstance().logError("SMSServer: error setting custom balancer!", null, null);
				}
			}
			if (getProperties().getProperty("smsserver.router", "").length() > 0)
			{
				try
				{
					Class<?> c = Class.forName((getProperties().getProperty("smsserver.router", "").indexOf('.') == -1 ? "org.smslib.routing." : "") + getProperties().getProperty("smsserver.router", ""));
					Constructor<?> constructor = c.getConstructor();
					org.smslib.routing.Router router = (org.smslib.routing.Router) constructor.newInstance();
					Service.getInstance().setRouter(router);
					Logger.getInstance().logInfo("SMSServer: set router to: " + getProperties().getProperty("smsserver.router", ""), null, null);
				}
				catch (Exception e)
				{
					Logger.getInstance().logError("SMSServer: error setting custom balancer!", null, null);
				}
			}
			for (int i = 0; i < Integer.MAX_VALUE; i++)
			{
				try
				{
					String propName = "gateway." + i;
					String propValue = getProperties().getProperty(propName, "");
					if (propValue.length() == 0) break;
					StringTokenizer tokens = new StringTokenizer(propValue, ",");
					String gtwId = tokens.nextToken().trim();
					String gtwClass = tokens.nextToken().trim();
					Object[] args = new Object[] { gtwId, getProperties(), this };
					Class<?>[] argsClass = new Class[] { String.class, Properties.class, SMSServer.class };
					Class<?> c = Class.forName((gtwClass.indexOf('.') == -1 ? "org.smslib.smsserver.gateways." : "") + gtwClass);
					Constructor<?> constructor = c.getConstructor(argsClass);
					AGateway gtw = (AGateway) constructor.newInstance(args);
					gtw.create();
					Service.getInstance().addGateway(gtw.getGateway());
					Logger.getInstance().logInfo("SMSServer: added gateway " + gtwId + " / " + gtw.getDescription(), null, null);
				}
				catch (Exception e)
				{
					Logger.getInstance().logError("SMSServer: Unknown Gateway in configuration file!", null, null);
				}
			}
			for (int i = 0; i < Integer.MAX_VALUE; i++)
			{
				try
				{
					String propName = "interface." + i;
					String propValue = getProperties().getProperty(propName, "");
					if (propValue.length() == 0) break;
					StringTokenizer tokens = new StringTokenizer(propValue, ",");
					String infId = tokens.nextToken().trim();
					String infClass = tokens.nextToken().trim();
					InterfaceTypes infType = null;
					String sinfType = tokens.hasMoreTokens() ? tokens.nextToken() : "inoutbound";
					sinfType = sinfType.trim();
					if ("inbound".equalsIgnoreCase(sinfType))
					{
						infType = InterfaceTypes.INBOUND;
					}
					else if ("outbound".equalsIgnoreCase(sinfType))
					{
						infType = InterfaceTypes.OUTBOUND;
					}
					else
					{ // NULL or other crap
						infType = InterfaceTypes.INOUTBOUND;
					}
					Object[] args = new Object[] { infId, getProperties(), this, infType };
					Class<?>[] argsClass = new Class[] { String.class, Properties.class, SMSServer.class, InterfaceTypes.class };
					Class<?> c = Class.forName((infClass.indexOf('.') == -1 ? "org.smslib.smsserver.interfaces." : "") + infClass);
					Constructor<?> constructor = c.getConstructor(argsClass);
					Interface<? extends Object> inf = (Interface<? extends Object>) constructor.newInstance(args);
					getInfList().add(inf);
					Logger.getInstance().logInfo("SMSServer: added interface " + infId + " / " + inf.getDescription() + " / " + inf.getType(), null, null);
				}
				/* Check for exceptions thrown by the constructor itself */
				catch (InvocationTargetException e)
				{
					Logger.getInstance().logError("SMSServer: Illegal Interface configuration: " + e.getCause().getMessage(), null, null);
				}
				catch (Exception e)
				{
					Logger.getInstance().logError("SMSServer: Unknown Interface in configuration file!", null, null);
				}
			}
		}
		finally
		{
			if (f != null) f.close();
		}
	}

	class InboundPollingThread extends Thread
	{
		@Override
		public void run()
		{
			try
			{
				while (!SMSServer.this.shutdown)
				{
					Logger.getInstance().logDebug("InboundPollingThread() run.", null, null);
					readMessages();
					if (SMSServer.this.optRunOnce)
					{
						SMSServer.this.shutdown = true;
						new Shutdown().start();
						break;
					}
					Thread.sleep(Integer.parseInt(getProperties().getProperty("settings.inbound_interval", "60")) * 1000);
				}
			}
			catch (InterruptedException e)
			{
				Logger.getInstance().logDebug("InboundPollingThread() interrupted.", null, null);
			}
			catch (Exception e)
			{
				Logger.getInstance().logDebug("Error in InboundPollingThread()", e, null);
			}
		}
	}

	class OutboundPollingThread extends Thread
	{
		@Override
		public void run()
		{
			try
			{
				while (!SMSServer.this.shutdown)
				{
					Logger.getInstance().logDebug("OutboundPollingThread() run.", null, null);
					sendMessages();
					if (SMSServer.this.optRunOnce) break;
					Thread.sleep(Integer.parseInt(getProperties().getProperty("settings.outbound_interval", "60")) * 1000);
				}
			}
			catch (InterruptedException e)
			{
				Logger.getInstance().logDebug("OutboundPollingThread() interrupted.", null, null);
			}
			catch (Exception e)
			{
				Logger.getInstance().logDebug("Error in OutboundPollingThread()", e, null);
			}
		}
	}

	private void process() throws Exception
	{
		this.inboundPollingThread = new InboundPollingThread();
		this.inboundPollingThread.setName("SMSServer - InboundPollingThread");
		this.inboundPollingThread.start();
		this.outboundPollingThread = new OutboundPollingThread();
		this.outboundPollingThread.setName("SMSServer - OutboundPollingThread");
		this.outboundPollingThread.start();
		while (!this.shutdown)
			Thread.sleep(1000);
	}

	void startInterfaces() throws Exception
	{
		for (Interface<? extends Object> inf : getInfList())
			inf.start();
	}

	void stopInterfaces() throws Exception
	{
		for (Interface<? extends Object> inf : getInfList())
			inf.stop();
	}

	private void run() throws Exception
	{
		loadConfiguration();
		try
		{
			startInterfaces();
			Service.getInstance().startService();
			process();
		}
		catch (Exception e)
		{
			Logger.getInstance().logError("SMSServer error!", e, null);
			stopInterfaces();
			Service.getInstance().stopService();
			if (this.inboundPollingThread != null)
			{
				this.inboundPollingThread.interrupt();
				this.inboundPollingThread.join();
			}
			if (this.outboundPollingThread != null)
			{
				this.outboundPollingThread.interrupt();
				this.outboundPollingThread.join();
			}
		}
	}

	void readMessages()
	{
		List<InboundMessage> msgList = new ArrayList<InboundMessage>();
		try
		{
			Service.getInstance().readMessages(msgList, MessageClasses.ALL);
			if (msgList.size() > 0)
			{
				for (Interface<? extends Object> inf : getInfList())
					if (inf.isInbound()) inf.messagesReceived(msgList);
				if (getProperties().getProperty("settings.delete_after_processing", "no").equalsIgnoreCase("yes")) for (InboundMessage msg : msgList)
					Service.getInstance().deleteMessage(msg);
			}
		}
		catch (Exception e)
		{
			Logger.getInstance().logError("SMSServer: reading messages exception!", e, null);
		}
	}

	void sendMessages()
	{
		boolean foundOutboundGateway = false;
		for (org.smslib.AGateway gtw : Service.getInstance().getGateways())
			if (gtw.isOutbound())
			{
				foundOutboundGateway = true;
				break;
			}
		if (foundOutboundGateway)
		{
			List<OutboundMessage> msgList = new ArrayList<OutboundMessage>();
			try
			{
				for (Interface<? extends Object> inf : getInfList())
					if (inf.isOutbound()) msgList.addAll(inf.getMessagesToSend());
			}
			catch (Exception e)
			{
				Logger.getInstance().logError("SMSServer: sending messages exception!", e, null);
			}
			if (getProperties().getProperty("settings.send_mode", "sync").equalsIgnoreCase(("sync")))
			{
				Logger.getInstance().logInfo("SMSServer: sending synchronously...", null, null);
				for (OutboundMessage msg : msgList)
				{
					try
					{
						Service.getInstance().sendMessage(msg);
						for (Interface<? extends Object> inf : getInfList())
							if (inf.isOutbound()) inf.markMessage(msg);
					}
					catch (Exception e)
					{
						Logger.getInstance().logError("SMSServer: sending messages exception!", e, null);
						try
						{
							for (Interface<? extends Object> inf : getInfList())
								if (inf.isOutbound()) inf.markMessage(msg);
						}
						catch (Exception e1)
						{
							Logger.getInstance().logError("SMSServer: sending messages exception!", e1, null);
						}
					}
				}
			}
			else
			{
				Logger.getInstance().logInfo("SMSServer: sending asynchronously... [" + msgList.size() + "]", null, null);
				for (OutboundMessage msg : msgList)
				{
					if (!Service.getInstance().queueMessage(msg))
					{
						try
						{
							for (Interface<? extends Object> inf : getInfList())
								if (inf.isOutbound()) inf.markMessage(msg);
						}
						catch (Exception e)
						{
							Logger.getInstance().logError("SMSServer: sending messages exception!", e, null);
						}
					}
				}
			}
		}
	}

	class InboundNotification implements IInboundMessageNotification
	{
		@Override
		public void process(org.smslib.AGateway gateway, MessageTypes msgType, InboundMessage msg)
		{
			List<InboundMessage> msgList = new ArrayList<InboundMessage>();
			msgList.add(msg);
			for (Interface<? extends Object> inf : getInfList())
				if (inf.isInbound())
				{
					try
					{
						inf.messagesReceived(msgList);
					}
					catch (Exception e)
					{
						Logger.getInstance().logError("Error receiving message!", e, null);
					}
				}
			if (getProperties().getProperty("settings.delete_after_processing", "no").equalsIgnoreCase("yes"))
			{
				try
				{
					Service.getInstance().deleteMessage(msg);
				}
				catch (Exception e)
				{
					Logger.getInstance().logError("Error deleting received message!", e, null);
				}
			}
			msgList.clear();
		}
	}

	class OutboundNotification implements IOutboundMessageNotification
	{
		@Override
		public void process(org.smslib.AGateway gateway, org.smslib.OutboundMessage msg)
		{
			try
			{
				for (Interface<? extends Object> inf : getInfList())
					if (inf.isOutbound()) inf.markMessage(msg);
			}
			catch (Exception e)
			{
				Logger.getInstance().logError("IOutboundMessageNotification error.", e, null);
			}
		}
	}

	class CallNotification implements ICallNotification
	{
		@Override
		public void process(org.smslib.AGateway gateway, String callerId)
		{
			try
			{
				for (Interface<? extends Object> inf : getInfList())
					inf.callReceived(gateway.getGatewayId(), callerId);
			}
			catch (Exception e)
			{
				Logger.getInstance().logError("ICallNotification error.", e, null);
			}
		}
	}

	class QueueSendingNotification implements IQueueSendingNotification
	{
		@Override
		public void process(org.smslib.AGateway gateway, OutboundMessage msg)
		{
			Logger.getInstance().logInfo("**** >>>> Now Sending: " + msg.getRecipient(), null, gateway.getGatewayId());
		}
	}

	class OrphanedMessageNotification implements IOrphanedMessageNotification
	{
		@Override
		public boolean process(org.smslib.AGateway gateway, InboundMessage msg)
		{
			System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			System.out.println("&&&&&&&&&&&&&&&&& ORPHANED INFO &&&&&&&&&&&&&&&&&");
			System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			System.out.println(msg);
			System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			System.out.println("&&&&&&&&&&&&&&&&& ORPHANED INFO &&&&&&&&&&&&&&&&&");
			System.out.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			// Return FALSE to leave orphaned parts in memory.
			return false;
		}
	}

	public boolean checkPriorityTimeFrame(int priority)
	{
		String timeFrame;
		String from, to, current;
		Calendar cal = Calendar.getInstance();
		if (priority < 0) timeFrame = getProperties().getProperty("settings.timeframe.low", "0000-2359");
		else if (priority == 0) timeFrame = getProperties().getProperty("settings.timeframe.normal", "0000-2359");
		else if (priority >= 0) timeFrame = getProperties().getProperty("settings.timeframe.high", "0000-2359");
		else timeFrame = "0000-2359";
		from = timeFrame.substring(0, 4);
		to = timeFrame.substring(5, 9);
		cal.setTime(new java.util.Date());
		current = cal.get(Calendar.HOUR_OF_DAY) < 10 ? "0" + cal.get(Calendar.HOUR_OF_DAY) : "" + cal.get(Calendar.HOUR_OF_DAY);
		current += cal.get(Calendar.MINUTE) < 10 ? "0" + cal.get(Calendar.MINUTE) : "" + cal.get(Calendar.MINUTE);
		if ((Integer.parseInt(current) >= Integer.parseInt(from)) && (Integer.parseInt(current) < Integer.parseInt(to))) return true;
		return false;
	}

	public static void main(String[] args)
	{
		System.out.println(Library.getLibraryDescription());
		System.out.println("\nSMSLib API version: " + Library.getLibraryVersion());
		System.out.println("SMSServer version: " + Library.getLibraryVersion());
		SMSServer app = new SMSServer();
		for (int i = 0; i < args.length; i++)
		{
			if (args[i].equalsIgnoreCase("-runonce")) app.optRunOnce = true;
			else System.out.println("Invalid argument: " + args[i]);
		}
		try
		{
			app.run();
			Logger.getInstance().logInfo("SMSServer exiting normally.", null, null);
		}
		catch (Exception e)
		{
			Logger.getInstance().logError("SMSServer Error: ", e, null);
			try
			{
				Service.getInstance().stopService();
			}
			catch (Exception e1)
			{
				Logger.getInstance().logError("SMSServer error while shutting down: ", e1, null);
			}
		}
	}
}
