/**
 * 
 * Copyright 2004 Protique Ltd
 * 
 * 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.activemq.transport.jabber;

import org.activemq.io.AbstractWireFormat;
import org.activemq.io.WireFormat;
import org.activemq.io.util.ByteArray;
import org.activemq.message.ActiveMQBytesMessage;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ActiveMQObjectMessage;
import org.activemq.message.ActiveMQTextMessage;
import org.activemq.message.ConnectionInfo;
import org.activemq.message.Packet;
import org.activemq.util.IdGenerator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.jms.Destination;
import javax.jms.JMSException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A wire format which uses XMPP format of messages
 *
 * @version $Revision: 1.1 $
 */
public class JabberWireFormat extends AbstractWireFormat {
    private static final Log log = LogFactory.getLog(JabberWireFormat.class);

    private static final String NAMESPACE = "http://etherx.jabber.org/streams";
    private static final String QUEUE_PREFIX = "queue:";
    private static final String TEMP_QUEUE_PREFIX = "tempQueue:";
    private static final String TEMP_TOPIC_PREFIX = "tempTopic:";

    private static final QName STREAM_QNAME = new QName("http://etherx.jabber.org/streams", "stream", "stream");

    private IdGenerator idGenerator = new IdGenerator();
    private String clientID;
    private String producerID;
    private String consumerID;
    private ConnectionInfo connectionInfo;
    private PrintWriter writer;

    public WireFormat copy() {
        return new JabberWireFormat();
    }

    public Packet readPacket(DataInput in) throws IOException {
        return null;  /** TODO */
    }

    public Packet readPacket(int firstByte, DataInput in) throws IOException {
        return null;  /** TODO */
    }


    /**
     * Reads a packet from the XML stream
     */
    public Packet readPacket(XMLStreamReader reader) throws XMLStreamException, JMSException {
        while (reader.hasNext()) {
            int type = reader.next();
            switch (type) {
                case XMLStreamConstants.START_ELEMENT:
                    return readMessage(reader);

                case XMLStreamConstants.END_DOCUMENT:
                    connectionInfo.setStarted(false);
                    return connectionInfo;
            }
        }
        return null;
    }

    /**
     * Reads the opening connection start
     */
    public Packet readConnectionStart(XMLStreamReader reader) throws XMLStreamException, JMSException {
        while (reader.hasNext()) {
            int type = reader.next();
            switch (type) {
                case XMLStreamConstants.START_ELEMENT:
                    QName name = reader.getName();
                    if (!name.equals(STREAM_QNAME)) {
                        log.warn("Bad initial QName for stream. Received: " + name + " while expecting: " + STREAM_QNAME);
                    }
                    return readConnectionInfo(reader);
            }
        }
        return null;
    }

    public Packet writePacket(Packet packet, DataOutput out) throws IOException, JMSException {
        switch (packet.getPacketType()) {
            case Packet.ACTIVEMQ_MESSAGE:
                writeMessage((ActiveMQMessage) packet, "", out);
                break;

            case Packet.ACTIVEMQ_TEXT_MESSAGE:
                writeTextMessage((ActiveMQTextMessage) packet, out);
                break;

            case Packet.ACTIVEMQ_BYTES_MESSAGE:
                writeBytesMessage((ActiveMQBytesMessage) packet, out);
                break;

            case Packet.ACTIVEMQ_OBJECT_MESSAGE:
                writeObjectMessage((ActiveMQObjectMessage) packet, out);
                break;

            case Packet.ACTIVEMQ_CONNECTION_INFO:
                writeConnectionInfo((ConnectionInfo) packet, out);
                break;

            case Packet.ACTIVEMQ_MAP_MESSAGE:
            case Packet.ACTIVEMQ_STREAM_MESSAGE:


            case Packet.ACTIVEMQ_BROKER_INFO:
            case Packet.ACTIVEMQ_MSG_ACK:
            case Packet.CONSUMER_INFO:
            case Packet.DURABLE_UNSUBSCRIBE:
            case Packet.INT_RESPONSE_RECEIPT_INFO:
            case Packet.PRODUCER_INFO:
            case Packet.RECEIPT_INFO:
            case Packet.RESPONSE_RECEIPT_INFO:
            case Packet.SESSION_INFO:
            case Packet.TRANSACTION_INFO:
            case Packet.XA_TRANSACTION_INFO:
            default:
                System.out.println("Ignoring message type: " + packet.getPacketType() + " packet: " + packet);
                log.warn("Ignoring message type: " + packet.getPacketType() + " packet: " + packet);
        }
        writer.flush();
        return null;
    }

