/*
 * Copyright (C) 2007-2009 Jive Software. All rights reserved.
 *
 * 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.jivesoftware.openfire.plugin.session;

import org.dom4j.Element;
import org.dom4j.tree.DefaultElement;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.session.ComponentSession;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.cache.ClusterTask;
import org.jivesoftware.util.cache.ExternalizableUtil;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.packet.*;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collection;

/**
 * Surrogate for sessions of external components hosted in some remote cluster node.
 *
 * @author Gaston Dombiak
 */
public class RemoteComponentSession extends RemoteSession implements ComponentSession {

    private ExternalComponent component;

    public RemoteComponentSession(byte[] nodeID, JID address) {
        super(nodeID, address);
        component = new RemoteExternalComponent(address);
    }

    public ExternalComponent getExternalComponent() {
        return component;
    }

    RemoteSessionTask getRemoteSessionTask(RemoteSessionTask.Operation operation) {
        return new ComponentSessionTask(address, operation);
    }

    ClusterTask getDeliverRawTextTask(String text) {
        return new DeliverRawTextTask(this, address, text);
    }

    ClusterTask getProcessPacketTask(Packet packet) {
        return new ProcessPacketTask(this, address, packet);
    }

    private class RemoteExternalComponent implements ExternalComponent {
        private JID address;

        public RemoteExternalComponent(JID address) {
            this.address = address;
        }

        public void setName(String name) {
            RemoteSessionTask task = new SetterTask(address, SetterTask.Type.name, name);
            doClusterTask(task);
        }

        public String getType() {
            ClusterTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.getType);
            return (String) doSynchronousClusterTask(task);
        }

        public void setType(String type) {
            RemoteSessionTask task = new SetterTask(address, SetterTask.Type.type, type);
            doClusterTask(task);
        }

        public String getCategory() {
            ClusterTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.getCategory);
            return (String) doSynchronousClusterTask(task);
        }

        public void setCategory(String category) {
            RemoteSessionTask task = new SetterTask(address, SetterTask.Type.catergory, category);
            doClusterTask(task);
        }

        public String getInitialSubdomain() {
            ClusterTask task =
                    new ComponentSessionTask(address, RemoteSessionTask.Operation.getInitialSubdomain);
            return (String) doSynchronousClusterTask(task);
        }

        public Collection<String> getSubdomains() {
            ClusterTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.getSubdomains);
            return (Collection<String>) doSynchronousClusterTask(task);
        }

        public String getName() {
            ClusterTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.getName);
            return (String) doSynchronousClusterTask(task);
        }

        public String getDescription() {
            ClusterTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.getDescription);
            return (String) doSynchronousClusterTask(task);
        }

        public void processPacket(Packet packet) {
            RemoteSessionTask task = new ProcessComponentPacketTask(address, packet);
            doClusterTask(task);
        }

        public void initialize(JID jid, ComponentManager componentManager) throws ComponentException {
            RemoteSessionTask task = new InitializeTask(address, jid);
            doClusterTask(task);
        }

        public void start() {
            RemoteSessionTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.start);
            doClusterTask(task);
        }

        public void shutdown() {
            RemoteSessionTask task = new ComponentSessionTask(address, RemoteSessionTask.Operation.shutdown);
            doClusterTask(task);
        }
    }

    private static class SetterTask extends ComponentSessionTask {
        private Type type;
        private String value;

        public SetterTask() {
            super();
        }

        protected SetterTask(JID address, Type type, String value) {
            super(address, null);
            this.type = type;
            this.value = value;
        }

        public void run() {
            if (type == Type.name) {
                ((ComponentSession) getSession()).getExternalComponent().setName(value);
            } else if (type == Type.type) {
                ((ComponentSession) getSession()).getExternalComponent().setType(value);
            } else if (type == Type.catergory) {
                ((ComponentSession) getSession()).getExternalComponent().setCategory(value);
            }
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal(out);
            ExternalizableUtil.getInstance().writeInt(out, type.ordinal());
            ExternalizableUtil.getInstance().writeBoolean(out, value != null);
            if (value != null) {
                ExternalizableUtil.getInstance().writeSafeUTF(out, value);
            }
        }

        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            super.readExternal(in);
            type = Type.values()[ExternalizableUtil.getInstance().readInt(in)];
            if (ExternalizableUtil.getInstance().readBoolean(in)) {
                value = ExternalizableUtil.getInstance().readSafeUTF(in);
            }
        }

        private static enum Type {
            name,
            type,
            catergory
        }
    }

    private static class ProcessComponentPacketTask extends ComponentSessionTask {
        private Packet packet;

        public ProcessComponentPacketTask() {
            super();
        }

        protected ProcessComponentPacketTask(JID address, Packet packet) {
            super(address, null);
            this.packet = packet;
        }

        public void run() {
            ((ComponentSession) getSession()).getExternalComponent().processPacket(packet);
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal(out);
            if (packet instanceof IQ) {
                ExternalizableUtil.getInstance().writeInt(out, 1);
            } else if (packet instanceof Message) {
                ExternalizableUtil.getInstance().writeInt(out, 2);
            } else if (packet instanceof Presence) {
                ExternalizableUtil.getInstance().writeInt(out, 3);
            }
            ExternalizableUtil.getInstance().writeSerializable(out, (DefaultElement) packet.getElement());
        }

        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            super.readExternal(in);
            int packetType = ExternalizableUtil.getInstance().readInt(in);
            Element packetElement = (Element) ExternalizableUtil.getInstance().readSerializable(in);
            switch (packetType) {
                case 1:
                    packet = new IQ(packetElement, true);
                    break;
                case 2:
                    packet = new Message(packetElement, true);
                    break;
                case 3:
                    packet = new Presence(packetElement, true);
                    break;
            }
        }
    }

    private static class InitializeTask extends ComponentSessionTask {
        private JID componentJID;

        public InitializeTask() {
            super();
        }

        protected InitializeTask(JID address, JID componentJID) {
            super(address, null);
            this.componentJID = componentJID;
        }

        public void run() {
            try {
                ((ComponentSession) getSession()).getExternalComponent()
                        .initialize(componentJID, InternalComponentManager.getInstance());
            } catch (ComponentException e) {
                Log.error("Error initializing component", e);
            }
        }

        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal(out);
            ExternalizableUtil.getInstance().writeSerializable(out, componentJID);
        }

        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            super.readExternal(in);
            componentJID = (JID) ExternalizableUtil.getInstance().readSerializable(in);
        }
    }
}
