/*
 * Copyright 2002-2014 the original author or authors.
 *
 * 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.springframework.integration.smpp.core;

import static org.springframework.integration.smpp.core.SmppConstants.DATA_CODING;
import static org.springframework.integration.smpp.core.SmppConstants.DST_ADDR;
import static org.springframework.integration.smpp.core.SmppConstants.DST_NPI;
import static org.springframework.integration.smpp.core.SmppConstants.DST_TON;
import static org.springframework.integration.smpp.core.SmppConstants.ESM_CLASS;
import static org.springframework.integration.smpp.core.SmppConstants.MAXIMUM_CHARACTERS;
import static org.springframework.integration.smpp.core.SmppConstants.PRIORITY_FLAG;
import static org.springframework.integration.smpp.core.SmppConstants.PROTOCOL_ID;
import static org.springframework.integration.smpp.core.SmppConstants.REGISTERED_DELIVERY_MODE;
import static org.springframework.integration.smpp.core.SmppConstants.REPLACE_IF_PRESENT_FLAG;
import static org.springframework.integration.smpp.core.SmppConstants.SCHEDULED_DELIVERY_TIME;
import static org.springframework.integration.smpp.core.SmppConstants.SERVICE_TYPE;
import static org.springframework.integration.smpp.core.SmppConstants.SMS_MSG;
import static org.springframework.integration.smpp.core.SmppConstants.SM_DEFAULT_MSG_ID;
import static org.springframework.integration.smpp.core.SmppConstants.SRC_ADDR;
import static org.springframework.integration.smpp.core.SmppConstants.SRC_NPI;
import static org.springframework.integration.smpp.core.SmppConstants.SRC_TON;
import static org.springframework.integration.smpp.core.SmppConstants.VALIDITY_PERIOD;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang.math.RandomUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jsmpp.bean.Alphabet;
import org.jsmpp.bean.DataCoding;
import org.jsmpp.bean.DataCodings;
import org.jsmpp.bean.DeliverSm;
import org.jsmpp.bean.ESMClass;
import org.jsmpp.bean.GSMSpecificFeature;
import org.jsmpp.bean.GeneralDataCoding;
import org.jsmpp.bean.MessageClass;
import org.jsmpp.bean.NumberingPlanIndicator;
import org.jsmpp.bean.OptionalParameter;
import org.jsmpp.bean.OptionalParameters;
import org.jsmpp.bean.RegisteredDelivery;
import org.jsmpp.bean.SMSCDeliveryReceipt;
import org.jsmpp.bean.TypeOfNumber;
import org.jsmpp.session.ClientSession;
import org.jsmpp.session.SMPPSession;
import org.jsmpp.util.AbsoluteTimeFormatter;
import org.jsmpp.util.TimeFormatter;

import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Fluent API to help make specifying all these parameters just a <em>tiny</em> bit easier.
 * For internal use only.
 *
 * @author Josh Long
 * @author Edge Dalmacio
 * @author Flemming Jønsson
 * @since 1.0
 */
public class SmesMessageSpecification {

	private static Log log = LogFactory.getLog(SmesMessageSpecification.class);

	private TimeFormatter timeFormatter = new AbsoluteTimeFormatter();

	private int maxLengthSmsMessages = 140;

	private String sourceAddress;

	private String destinationAddress;

	private String serviceType;

	private TypeOfNumber sourceAddressTypeOfNumber;

	private NumberingPlanIndicator sourceAddressNumberingPlanIndicator;

	private TypeOfNumber destinationAddressTypeOfNumber;

	private NumberingPlanIndicator destinationAddressNumberingPlanIndicator;

	private ESMClass esmClass;

	private byte protocolId;

	private byte priorityFlag;

	private String scheduleDeliveryTime = timeFormatter.format(new Date());

	private String validityPeriod;

	private RegisteredDelivery registeredDelivery;

	private byte replaceIfPresentFlag;

	private DataCoding dataCoding;

	private byte smDefaultMsgId;

	private byte[] shortMessage;

	private List<byte[]> shortMessageParts;

	private ClientSession smppSession;

	private OptionalParameter messagePayloadParameter;

