// 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.smpp.jsmpp;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;
import org.jsmpp.InvalidResponseException;
import org.jsmpp.PDUException;
import org.jsmpp.bean.AlertNotification;
import org.jsmpp.bean.Alphabet;
import org.jsmpp.bean.BindType;
import org.jsmpp.bean.DataSm;
import org.jsmpp.bean.DeliverSm;
import org.jsmpp.bean.DeliveryReceipt;
import org.jsmpp.bean.ESMClass;
import org.jsmpp.bean.GeneralDataCoding;
import org.jsmpp.bean.MessageClass;
import org.jsmpp.bean.MessageType;
import org.jsmpp.bean.NumberingPlanIndicator;
import org.jsmpp.bean.RegisteredDelivery;
import org.jsmpp.bean.SMSCDeliveryReceipt;
import org.jsmpp.bean.TypeOfNumber;
import org.jsmpp.extra.NegativeResponseException;
import org.jsmpp.extra.ProcessRequestException;
import org.jsmpp.extra.ResponseTimeoutException;
import org.jsmpp.extra.SessionState;
import org.jsmpp.session.BindParameter;
import org.jsmpp.session.DataSmResult;
import org.jsmpp.session.MessageReceiverListener;
import org.jsmpp.session.SMPPSession;
import org.jsmpp.session.Session;
import org.jsmpp.session.SessionStateListener;
import org.jsmpp.util.InvalidDeliveryReceiptException;
import org.smslib.AGateway;
import org.smslib.GatewayException;
import org.smslib.InboundMessage;
import org.smslib.OutboundMessage;
import org.smslib.Service;
import org.smslib.StatusReportMessage;
import org.smslib.TimeoutException;
import org.smslib.Message.MessageEncodings;
import org.smslib.Message.MessageTypes;
import org.smslib.OutboundMessage.FailureCauses;
import org.smslib.OutboundMessage.MessageStatuses;
import org.smslib.StatusReportMessage.DeliveryStatuses;
import org.smslib.helper.Logger;
import org.smslib.notify.InboundMessageNotification;
import org.smslib.smpp.AbstractSMPPGateway;
import org.smslib.smpp.BindAttributes;

/**
 * A gateway that supports SMPP through JSMPP (http://jsmpp.org/).
 * 
 * @author Bassam Al-Sarori
 */
public class JSMPPGateway extends AbstractSMPPGateway
{
	private SMPPSession session = null;

	private MessageReceiver messageReceiver = new MessageReceiver();

	private SessionStateListener stateListener = new JSMPPSessionStateListener();

	private BindType bindType;

	private TypeOfNumber bindTypeOfNumber;

	private NumberingPlanIndicator bindNumberingPlanIndicator;

	/**
	 * @param id
	 * @param host
	 * @param port
	 * @param bindAttributes
	 */
	public JSMPPGateway(String id, String host, int port, BindAttributes bindAttributes)
	{
		super(id, host, port, bindAttributes);
		setAttributes(AGateway.GatewayAttributes.SEND | AGateway.GatewayAttributes.CUSTOMFROM | AGateway.GatewayAttributes.BIGMESSAGES | AGateway.GatewayAttributes.FLASHSMS | AGateway.GatewayAttributes.RECEIVE);
		init();
	}

	private void init()
	{
		switch (bindAttributes.getBindType())
		{
			case RECEIVER:
				bindType = BindType.BIND_RX;
				setInbound(true);
				setOutbound(false);
				break;
			case TRANSMITTER:
				bindType = BindType.BIND_TX;
				setInbound(false);
				setOutbound(true);
				break;
			case TRANSCEIVER:
				bindType = BindType.BIND_TRX;
				setInbound(true);
				setOutbound(true);
				break;
			default:
				IllegalArgumentException illegalArgumentException = new IllegalArgumentException("Unknown BindType " + bindAttributes.getBindType());
				Logger.getInstance().logError(illegalArgumentException.getMessage(), illegalArgumentException, getGatewayId());
				throw illegalArgumentException;
		}
		bindTypeOfNumber = TypeOfNumber.valueOf(bindAttributes.getBindAddress().getTypeOfNumber().value());
		bindNumberingPlanIndicator = NumberingPlanIndicator.valueOf(bindAttributes.getBindAddress().getNumberingPlanIndicator().value());
		initSession();
	}

	private void initSession()
	{
		session = new SMPPSession();
		session.addSessionStateListener(stateListener);
		session.setMessageReceiverListener(messageReceiver);
	}