    /**
     * Can this wireformat process packets of this version
     *
     * @param version the version number to test
     * @return true if can accept the version
     */
    public boolean canProcessWireFormatVersion(int version) {
        return true;
    }

    /**
     * @return the current version of this wire format
     */
    public int getCurrentWireFormatVersion() {
        return 1;
    }

    public PrintWriter getWriter() {
        return writer;
    }

    public void setWriter(PrintWriter writer) {
        this.writer = writer;
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    protected Packet readConnectionInfo(XMLStreamReader reader) throws XMLStreamException {
        connectionInfo = new ConnectionInfo();
        connectionInfo.setStarted(true);
        connectionInfo.setClientId(idGenerator.generateId());
        connectionInfo.setClientVersion("" + getCurrentWireFormatVersion());
        producerID = idGenerator.generateId();
        consumerID = idGenerator.generateId();
        return connectionInfo;
    }


    protected void writeConnectionInfo(ConnectionInfo packet, DataOutput out) throws IOException {
        System.out.println("#### writing connection info");

        if (packet.isStarted()) {
            String from = packet.getHostName();
            if (from == null) {
                from = "localhost";
            }
            String id = packet.getClientId();
            if (id == null) {
                id = "";
            }
            writer.println("<?xml version='1.0'?>");
            writer.println("<stream:stream");
            writer.println("  from='" + from + "'");
            writer.println("  id='" + id + "'");
            writer.println("  xmlns='jabber:client'");
            writer.println("  xmlns:stream='http://etherx.jabber.org/streams'");
            writer.println("  version='1.0'>");
            writer.println("<stream:features>");
            writer.println("  <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>");
            writer.println("    <required/>");
            writer.println("  </starttls>");
            writer.println("  <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>");
            writer.println("    <mechanism>DIGEST-MD5</mechanism>");
            writer.println("    <mechanism>PLAIN</mechanism>");
            writer.println("  </mechanisms>");
            writer.println("</stream:features>");
            writer.println("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
        }
        else {
            writer.println("</stream:stream>");
        }
        writer.flush();
    }

    protected Packet readMessage(XMLStreamReader reader) throws XMLStreamException, JMSException {
        ActiveMQTextMessage message = new ActiveMQTextMessage();
        QName name = reader.getName();
        message.setJMSType(name.getLocalPart());
        String temp = reader.getAttributeValue(NAMESPACE, "to");
        if (temp != null && temp.length() > 0) {
            message.setJMSDestination(createDestination(temp));
        }
        temp = reader.getAttributeValue(NAMESPACE, "from");
        if (temp != null && temp.length() > 0) {
            message.setJMSReplyTo(createDestination(temp));
        }

        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case XMLStreamConstants.START_ELEMENT:
                    if (!readElement(reader, message)) {
                        log.warn("Unknown element: " + reader.getName());
                    }
                    break;

                case XMLStreamConstants.END_ELEMENT:
                case XMLStreamConstants.END_DOCUMENT:
                    return message;
            }
        }
        return message;
    }

    protected boolean readElement(XMLStreamReader reader, ActiveMQTextMessage message) throws JMSException, XMLStreamException {
        QName name = reader.getName();
        String localPart = name.getLocalPart();
        if (localPart.equals("body")) {
            message.setText(readXMLAsText(reader));
            return true;
        }
        else if (localPart.equals("thread")) {
            message.setJMSCorrelationID(readXMLAsText(reader));
            return true;
        }
        else {
            return false;
        }
    }

    protected String readXMLAsText(XMLStreamReader reader) throws XMLStreamException {
        StringBuffer buffer = new StringBuffer();
        int elementCount = 0;
        while (reader.hasNext()) {
            switch (reader.nextTag()) {
                case XMLStreamConstants.START_ELEMENT:
                    if (elementCount++ > 0) {
                        writeStartElement(reader);
                    }
                    break;

                case XMLStreamConstants.CHARACTERS:
                    buffer.append(reader.getText());
                    break;

                case XMLStreamConstants.END_ELEMENT:
                    if (--elementCount <= 0) {
                        return buffer.toString();
                    }
                    writeEndElement(reader);
                    break;

                case XMLStreamConstants.END_DOCUMENT:
                    return buffer.toString();
            }
        }
        return buffer.toString();
    }

    protected void writeStartElement(XMLStreamReader reader) {
        writer.print("<");
        writeQName(reader.getName());
        for (int i = 0, size = reader.getNamespaceCount(); i < size; i++) {
            writer.print("xmlns");
            String prefix = reader.getNamespacePrefix(i);
            if (prefix != null && prefix.length() > 0) {
                writer.print(":");
                writer.print(prefix);
            }
            writer.print("='");
            writer.print(reader.getNamespaceURI(i));
            writer.print("'");
        }
        for (int i = 0, size = reader.getAttributeCount(); i < size; i++) {
            writer.print("xmlns");
            writeQName(reader.getAttributeName(i));
            writer.print("='");
            writer.print(reader.getAttributeValue(i));
            writer.print("'");
        }
        writer.println(">");
    }