	/**
	 * This method takes an inbound SMS message and converts it to a Spring Integration message
	 * @param dsm the {@link DeliverSm} from
	 * {@link AbstractReceivingMessageListener#onTextMessage(org.jsmpp.bean.DeliverSm, String)}
	 * @param txtMessage the String from
	 * {@link AbstractReceivingMessageListener#onTextMessage(org.jsmpp.bean.DeliverSm, String)}
	 * @return a Spring Integration message
	 */
	public static Message<?> toMessageFromSms(DeliverSm dsm, String txtMessage) {

		Assert.isTrue(!dsm.isSmscDeliveryReceipt(), "the message should not be a delivery confirmation receipt!");

		MessageBuilder<String> mb = MessageBuilder.withPayload(txtMessage);
		mb.setHeader(SmppConstants.SMS, dsm);
		mb.setHeader(SmppConstants.REPLACE_IF_PRESENT, dsm.getReplaceIfPresent());
		mb.setHeader(SmppConstants.SHORT_MESSAGE, dsm.getShortMessage());
		mb.setHeader(SmppConstants.OPTIONAL_PARAMETERS, dsm.getOptionalParameters());
		mb.setHeader(SmppConstants.UDHI_AND_REPLY_PATH, dsm.isUdhiAndReplyPath());
		mb.setHeader(SmppConstants.VALIDITY_PERIOD, dsm.getValidityPeriod());
		mb.setHeader(SmppConstants.COMMAND_LENGTH, dsm.getCommandLength());
		mb.setHeader(SmppConstants.COMMAND_ID, dsm.getCommandId());
		mb.setHeader(SmppConstants.SME_ACK_NOT_REQUESTED, dsm.isSmeAckNotRequested());
		mb.setHeader(SmppConstants.DATA_CODING, dsm.getDataCoding());
		mb.setHeader(SmppConstants.REPLY_PATH, dsm.isReplyPath());
		mb.setHeader(SmppConstants.SOURCE_ADDR_TON, dsm.getSourceAddrTon());
		mb.setHeader(SmppConstants.SM_DEFAULT_MSG_ID, dsm.getSmDefaultMsgId());
		mb.setHeader(SmppConstants.UDHI, dsm.isUdhi());
		mb.setHeader(SmppConstants.SME_MANUAL_ACKNOWLEDGMENT, dsm.isSmeManualAcknowledgment());
		mb.setHeader(SmppConstants.CONVERSATION_ABORT, dsm.isConversationAbort());
		mb.setHeader(SmppConstants.DEST_ADDRESS, dsm.getDestAddress());
		mb.setHeader(SmppConstants.ESM_CLASS, dsm.getEsmClass());
		mb.setHeader(SmppConstants.COMMAND_ID_AS_HEX, dsm.getCommandIdAsHex());
		mb.setHeader(SmppConstants.SME_DELIVERY_AND_MANUAL_ACK_REQUESTED, dsm.isSmeDeliveryAndManualAckRequested());
		mb.setHeader(SmppConstants.SMSC_DELIVERY_RECEIPT, dsm.isSmscDeliveryReceipt());
		mb.setHeader(SmppConstants.SME_MANUAL_ACK_REQUESTED, dsm.isSmeManualAckRequested());
		mb.setHeader(SmppConstants.PRIORITY_FLAG, dsm.getPriorityFlag());
		mb.setHeader(SmppConstants.DEST_ADDR_TON, dsm.getDestAddrTon());
		mb.setHeader(SmppConstants.COMMAND_STATUS_AS_HEX, dsm.getCommandStatusAsHex());
		mb.setHeader(SmppConstants.SERVICE_TYPE, dsm.getServiceType());
		mb.setHeader(SmppConstants.INTERMEDIATE_DELIVERY_NOTIFICATION, dsm.isIntermedietDeliveryNotification());
		mb.setHeader(SmppConstants.SOURCE_ADDR_NPI, dsm.getSourceAddrNpi());
		mb.setHeader(SmppConstants.REGISTERED_DELIVERY, dsm.getRegisteredDelivery());
		mb.setHeader(SmppConstants.DEST_ADDR_NPI, dsm.getDestAddrNpi());
		mb.setHeader(SmppConstants.COMMAND_STATUS, dsm.getCommandStatus());
		mb.setHeader(SmppConstants.DEFAULT_MESSAGE_TYPE, dsm.isDefaultMessageType());
		mb.setHeader(SmppConstants.PROTOCOL_ID, dsm.getProtocolId());
		mb.setHeader(SmppConstants.SOURCE_ADDR, dsm.getSourceAddr());
		mb.setHeader(SmppConstants.SEQUENCE_NUMBER, dsm.getSequenceNumber());
		mb.setHeader(SmppConstants.SCHEDULE_DELIVERY_TIME, dsm.getScheduleDeliveryTime());
		mb.setHeader(SmppConstants.SME_DELIVERY_ACK_REQUESTED, dsm.isSmeDeliveryAckRequested());
		return mb.build();
	}