	@Override
	public void startGateway() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		if (!session.getSessionState().isBound())
		{
			if (enquireLink > 0)
			{
				session.setEnquireLinkTimer(enquireLink);
			}
			session.connectAndBind(host, port, new BindParameter(bindType, bindAttributes.getSystemId(), bindAttributes.getPassword(), bindAttributes.getSystemType(), bindTypeOfNumber, bindNumberingPlanIndicator, null));
		}
		else
		{
			Logger.getInstance().logWarn("SMPP session already bound.", null, getGatewayId());
			//	throw new GatewayException("Session already bound");
		}
	}

	@Override
	public void stopGateway() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		if (session.getSessionState().isBound())
		{
			session.removeSessionStateListener(stateListener);
			session.unbindAndClose();
			//super.stopGateway();
		}
		else
		{
			Logger.getInstance().logWarn("SMPP session not bound.", null, getGatewayId());
			//throw new GatewayException("Session not bound");
		}
		super.stopGateway();
	}

	class MessageReceiver implements MessageReceiverListener
	{
		public void onAcceptDeliverSm(DeliverSm deliverSm) throws ProcessRequestException
		{
			if (MessageType.SMSC_DEL_RECEIPT.containedIn(deliverSm.getEsmClass()))
			{
				try
				{
					DeliveryReceipt delReceipt = deliverSm.getShortMessageAsDeliveryReceipt();
					StatusReportMessage statusReportMessage = new StatusReportMessage(delReceipt.getId(), deliverSm.getDestAddress(), deliverSm.getSourceAddr(), delReceipt.getText(), delReceipt.getSubmitDate(), delReceipt.getDoneDate());
					switch (delReceipt.getFinalStatus())
					{
						case DELIVRD:
							statusReportMessage.setStatus(DeliveryStatuses.DELIVERED);
							break;
						case REJECTD:
						case EXPIRED:
						case UNDELIV:
							statusReportMessage.setStatus(DeliveryStatuses.ABORTED);
							break;
						default:
							statusReportMessage.setStatus(DeliveryStatuses.UNKNOWN);
					}
					statusReportMessage.setGatewayId(getGatewayId());
					Service.getInstance().getNotifyQueueManager().getNotifyQueue().add(new InboundMessageNotification(getMyself(), MessageTypes.STATUSREPORT, statusReportMessage));
				}
				catch (InvalidDeliveryReceiptException e)
				{
					Logger.getInstance().logError("Failed getting delivery receipt.", e, getGatewayId());
				}
			}
			else
			{
				InboundMessage msg = new InboundMessage(new java.util.Date(), deliverSm.getSourceAddr(), new String(deliverSm.getShortMessage()), 0, null);
				msg.setGatewayId(JSMPPGateway.this.getGatewayId());
				if (Alphabet.ALPHA_DEFAULT.value() == deliverSm.getDataCoding())
				{
					msg.setEncoding(MessageEncodings.ENC7BIT);
				}
				else if (Alphabet.ALPHA_8_BIT.value() == deliverSm.getDataCoding())
				{
					msg.setEncoding(MessageEncodings.ENC8BIT);
				}
				else if (Alphabet.ALPHA_UCS2.value() == deliverSm.getDataCoding())
				{
					msg.setEncoding(MessageEncodings.ENCUCS2);
				}
				else
				{
					msg.setEncoding(MessageEncodings.ENCCUSTOM);
				}
				incInboundMessageCount();
				Service.getInstance().getNotifyQueueManager().getNotifyQueue().add(new InboundMessageNotification(getMyself(), MessageTypes.INBOUND, msg));
			}
		}

		public DataSmResult onAcceptDataSm(DataSm dataSm, Session source) throws ProcessRequestException
		{
			// ignored
			return null;
		}

		public void onAcceptAlertNotification(AlertNotification alertNotification)
		{
			// ignored
		}
	}

	class JSMPPSessionStateListener implements SessionStateListener
	{
		public void onStateChange(SessionState newState, SessionState oldState, Object source)
		{
			if (newState.isBound())
			{
				if (!getStatus().equals(GatewayStatuses.STARTED))
				{
					try
					{
						JSMPPGateway.super.startGateway();
					}
					catch (TimeoutException e)
					{
						Logger.getInstance().logError("Failed starting Gateway.", e, getGatewayId());
					}
					catch (GatewayException e)
					{
						Logger.getInstance().logError("Failed starting Gateway.", e, getGatewayId());
					}
					catch (IOException e)
					{
						Logger.getInstance().logError("Failed starting Gateway.", e, getGatewayId());
					}
					catch (InterruptedException e)
					{
						Logger.getInstance().logError("Failed starting Gateway.", e, getGatewayId());
					}
				}
			}
			else if (newState.equals(SessionState.CLOSED))
			{
				if (getStatus().equals(GatewayStatuses.STARTED))
				{
					JSMPPGateway.super.setStatus(GatewayStatuses.RESTART);
					initSession();
				}
			}
			//System.out.println("State Changed: from "+oldState+" To "+newState);
		}

		@Override
		public void onStateChange(SessionState arg0, SessionState arg1, Session arg2)
		{
			// TODO Auto-generated method stub
		}
	}

	@Override
	public boolean sendMessage(OutboundMessage msg) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		Alphabet encoding = Alphabet.ALPHA_DEFAULT;
		switch (msg.getEncoding())
		{
			case ENC8BIT:
				encoding = Alphabet.ALPHA_8_BIT;
				break;
			case ENCUCS2:
				encoding = Alphabet.ALPHA_UCS2;
				break;
			case ENCCUSTOM:
				encoding = Alphabet.ALPHA_UNSPECIFIED_2;
				break;
		}
		GeneralDataCoding dataCoding;
		switch (msg.getDCSMessageClass())
		{
			case MSGCLASS_FLASH:
				dataCoding = new GeneralDataCoding(encoding, MessageClass.CLASS0, true);
				break;
			case MSGCLASS_ME:
				dataCoding = new GeneralDataCoding(encoding, MessageClass.CLASS1, true);
				break;
			case MSGCLASS_SIM:
				dataCoding = new GeneralDataCoding(encoding, MessageClass.CLASS2, true);
				break;
			case MSGCLASS_TE:
				dataCoding = new GeneralDataCoding(encoding, MessageClass.CLASS3, true);
				break;
			default:
				dataCoding = new GeneralDataCoding(encoding, MessageClass.CLASS1, true);
		}
		try
		{
			final RegisteredDelivery registeredDelivery = new RegisteredDelivery();
			registeredDelivery.setSMSCDeliveryReceipt((msg.getStatusReport()) ? SMSCDeliveryReceipt.SUCCESS_FAILURE : SMSCDeliveryReceipt.DEFAULT);
			String msgId = session.submitShortMessage(bindAttributes.getSystemType(), TypeOfNumber.valueOf(sourceAddress.getTypeOfNumber().value()), NumberingPlanIndicator.valueOf(sourceAddress.getNumberingPlanIndicator().value()), (msg.getFrom() != null) ? msg.getFrom() : getFrom(), TypeOfNumber.valueOf(destinationAddress.getTypeOfNumber().value()), NumberingPlanIndicator.valueOf(destinationAddress.getNumberingPlanIndicator().value()), msg.getRecipient(), new ESMClass(), (byte) 0, (byte) msg.getPriority(), null, formatTimeFromHours(msg.getValidityPeriod()), registeredDelivery, (byte) 0, dataCoding, (byte) 0, msg.getText().getBytes());
			msg.setRefNo(msgId);
			msg.setDispatchDate(new Date());
			msg.setGatewayId(getGatewayId());
			msg.setMessageStatus(MessageStatuses.SENT);
			incOutboundMessageCount();
		}
		catch (PDUException e)
		{
			msg.setGatewayId(getGatewayId());
			msg.setMessageStatus(MessageStatuses.FAILED);
			msg.setFailureCause(FailureCauses.BAD_FORMAT);
			Logger.getInstance().logError("Message Format not accepted.", e, getGatewayId());
			return false;
		}
		catch (ResponseTimeoutException e)
		{
			Logger.getInstance().logError("Message could not be sent.", e, getGatewayId());
			throw new TimeoutException(e.getMessage());
		}
		catch (InvalidResponseException e)
		{
			Logger.getInstance().logError("Message could not be sent.", e, getGatewayId());
			throw new IOException("InvalidResponseException: ", e);
		}
		catch (NegativeResponseException e)
		{
			Logger.getInstance().logError("Message could not be sent.", e, getGatewayId());
			throw new IOException("NegativeResponseException: ", e);
		}
		return true;
	}

	private String formatTimeFromHours(int timeInHours)
	{
		if (timeInHours < 0) { return null; }
		Calendar cDate = Calendar.getInstance();
		cDate.clear();
		cDate.set(Calendar.YEAR, 0);
		cDate.add(Calendar.HOUR, timeInHours);
		int years = cDate.get(Calendar.YEAR) - cDate.getMinimum(Calendar.YEAR);
		int months = cDate.get(Calendar.MONTH);
		int days = cDate.get(Calendar.DAY_OF_MONTH) - 1;
		int hours = cDate.get(Calendar.HOUR_OF_DAY);
		String yearsString = (years < 10) ? "0" + years : years + "";
		String monthsString = (months < 10) ? "0" + months : months + "";
		String daysString = (days < 10) ? "0" + days : days + "";
		String hoursString = (hours < 10) ? "0" + hours : hours + "";
		return yearsString + monthsString + daysString + hoursString + "0000000R";
	}

	@Override
	public void setEnquireLink(int enquireLink)
	{
		super.setEnquireLink(enquireLink);
		if (session != null)
		{
			session.setEnquireLinkTimer(enquireLink);
		}
	}
}