    protected void writeEndElement(XMLStreamReader reader) {
        writer.print("</");
        writeQName(reader.getName());
        writer.println(">");
    }

    protected void writeQName(QName name) {
        String prefix = name.getPrefix();
        if (prefix != null && prefix.length() > 0) {
            writer.print(prefix);
            writer.print(":");
        }
        writer.print(name.getLocalPart());
    }

    protected Destination createDestination(String text) {
        int type = ActiveMQDestination.ACTIVEMQ_TOPIC;
        if (text.startsWith(QUEUE_PREFIX)) {
            type = ActiveMQDestination.ACTIVEMQ_QUEUE;
            text = text.substring(QUEUE_PREFIX.length());
        }
        if (text.startsWith(TEMP_QUEUE_PREFIX)) {
            type = ActiveMQDestination.ACTIVEMQ_TEMPORARY_QUEUE;
            text = text.substring(TEMP_QUEUE_PREFIX.length());
        }
        if (text.startsWith(TEMP_TOPIC_PREFIX)) {
            type = ActiveMQDestination.ACTIVEMQ_TEMPORARY_TOPIC;
            text = text.substring(TEMP_TOPIC_PREFIX.length());
        }
        text = text.trim();
        if (text.length() == 0) {
            return null;
        }
        return ActiveMQDestination.createDestination(type, text);
    }

    protected String toString(Destination destination) {
        if (destination instanceof ActiveMQDestination) {
            ActiveMQDestination activeDestination = (ActiveMQDestination) destination;
            String physicalName = activeDestination.getPhysicalName();
            switch (activeDestination.getDestinationType()) {
                case ActiveMQDestination.ACTIVEMQ_QUEUE:
                    return QUEUE_PREFIX + physicalName;

                case ActiveMQDestination.ACTIVEMQ_TEMPORARY_QUEUE:
                    return TEMP_QUEUE_PREFIX + physicalName;

                case ActiveMQDestination.ACTIVEMQ_TEMPORARY_TOPIC:
                    return TEMP_TOPIC_PREFIX + physicalName;
            }
            return physicalName;
        }
        return destination != null ? destination.toString() : "";
    }

    protected void writeObjectMessage(ActiveMQObjectMessage message, DataOutput out) throws JMSException, IOException {
        Serializable object = message.getObject();
        String text = (object != null) ? object.toString() : "";
        writeMessage(message, text, out);
    }

    protected void writeTextMessage(ActiveMQTextMessage message, DataOutput out) throws JMSException, IOException {
        writeMessage(message, message.getText(), out);
    }

    protected void writeBytesMessage(ActiveMQBytesMessage message, DataOutput out) throws IOException {
        ByteArray data = message.getBodyAsBytes();
        String text = encodeBinary(data.getBuf(), data.getOffset(), data.getLength());
        writeMessage(message, text, out);
    }

    protected void writeMessage(ActiveMQMessage message, String body, DataOutput out) throws IOException {
        String type = getXmppType(message);

        writer.print("<");
        writer.print(type);
        writer.print(" to='");
        writer.print(toString(message.getJMSDestination()));
        writer.print("' from='");
        writer.print(toString(message.getJMSReplyTo()));
        String messageID = message.getJMSMessageID();
        if (messageID != null) {
            writer.print("' id='");
            writer.print(messageID);
        }

        HashMap properties = message.getProperties();
        if (properties != null) {
            for (Iterator iter = properties.entrySet().iterator(); iter.hasNext();) {
                Map.Entry entry = (Map.Entry) iter.next();
                Object key = entry.getKey();
                Object value = entry.getValue();
                if (value != null) {
                    writer.print("' ");
                    writer.print(key.toString());
                    writer.print("='");
                    writer.print(value.toString());
                }
            }
        }

        writer.println("'>");

        String id = message.getJMSCorrelationID();
        if (id != null) {
            writer.print("<thread>");
            writer.print(id);
            writer.print("</thread>");
        }
        writer.print("<body>");
        writer.print(body);
        writer.println("</body>");
        writer.print("</");
        writer.print(type);
        writer.println(">");
    }

    protected String encodeBinary(byte[] data, int offset, int length) {
        // TODO
        throw new RuntimeException("Not implemented yet!");
    }

    protected String getXmppType(ActiveMQMessage message) {
        String type = message.getJMSType();
        if (type == null) {
            type = "message";
        }
        return type;
    }
}