	/**
	 * this method will take an inbound Spring Integration {@link Message}
	 * and map it to a {@link SmesMessageSpecification}
	 * which we can use to send the SMS message.
	 * @param msg a new {@link Message}
	 * @param smppSession the SMPPSession
	 * @return a {@link SmesMessageSpecification}
	 */
	public static SmesMessageSpecification fromMessage(ClientSession smppSession, Message<?> msg) {
		if (log.isDebugEnabled()) {
			log.debug("Message: " + msg);
		}
		String srcAddy = valueIfHeaderExists(SRC_ADDR, msg);
		String dstAddy = valueIfHeaderExists(DST_ADDR, msg);
		String smsTxt = valueIfHeaderExists(SMS_MSG, msg);
		if (!StringUtils.hasText(smsTxt)) {
			Object payload = msg.getPayload();
			if (payload instanceof String) {
				smsTxt = (String) payload;
			}
		}
		final DataCoding dataCodingFromHeader = SmesMessageSpecification.dataCodingFromHeader(msg);
		final SmesMessageSpecification spec = new SmesMessageSpecification()
				.reset()
				.setSmppSession(smppSession)
				.setSourceAddress(srcAddy)
				.setDestinationAddress(dstAddy)
				.setDataCoding(dataCodingFromHeader);
		spec.setMaxLengthSmsMessages(maximumCharactersFromHeader(msg));
		spec.setEsmClass(SmesMessageSpecification.esmClassFromHeader(msg));
		if (msg.getHeaders().containsKey(SmppConstants.USE_MSG_PAYLOAD_PARAM)) {
			spec.setShortMessageUsingPayload(smsTxt);
		}
		else {
			spec.setShortTextMessage(smsTxt);
		}
		spec.setDestinationAddressNumberingPlanIndicator(
				SmesMessageSpecification.<NumberingPlanIndicator>valueIfHeaderExists(DST_NPI, msg));
		spec.setSourceAddressNumberingPlanIndicator(
				SmesMessageSpecification.<NumberingPlanIndicator>valueIfHeaderExists(SRC_NPI, msg));
		spec.setDestinationAddressTypeOfNumber(SmesMessageSpecification.<TypeOfNumber>valueIfHeaderExists(DST_TON, msg));
		spec.setSourceAddressTypeOfNumber(SmesMessageSpecification.<TypeOfNumber>valueIfHeaderExists(SRC_TON, msg));
		spec.setServiceType(SmesMessageSpecification.<String>valueIfHeaderExists(SERVICE_TYPE, msg));
		spec.setScheduleDeliveryTime(SmesMessageSpecification.<Date>valueIfHeaderExists(SCHEDULED_DELIVERY_TIME, msg));
		spec.setValidityPeriod(SmesMessageSpecification.<String>valueIfHeaderExists(VALIDITY_PERIOD, msg));

		// byte landmine. autoboxing causes havoc with <em>null</em> bytes.
		Byte priorityFlag1 = SmesMessageSpecification.<Byte>valueIfHeaderExists(PRIORITY_FLAG, msg);
		if (priorityFlag1 != null) {
			spec.setPriorityFlag(priorityFlag1);
		}

		Byte smDefaultMsgId1 = SmesMessageSpecification.<Byte>valueIfHeaderExists(SM_DEFAULT_MSG_ID, msg);
		if (smDefaultMsgId1 != null) {
			spec.setSmDefaultMsgId(smDefaultMsgId1);
		}

		Byte replaceIfPresentFlag1 = SmesMessageSpecification.<Byte>valueIfHeaderExists(REPLACE_IF_PRESENT_FLAG, msg);
		if (replaceIfPresentFlag1 != null) {
			spec.setReplaceIfPresentFlag(replaceIfPresentFlag1);
		}

		Byte protocolId1 = SmesMessageSpecification.<Byte>valueIfHeaderExists(PROTOCOL_ID, msg);
		if (null != protocolId1) {
			spec.setProtocolId(protocolId1);
		}

		spec.setRegisteredDelivery(registeredDeliveryFromHeader(msg));

		return spec;
	}

	private static DataCoding dataCodingFromHeader(Message<?> msg) {
		Object dc = msg.getHeaders().get(DATA_CODING);
		if (dc instanceof DataCoding) {
			return (DataCoding) dc;
		}
		if (dc instanceof Byte) {
			return DataCodings.newInstance((Byte) dc);
		}

		return null;
	}

	/**
	 * Getting maximum characters from header. This will allow checking maximum character based on
	 * {@link SmppConstants#MAXIMUM_CHARACTERS} header or determine the maximum character based on data coding
	 * header {@link SmppConstants#DATA_CODING}.
	 * <p/>
	 * The order of the selection is
	 * <ol>
	 *     <li>If {@link SmppConstants#MAXIMUM_CHARACTERS} is set, use it</li>
	 *     <li>If {@link SmppConstants#DATA_CODING} is set, find maximum character for the data coding</li>
	 *     <li>Using default maximum character which is 140</li>
	 * </ol>
	 * @param msg the Spring Integration message
	 * @return maximum character can be sent through the session
	 */
	private static int maximumCharactersFromHeader(Message<?> msg) {
		if (msg.getHeaders().containsKey(MAXIMUM_CHARACTERS)) {
			return msg.getHeaders().get(MAXIMUM_CHARACTERS, Integer.class);
		}
		if (msg.getHeaders().containsKey(DATA_CODING)) {
			final Object dc = msg.getHeaders().get(DATA_CODING);
			if (dc instanceof Byte) {
				return DataCodingSpecification.getMaxCharacters((Byte) dc);
			}
			else {
				return DataCodingSpecification.getMaxCharacters(((DataCoding) dc).toByte());
			}
		}
		return 140;
	}

	/**
	 * need to be a little flexibile about what we take in as
	 * {@link SmppConstants#REGISTERED_DELIVERY_MODE}. The value can
	 * be a String or a member of the {@link SMSCDeliveryReceipt} enum.
	 * @param msg the Spring Integration message
	 * @return a value for {@link RegisteredDelivery} or null, which is
	 * good because it'll simply let the existing default work
	 */
	private static RegisteredDelivery registeredDeliveryFromHeader(Message<?> msg) {
		Object rd = valueIfHeaderExists(REGISTERED_DELIVERY_MODE, msg);

		if (rd instanceof String) {
			String rdString = (String) rd;
			SMSCDeliveryReceipt smscDeliveryReceipt = SMSCDeliveryReceipt.valueOf(rdString);
			Assert.notNull(smscDeliveryReceipt, "the registeredDelivery can't be null");
			return new RegisteredDelivery(smscDeliveryReceipt);
		}

		if (rd instanceof SMSCDeliveryReceipt) {
			SMSCDeliveryReceipt smscDeliveryReceipt = (SMSCDeliveryReceipt) rd;
			return new RegisteredDelivery(smscDeliveryReceipt);
		}

		if (rd instanceof RegisteredDelivery) {
			return (RegisteredDelivery) rd;
		}
		return null;
	}

	/**
	 * you need to use the builder API
	 *
	 * @param smppSession the SMPPSession instance against which we should work.
	 * @see SmesMessageSpecification#SmesMessageSpecification()
	 */
	SmesMessageSpecification(SMPPSession smppSession) {
		this.smppSession = smppSession;
	}

	/**
	 * tries to safely extract the ESMClass
	 * @param im message
	 * @return esm class
	 */
	static private ESMClass esmClassFromHeader(Message<?> im) {
		String h = ESM_CLASS;
		Object o = valueIfHeaderExists(h, im);
		ESMClass response = null;
		if (o instanceof Byte) {
			response = new ESMClass((Byte) o);

		}
		else if (o instanceof ESMClass) {
			response = (ESMClass) o;
		}
		return response;
	}

	@SuppressWarnings("unchecked")
	static private <T> T valueIfHeaderExists(String h, Message<?> msg) {
		if (msg != null && msg.getHeaders().containsKey(h)) {
			return (T) msg.getHeaders().get(h);
		}
		return null;
	}

	/**
	 * Everybody else has to use the builder API. DO NOT make this private
	 * or it will not be proxied and that will make me sad!
	 * @param ss the {@link SMPPSession}
	 * @return the current spec
	 */
	SmesMessageSpecification setSmppSession(ClientSession ss) {
		this.smppSession = ss;
		return this;
	}

	/**
	 * use the builder API, but we need this to cleanly proxy
	 */
	SmesMessageSpecification() {
		this(null);
	}

	/**
	 * Conceptually, you could get away with just specifying these three parameters,
	 * though I don't know how likely that is in practice.
	 * @param srcAddress    the source address
	 * @param destAddress the destination address
	 * @param txtMessage    the message to send (must be  no more than 140 characters
	 * @param ss the SMPPSession
	 * @return the {@link SmesMessageSpecification}
	 */
	public static SmesMessageSpecification newSmesMessageSpecification(ClientSession ss, String srcAddress,
			String destAddress, String txtMessage) {

		SmesMessageSpecification smesMessageSpecification = new SmesMessageSpecification();

		smesMessageSpecification
				.reset()
				.setSmppSession(ss)
				.setSourceAddress(srcAddress)
				.setDestinationAddress(destAddress)
				.setShortTextMessage(txtMessage);

		return smesMessageSpecification;
	}

	/**
	 * Only sets the #sourceAddressTypeOfNumber if the current value is null, otherwise, it leaves it.
	 * @param sourceAddressTypeOfNumberIfRequired
	 *         the {@link TypeOfNumber}
	 * @return this
	 */
	public SmesMessageSpecification setSourceAddressTypeOfNumberIfRequired(TypeOfNumber sourceAddressTypeOfNumberIfRequired) {
		if (this.sourceAddressTypeOfNumber == null) {
			this.sourceAddressTypeOfNumber = sourceAddressTypeOfNumberIfRequired;
		}
		return this;
	}

	/**
	 * send the message on its way.
	 * <p/>
	 * todo can we do something smart here or through an adapter to handle the situation
	 * where we have asked for a message receipt? what about if we're using a message
	 * receipt <em>and</em> we're only a receiver or a sender connection and not a transceiver?
	 * We need gateway semantics across two unidirectional SMPPSessions, then
	 * @return the messageId(s) (required if you want to then track it
	 * or correlate it with message receipt confirmations)
	 * @throws Exception the
	 * {@link SMPPSession#submitShortMessage(String, org.jsmpp.bean.TypeOfNumber,
	 * org.jsmpp.bean.NumberingPlanIndicator, String, org.jsmpp.bean.TypeOfNumber,
	 * org.jsmpp.bean.NumberingPlanIndicator, String, org.jsmpp.bean.ESMClass,
	 * byte, byte, String, String, org.jsmpp.bean.RegisteredDelivery, byte,
	 * org.jsmpp.bean.DataCoding, byte, byte[], org.jsmpp.bean.OptionalParameter...)}
	 * method throws lots of Exceptions, including {@link java.io.IOException}
	 */
	public List<String> send() throws Exception {
		validate();
		List<String> msgIds = new LinkedList<String>();
		if (messagePayloadParameter == null) {
			if (this.shortMessageParts.isEmpty()) {
				String msgId = this.smppSession.submitShortMessage(
						this.serviceType,
						this.sourceAddressTypeOfNumber,
						this.sourceAddressNumberingPlanIndicator,
						this.sourceAddress,

						this.destinationAddressTypeOfNumber,
						this.destinationAddressNumberingPlanIndicator,
						this.destinationAddress,

						this.esmClass,
						this.protocolId,
						this.priorityFlag,
						this.scheduleDeliveryTime,
						this.validityPeriod,
						this.registeredDelivery,
						this.replaceIfPresentFlag,
						this.dataCoding,
						this.smDefaultMsgId,
						this.shortMessage);
				msgIds.add(msgId);
			}
			else {
				if (log.isDebugEnabled()) {
					log.debug("Sending message using sar_msg_ref_num, sar_segment_seqnum and sar_total_segments");
				}
				OptionalParameter sarMsgRefNum = OptionalParameters.newSarMsgRefNum(RandomUtils.nextInt(0x10000));
				OptionalParameter sarTotalSegments = OptionalParameters.newSarTotalSegments(shortMessageParts.size());
				String charsetName = DataCodingSpecification.getCharsetName(dataCoding.toByte());
				for (int i = 0; i < shortMessageParts.size(); i++) {
					byte[] shortMessagePart = shortMessageParts.get(i);
					String msgId = this.smppSession.submitShortMessage(
							this.serviceType,
							this.sourceAddressTypeOfNumber,
							this.sourceAddressNumberingPlanIndicator,
							this.sourceAddress,

							this.destinationAddressTypeOfNumber,
							this.destinationAddressNumberingPlanIndicator,
							this.destinationAddress,

							this.esmClass,
							this.protocolId,
							this.priorityFlag,
							this.scheduleDeliveryTime,
							this.validityPeriod,
							this.registeredDelivery,
							this.replaceIfPresentFlag,
							this.dataCoding,
							this.smDefaultMsgId,
							shortMessagePart,
							sarMsgRefNum,
							OptionalParameters.newSarSegmentSeqnum(i + 1),
							sarTotalSegments);
					if (log.isDebugEnabled()) {
						log.debug("sent message : " + new String(shortMessagePart, charsetName));
						log.debug("message ID for the sent message is: " + msgId);
					}
					msgIds.add(msgId);
				}
			}
		}
		else {
			// SPEC 3.2.3
			log.debug("Sending message using message_payload");
			String msgId = this.smppSession.submitShortMessage(
					this.serviceType,
					this.sourceAddressTypeOfNumber,
					this.sourceAddressNumberingPlanIndicator,
					this.sourceAddress,

					this.destinationAddressTypeOfNumber,
					this.destinationAddressNumberingPlanIndicator,
					this.destinationAddress,

					this.esmClass,
					this.protocolId,
					this.priorityFlag,
					this.scheduleDeliveryTime,
					this.validityPeriod,
					this.registeredDelivery,
					this.replaceIfPresentFlag,
					this.dataCoding,
					this.smDefaultMsgId,
					new byte[0],
					this.messagePayloadParameter
			);
			msgIds.add(msgId);
		}

		return Collections.unmodifiableList(msgIds);
	}

	protected void validate() {
		Assert.notNull(this.sourceAddress, "the source address must not be null");
		Assert.notNull(this.destinationAddress, "the destination address must not be null");
		final boolean shortMessageSet = this.shortMessage != null && this.shortMessage.length > 0
				|| !shortMessageParts.isEmpty();
		Assert.isTrue(messagePayloadParameter != null ^ shortMessageSet,
				"message can only be set in payload or short message. cannot be both");
		if (messagePayloadParameter == null) {
			Assert.isTrue(shortMessageSet, "the message must not be null");
		}
	}

	public SmesMessageSpecification setSourceAddress(String sourceAddr) {
		if (!nullHeaderWillOverwriteDefault(sourceAddr)) {
			this.sourceAddress = sourceAddr;
		}
		return this;
	}

	/**
	 * the 'to' phone number
	 *
	 * @param destinationAddr the phone number
	 * @return the current spec
	 */
	public SmesMessageSpecification setDestinationAddress(String destinationAddr) {
		this.destinationAddress = destinationAddr;
		return this;
	}

	public SmesMessageSpecification setServiceType(String serviceType) {
		if (!nullHeaderWillOverwriteDefault(serviceType)) {
			this.serviceType = serviceType;
		}
		return this;
	}

	public SmesMessageSpecification setSourceAddressTypeOfNumber(TypeOfNumber sourceAddrTon) {
		if (!nullHeaderWillOverwriteDefault(sourceAddrTon)) {
			this.sourceAddressTypeOfNumber = sourceAddrTon;
		}
		return this;
	}

	public SmesMessageSpecification setSourceAddressNumberingPlanIndicator(NumberingPlanIndicator sourceAddrNpi) {
		if (!nullHeaderWillOverwriteDefault(sourceAddrNpi)) {
			this.sourceAddressNumberingPlanIndicator = sourceAddrNpi;
		}
		return this;
	}

	public SmesMessageSpecification setDestinationAddressTypeOfNumber(TypeOfNumber destAddrTon) {
		if (!nullHeaderWillOverwriteDefault(destAddrTon)) {
			this.destinationAddressTypeOfNumber = destAddrTon;
		}
		return this;
	}

	/**
	 * guard against overwriting perfectly good defaults with null values.
	 *
	 * @param v value the value
	 * @return can the write proceed unabated?
	 */
	private boolean nullHeaderWillOverwriteDefault(Object v) {
		if (v == null) {
			if (log.isDebugEnabled()) {
				log.debug("There is a default in place for this property; don't overwrite it with null");
			}
			return true;
		}
		return false;
	}

	public SmesMessageSpecification setDestinationAddressNumberingPlanIndicator(NumberingPlanIndicator destAddrNpi) {
		if (!nullHeaderWillOverwriteDefault(destAddrNpi)) {
			this.destinationAddressNumberingPlanIndicator = destAddrNpi;
		}
		return this;
	}

	public SmesMessageSpecification setEsmClass(ESMClass esmClass) {
		if (!nullHeaderWillOverwriteDefault(esmClass)) {
			this.esmClass = esmClass;
		}
		return this;
	}

	public SmesMessageSpecification setProtocolId(byte protocolId) {
		if (!nullHeaderWillOverwriteDefault(protocolId)) {
			this.protocolId = protocolId;
		}
		return this;
	}

	public SmesMessageSpecification setPriorityFlag(byte pf) {
		if (!nullHeaderWillOverwriteDefault(pf)) {
			this.priorityFlag = pf;
		}
		return this;
	}

	/**
	 * When you submit a message to an SMSC, it is possible to sometimes specify a
	 * <em>validity period</em> for the message. This setting is an instruction to
	 * the SMSC that stipulates that if the message cannot be delivered to
	 * the recipient within the next N minutes or hours or days,
	 * the SMSC should discard the message. This would mean that if the recipient'running
	 * mobile phone is turned off, or outSession of coverage for x minutes/hours/days
	 * after the message is submitted, the SMSC should not perform further delivery
	 * retry and should discard the message.
	 * <p/>
	 * Of course, there is no guarantee that the operator SMSC will respect this setting,
	 * so it needs to be tested with a particular operator first to determine if
	 * it can be used reliably.
	 * <p/>
	 * That information came from <a href="http://www.nowsms.com/smpp-information">the NowSMS website.</a>.
	 * @param v the period of validity. There are specific formats for this,
	 * however this method provides no validation.
	 *          <p/>
	 *          todo provide format validation if possible
	 * @return the current SmesMessageSpecification
	 */
	public SmesMessageSpecification setValidityPeriod(String v) {
		if (!nullHeaderWillOverwriteDefault(v)) {
			this.validityPeriod = v;
		}
		return this;
	}

	public SmesMessageSpecification setScheduleDeliveryTime(Date d) {
		if (!nullHeaderWillOverwriteDefault(d)) {
			this.scheduleDeliveryTime = timeFormatter.format(d);
		}
		return this;
	}

	public SmesMessageSpecification setRegisteredDelivery(RegisteredDelivery rd) {
		if (!nullHeaderWillOverwriteDefault(rd)) {
			this.registeredDelivery = rd;
		}
		return this;
	}

	public SmesMessageSpecification setReplaceIfPresentFlag(byte replaceIfPresentFlag) {
		if (!nullHeaderWillOverwriteDefault(replaceIfPresentFlag)) {
			this.replaceIfPresentFlag = replaceIfPresentFlag;
		}
		return this;
	}

	public SmesMessageSpecification setDataCoding(DataCoding dataCoding) {
		if (!nullHeaderWillOverwriteDefault(dataCoding)) {
			this.dataCoding = dataCoding;
		}
		return this;
	}

	public SmesMessageSpecification setSmDefaultMsgId(byte smDefaultMsgId) {
		this.smDefaultMsgId = smDefaultMsgId;
		return this;
	}

	public SmesMessageSpecification setTimeFormatter(TimeFormatter timeFormatter) {
		if (!nullHeaderWillOverwriteDefault(timeFormatter)) {
			this.timeFormatter = timeFormatter;
		}
		return this;
	}

	/**
	 * Setting short message. This will take into account if {@link #dataCoding}
	 * or if {@link #maxLengthSmsMessages}
	 * is set through header to validate the maximum characters can be set.
	 * @param s the text message body
	 * @return the SmesMessageSpecification
	 */
	public SmesMessageSpecification setShortTextMessage(String s) {
		Assert.notNull(s, "the SMS message payload must not be null");
		if (esmClass != null && GSMSpecificFeature.UDHI.containedIn(esmClass)) {
			log.debug("Setting short message with UDH");
			this.shortMessage = UdhUtil.getMessageWithUdhInBytes(s, dataCoding.toByte());
		}
		else {
			if (s.length() > this.maxLengthSmsMessages) {
				for (String split : splitToLength(s, this.maxLengthSmsMessages - 5)) {
					this.shortMessageParts.add(DataCodingSpecification.getMessageInBytes(split, dataCoding.toByte()));
				}
			}
			else {
				this.shortMessage = DataCodingSpecification.getMessageInBytes(s, dataCoding.toByte());
			}
		}
		return this;
	}

	private List<String> splitToLength(String text, int subStringSize) {
		List<String> substrings = new ArrayList<String>((text.length() + subStringSize - 1) / subStringSize);
		for (int idx = 0; idx < text.length(); idx += subStringSize) {
			String substring = text.substring(idx, Math.min(text.length(), idx + subStringSize));
			substrings.add(substring);
		}
		return substrings;
	}

	/**
	 * Setting short message using message_payload ({@link org.jsmpp.bean.OptionalParameter.Tag#MESSAGE_PAYLOAD})
	 * optional parameter
	 * @param s the text messages body
	 * @return the SmesMessageSpecification
	 */
	public SmesMessageSpecification setShortMessageUsingPayload(String s) {
		final byte[] content = DataCodingSpecification.getMessageInBytes(s, dataCoding.toByte());
		this.messagePayloadParameter =
				new OptionalParameter.OctetString(OptionalParameter.Tag.MESSAGE_PAYLOAD.code(), content);
		return this;
	}

	/**
	 * this is a good value, but not strictly speaking universal.
	 * This is intended only for exceptional configuration cases
	 * <p/>
	 * See: http://www.nowsms.com/long-sms-text-messages-and-the-160-character-limit
	 *
	 * @param maxLengthSmsMessages the length of sms messages
	 * @see #setShortTextMessage(String)
	 */
	public void setMaxLengthSmsMessages(int maxLengthSmsMessages) {
		this.maxLengthSmsMessages = maxLengthSmsMessages;
	}

	/**
	 * Resets the thread local, pooled objects to a known state before reuse.
	 * <p/>
	 * Resetting the variables is trivially cheap compared to proxying a new one each time.
	 *
	 * @return the cleaned up {@link SmesMessageSpecification}
	 */
	protected SmesMessageSpecification reset() {

		// configuration params - should they be reset?
		maxLengthSmsMessages = 140;
		timeFormatter = new AbsoluteTimeFormatter();

		sourceAddress = null;
		destinationAddress = null;
		serviceType = "CMT";
		sourceAddressTypeOfNumber = TypeOfNumber.UNKNOWN;
		sourceAddressNumberingPlanIndicator = NumberingPlanIndicator.UNKNOWN;
		destinationAddressTypeOfNumber = TypeOfNumber.UNKNOWN;
		destinationAddressNumberingPlanIndicator = NumberingPlanIndicator.UNKNOWN;
		esmClass = new ESMClass();
		protocolId = 0;
		priorityFlag = 1;
		scheduleDeliveryTime = null;
		validityPeriod = null;
		registeredDelivery = new RegisteredDelivery(SMSCDeliveryReceipt.DEFAULT);
		replaceIfPresentFlag = 0;
		dataCoding = new GeneralDataCoding(Alphabet.ALPHA_DEFAULT, MessageClass.CLASS1, false);
		smDefaultMsgId = 0;
		shortMessage = null; // the bytes to the 140 character text message
		shortMessageParts = new ArrayList<byte[]>();
		smppSession = null;
		messagePayloadParameter = null;
		return this;
	}

	public SmesMessageSpecification setSourceAddressIfRequired(String defaultSourceAddress) {
		if (!StringUtils.hasText(this.sourceAddress)) {
			this.sourceAddress = defaultSourceAddress;
		}
		return this;
	}

}

/*	private static String fromPropertyToHeaderConstant(String n) {

		StringBuffer stringBuffer = new StringBuffer();

		for (char c : n.toCharArray()) {
			if (Character.isUpperCase(c)) {
				stringBuffer.append("_");
			}
			stringBuffer.append(c);
		}

		String nn = stringBuffer.toString().toUpperCase();

		String is = "IS_",
				get = "GET_";
		if (nn.startsWith(is)) nn = nn.substring(is.length());
		if (nn.startsWith(get)) nn = nn.substring(get.length());

		return nn;
	}

	static public void main(String[] args) throws Throwable {
		String m = "mb.setHeader( SmppConstants.%s,  dsm.%s() );";
		String h = "public static final String %s = \"%s\";";
		Set<String> marshalling = new HashSet<String>();
		Set<String> headers = new HashSet<String>();
		PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(DeliverSm.class);
		for (PropertyDescriptor propertyDescriptor : pds) {
			Method reader = propertyDescriptor.getReadMethod();
			String readerName = reader.getName();

			String header = fromPropertyToHeaderConstant(readerName);
			headers.add(header);
			marshalling.add(readerName + ":" + header);
		}

		for (String s : headers) System.outSession.println(String.format(h, s, s));

		for (String s : marshalling) {

			String[] tuple = s.split(":");

			System.outSession.println(String.format(m, tuple[1], tuple[0]));
		}
	}
*/
