package org.jacorb.notification;

/*
 *        JacORB - a free Java ORB
 *
 *   Copyright (C) 1997-2014 Gerald Brose / The JacORB Team.
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Library General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import org.jacorb.config.Configuration;
import org.jacorb.config.ConfigurationException;
import org.jacorb.notification.conf.Attributes;
import org.jacorb.notification.container.BiDirGiopPOAComponentAdapter;
import org.jacorb.notification.container.PicoContainerFactory;
import org.jacorb.notification.interfaces.Disposable;
import org.jacorb.notification.lifecycle.ManageableServant;
import org.jacorb.notification.util.AdminPropertySet;
import org.jacorb.notification.util.DisposableManager;
import org.jacorb.notification.util.PropertySet;
import org.jacorb.notification.util.QoSPropertySet;
import org.jacorb.orb.util.CorbaLoc;
import org.omg.CORBA.Any;
import org.omg.CORBA.IntHolder;
import org.omg.CORBA.ORB;
import org.omg.CORBA.UserException;
import org.omg.CosNaming.NameComponent;
import org.omg.CosNaming.NamingContext;
import org.omg.CosNaming.NamingContextHelper;
import org.omg.CosNaming.NamingContextPackage.CannotProceed;
import org.omg.CosNaming.NamingContextPackage.InvalidName;
import org.omg.CosNaming.NamingContextPackage.NotFound;
import org.omg.CosNotification.BestEffort;
import org.omg.CosNotification.ConnectionReliability;
import org.omg.CosNotification.EventReliability;
import org.omg.CosNotification.Persistent;
import org.omg.CosNotification.Property;
import org.omg.CosNotification.PropertyError;
import org.omg.CosNotification.PropertyRange;
import org.omg.CosNotification.QoSError_code;
import org.omg.CosNotification.UnsupportedAdmin;
import org.omg.CosNotification.UnsupportedQoS;
import org.omg.CosNotifyChannelAdmin.ChannelNotFound;
import org.omg.PortableServer.IdAssignmentPolicy;
import org.omg.PortableServer.IdAssignmentPolicyValue;
import org.omg.PortableServer.POA;
import org.omg.PortableServer.POAHelper;
import org.omg.PortableServer.Servant;
import org.picocontainer.MutablePicoContainer;
import org.slf4j.Logger;

/**
 * @author Alphonse Bendt
 */

public abstract class AbstractChannelFactory implements ManageableServant, Disposable
{
    interface ShutdownCallback
    {
        void needTime(int time);

        void shutdownComplete();
    }

    // //////////////////////////////////////

    private static final String STANDARD_IMPL_NAME = "JacORB-NotificationService";

    private static final long SHUTDOWN_INTERVAL = 1000;

    private static final String EVENTCHANNEL_FACTORY_POA_NAME = "EventChannelFactoryPOA";

    // //////////////////////////////////////

    private NameComponent[] registeredName_ = null;

    private NamingContext namingContext_;

    /**
     * the method that is executed when destroy is invoked.
     */
    private Runnable destroyMethod_ = new Runnable()
    {
        @Override
        public void run()
        {
            dispose();
        }
    };

    // ///////

    protected final MutablePicoContainer container_;

    protected final Configuration config_;

    protected final org.omg.CORBA.Object thisRef_;

    protected final Logger logger_;

    private final String ior_;

    private final String corbaLoc_;

    private final POA eventChannelFactoryPOA_;

    private final byte[] oid_;

    private final ChannelManager channelManager_ = new ChannelManager();

    private final AtomicInteger eventChannelIDPool_ = new AtomicInteger(0);

    private final DisposableManager disposableManager_ = new DisposableManager();

    // //////////////////////////////////////

    protected AbstractChannelFactory(final MutablePicoContainer container, final ORB orb)
            throws UserException
    {
        container_ = PicoContainerFactory.createRootContainer(container, (org.jacorb.orb.ORB) orb);

        if (container != null)
        {
            disposableManager_.addDisposable(new Disposable()
            {
                @Override
                public void dispose()
                {
                    container.removeChildContainer(container_);
                }
            });
        }

        disposableManager_.addDisposable(new Disposable() {
            @Override
            public void dispose()
            {
                final POA _poa = (POA) container_.getComponentInstanceOfType(POA.class);

                _poa.destroy(true, false);
            }
        });

        config_ = (Configuration) container_.getComponentInstanceOfType(Configuration.class);

        logger_ = config_.getLogger(getClass().getName());

        POA _rootPOA = (POA) container_.getComponentInstanceOfType(POA.class);

        List<IdAssignmentPolicy> _ps = new ArrayList<IdAssignmentPolicy>();

        _ps.add(_rootPOA.create_id_assignment_policy(IdAssignmentPolicyValue.USER_ID));

        BiDirGiopPOAComponentAdapter.addBiDirGiopPolicy(_ps, orb, config_);

        org.omg.CORBA.Policy[] _policies = _ps
                .toArray(new org.omg.CORBA.Policy[_ps.size()]);

        eventChannelFactoryPOA_ = _rootPOA.create_POA(EVENTCHANNEL_FACTORY_POA_NAME, _rootPOA
                .the_POAManager(), _policies);

        for (int x = 0; x < _policies.length; ++x)
        {
            _policies[x].destroy();
        }

        oid_ = (getObjectName().getBytes());

        eventChannelFactoryPOA_.activate_object_with_id(oid_, getServant());

        thisRef_ = eventChannelFactoryPOA_.id_to_reference(oid_);

        if (logger_.isDebugEnabled())
        {
            logger_.debug("activated EventChannelFactory with OID '" + new String(oid_) + "' on '"
                    + eventChannelFactoryPOA_.the_name() + "'");
        }

        ior_ = orb.object_to_string(eventChannelFactoryPOA_.id_to_reference(oid_));

        corbaLoc_ = "corbaloc:" + CorbaLoc.generateCorbaloc(orb,eventChannelFactoryPOA_.id_to_servant(oid_)._this_object());

        ((org.jacorb.orb.ORB) orb).addObjectKey(getShortcut(), ior_);
    }

    // //////////////////////////////////////

    protected abstract AbstractEventChannel newEventChannel() throws ConfigurationException;

    protected abstract org.omg.CORBA.Object create_abstract_channel(Property[] admin,
            Property[] qos, IntHolder id) throws UnsupportedAdmin, UnsupportedQoS;

    protected abstract String getObjectName();

    protected abstract String getShortcut();

    protected abstract Servant getServant();

    // //////////////////////////////////////

    @Override
    public synchronized org.omg.CORBA.Object activate()
    {
        return thisRef_;
    }

    public void setDestroyMethod(Runnable destroyMethod)
    {
        destroyMethod_ = destroyMethod;
    }

    protected ORB getORB()
    {
        return (ORB) container_.getComponentInstance(ORB.class);
    }

    @Override
    public final void deactivate()
    {
        try
        {
            eventChannelFactoryPOA_.deactivate_object(oid_);
        } catch (Exception e)
        {
            logger_.error("unable to deactivate object", e);

            throw new RuntimeException();
        }
    }

    protected Configuration getConfiguration()
    {
        return config_;
    }

    @Override
    public void dispose()
    {
        try
        {
            unregisterName();
        } catch (Exception e)
        {
            logger_.error("unable to unregister NameService registration", e);
        }

        deactivate();

        channelManager_.dispose();

        container_.dispose();

        disposableManager_.dispose();
    }

    protected void addToChannels(int id, AbstractEventChannel channel)
    {
        channelManager_.add_channel(id, channel);
    }

    protected int[] getAllChannels()
    {
        return channelManager_.get_all_channels();
    }

    protected AbstractEventChannel get_event_channel_servant(int id) throws ChannelNotFound
    {
        return channelManager_.get_channel_servant(id);
    }

    protected Iterator getChannelIterator()
    {
        return channelManager_.getChannelIterator();
    }

    protected AbstractEventChannel create_channel_servant(IntHolder id, Property[] qosProps,
            Property[] adminProps) throws UnsupportedAdmin, UnsupportedQoS, ConfigurationException
    {
        // check QoS and Admin Settings

        AdminPropertySet _adminSettings = new AdminPropertySet(config_);

        _adminSettings.set_admin(adminProps);

        QoSPropertySet _qosSettings = new QoSPropertySet(config_, QoSPropertySet.CHANNEL_QOS);

        _qosSettings.set_qos(qosProps);

        if (logger_.isDebugEnabled())
        {
            logger_.debug("uniqueQoSProps: " + _qosSettings);
            logger_.debug("uniqueAdminProps: " + _adminSettings);
        }

        checkQoSSettings(_qosSettings);

        AbstractEventChannel _eventChannelServant = newEventChannel();

        id.value = _eventChannelServant.getID();

        _eventChannelServant.set_qos(_qosSettings.toArray());
        _eventChannelServant.set_admin(_adminSettings.toArray());

        channelCreated(_eventChannelServant);

        if (logger_.isDebugEnabled())
        {
            logger_.debug("created channel_servant id=" + id.value);
        }

        return _eventChannelServant;
    }

    protected void channelCreated(AbstractEventChannel channel)
    {
        // empty
    }

    private int createChannelIdentifier()
    {
        return eventChannelIDPool_.getAndIncrement();
    }

    private void checkQoSSettings(PropertySet uniqueQoSProperties) throws UnsupportedQoS
    {
        if (uniqueQoSProperties.containsKey(EventReliability.value))
        {
            short _eventReliabilty = uniqueQoSProperties.get(EventReliability.value)
                    .extract_short();

            switch (_eventReliabilty) {
            case BestEffort.value:
                logger_.info("EventReliability=BestEffort");
                break;

            case Persistent.value:
                throwPersistentNotSupported(EventReliability.value);

            // fallthrough
            default:
                throwBadValue(EventReliability.value);
            }
        }

        short _connectionReliability = BestEffort.value;

        if (uniqueQoSProperties.containsKey(ConnectionReliability.value))
        {
            _connectionReliability = uniqueQoSProperties.get(ConnectionReliability.value)
                    .extract_short();

            switch (_connectionReliability) {
            case BestEffort.value:
                logger_.info("ConnectionReliability=BestEffort");
                break;

            case Persistent.value:
                throwPersistentNotSupported(ConnectionReliability.value);

                break; // to satisfy compiler
            default:
                throwBadValue(ConnectionReliability.value);
            }
        }
    }

    private void throwPersistentNotSupported(String property) throws UnsupportedQoS
    {
        Any _lowVal = getORB().create_any();
        Any _highVal = getORB().create_any();

        _lowVal.insert_short(BestEffort.value);
        _highVal.insert_short(BestEffort.value);

        UnsupportedQoS _e = new UnsupportedQoS(new PropertyError[] { new PropertyError(
                QoSError_code.UNSUPPORTED_VALUE, property, new PropertyRange(_lowVal, _highVal)) });

        throw _e;
    }

    private void throwBadValue(String property) throws UnsupportedQoS
    {
        Any _lowVal = getORB().create_any();
        Any _highVal = getORB().create_any();

        _lowVal.insert_short(BestEffort.value);
        _highVal.insert_short(BestEffort.value);

        UnsupportedQoS _e = new UnsupportedQoS("The specified Property Value is not supported",
                new PropertyError[] { new PropertyError(QoSError_code.BAD_VALUE, property,
                        new PropertyRange(_lowVal, _highVal)) });
        throw _e;
    }

    public void destroy()
    {
        // start extra thread to
        // shut down the Notification Service.
        // otherwise ORB.shutdown() would be called inside
        // a remote invocation which causes an exception.
        final Thread _shutdown = new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    logger_.info("Notification Service is going down in " + SHUTDOWN_INTERVAL
                            + " ms");

                    Thread.sleep(SHUTDOWN_INTERVAL);
                } catch (InterruptedException e)
                {
                    // ignore
                }

                destroyMethod_.run();
            }
        };

        _shutdown.start();
    }

    /**
     * shutdown is called by the Java Wrapper
     */
    public void shutdown(ShutdownCallback cb)
    {
        // estimate shutdown time.
        // during shutdown disconnect must be called on every
        // connected client. in worst case the client is not
        // acccessible anymore and disconnect raises TRANSIENT. as
        // this could take some time request some more time from the
        // WrapperManager who is initiating the shutdown.

        int _numberOfClients = 0;

        Iterator i = getChannelIterator();

        while (i.hasNext())
        {
            AbstractEventChannel _channel = (AbstractEventChannel) ((Map.Entry) i.next())
                    .getValue();

            _numberOfClients += _channel.getNumberOfConnectedClients();
        }

        // TODO fetch this from somewhere?
        int _connectionTimeout = 4000;

        int _estimatedShutdowntime = 2000 + _numberOfClients * _connectionTimeout;

        if (logger_.isInfoEnabled())
        {
            logger_.info("Connected Clients: " + _numberOfClients);
            logger_.info("Connection Timeout: " + _connectionTimeout + " ms");
            logger_.info("Estimated Shutdowntime: " + _estimatedShutdowntime + " ms");
        }

        // estimate 4000ms shutdowntime per channel
        cb.needTime(_estimatedShutdowntime);

        logger_.info("NotificationService is going down");

        destroyMethod_.run();

        logger_.info("NotificationService down");

        cb.shutdownComplete();
    }

    public String getIOR()
    {
        return ior_;
    }

    public String getCorbaLoc()
    {
        return corbaLoc_;
    }

    private static AbstractChannelFactory newChannelFactory(MutablePicoContainer container,
            ORB orb, boolean typed) throws UserException
    {
        if (typed)
        {
            return new TypedEventChannelFactoryImpl(container, orb);
        }

        return new EventChannelFactoryImpl(container, orb);
    }

    private static AbstractChannelFactory newFactory(MutablePicoContainer container, final ORB orb,
            boolean startThread, Properties props) throws Exception
    {
        AbstractChannelFactory _factory = newChannelFactory(container, orb, "on".equals(props
                .get(Attributes.ENABLE_TYPED_CHANNEL)));

        // force activation
        _factory.activate();

        _factory.printIOR(props);

        _factory.printCorbaLoc(props);

        _factory.writeIOR(props);

        _factory.registerName(props);

        _factory.startChannels(props);

        if (startThread)
        {
            POA _poa = POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
            _poa.the_POAManager().activate();

            Thread _orbThread = new Thread(new Runnable()
            {
                @Override
                public void run()
                {
                    orb.run();
                }
            });

            _orbThread.setName("Notification ORB Runner Thread");

            _orbThread.setDaemon(false);

            _orbThread.start();

            _factory.disposableManager_.addDisposable(new Disposable() {
                @Override
                public void dispose() {
                    orb.shutdown(false);
                }
            });
        }

        return _factory;
    }

    public static AbstractChannelFactory newFactory(ORB optionalORB, MutablePicoContainer optionalContainer, Properties props)
            throws Exception
    {
        props.put("jacorb.implname", STANDARD_IMPL_NAME);

        final ORB _orb;
        if (optionalORB != null)
        {
            _orb = optionalORB;
        }
        else
        {
            _orb = ORB.init(new String[] {}, props);
        }

        AbstractChannelFactory factory = newFactory(optionalContainer, _orb, (optionalORB == null), props);

        return factory;
    }

    public static AbstractChannelFactory newFactory(Properties props) throws Exception
    {
        return newFactory(null, null, props);
    }

    public void registerName(Properties props) throws Exception
    {
        registerName(props.getProperty(Attributes.REGISTER_NAME_ID, null), props.getProperty(
                Attributes.REGISTER_NAME_KIND, ""));
    }

    public synchronized void registerName(String nameId, String nameKind) throws NotFound,
            CannotProceed, InvalidName, org.omg.CORBA.ORBPackage.InvalidName
    {
        if (nameId == null)
        {
            return;
        }

        namingContext_ = NamingContextHelper.narrow(getORB().resolve_initial_references("NameService"));

        NameComponent[] _name = new NameComponent[] { new NameComponent(nameId, nameKind) };

        if (logger_.isInfoEnabled())
        {
            logger_.info("namingContext.rebind(" + format(_name) + " => " + getCorbaLoc() + ")");
        }

        namingContext_.rebind(_name, thisRef_);

        registeredName_ = _name;
    }

    public synchronized void unregisterName() throws NotFound, CannotProceed, InvalidName
    {
        if (namingContext_ != null)
        {
            if (registeredName_ != null)
            {
                if (logger_.isInfoEnabled())
                {
                    logger_.info("namingContext.unbind(" + format(registeredName_) + ")");
                }

                namingContext_.unbind(registeredName_);

                registeredName_ = null;
            }
        }
    }

    private static String format(NameComponent[] name)
    {
        StringBuffer b = new StringBuffer();

        for (int i = 0; i < name.length; ++i)
        {
            if (i != 0)
            {
                b.append('/');
            }

            format(name[i], b);
        }

        return b.toString();
    }

    private static void format(NameComponent name, StringBuffer b)
    {
        b.append(name.id);

        if (name.kind != null)
        {
            b.append('.');
            b.append(name.kind);
        }
    }

    private void startChannels(Properties props) throws UnsupportedQoS, UnsupportedAdmin
    {
        if (props.containsKey(Attributes.START_CHANNELS))
        {
            startChannels(Integer.parseInt((String) props.get(Attributes.START_CHANNELS)));
        }
    }

    private void startChannels(int channels) throws UnsupportedQoS, UnsupportedAdmin
    {
        for (int i = 0; i < channels; i++)
        {
            IntHolder ih = new IntHolder();
            create_abstract_channel(new Property[0], new Property[0], ih);
        }
    }

    private void printIOR(Properties props)
    {
        if ("on".equals(props.get(Attributes.PRINT_IOR)))
        {
            System.out.println(getIOR());
        }
    }

    private void printCorbaLoc(Properties props)
    {
        if ("on".equals(props.get(Attributes.PRINT_CORBALOC)))
        {
            System.out.println(getCorbaLoc());
        }
    }

    private void writeIOR(Properties props) throws IOException
    {
        String _iorFileName = (String) props.get(Attributes.IOR_FILE);

        if (_iorFileName != null)
        {
            writeIOR(_iorFileName);
        }
    }

    public void writeIOR(String fileName) throws IOException
    {
        FileWriter out = new FileWriter(fileName);

        try
        {
            writeIOR(out);
            out.flush();
        } finally
        {
            out.close();
        }
    }

    private void writeIOR(Writer out)
    {
        PrintWriter writer = new PrintWriter(out);
        writer.println(getIOR());
    }

    public POA _default_POA()
    {
        return eventChannelFactoryPOA_;
    }

    protected MutablePicoContainer newContainerForChannel()
    {
        final MutablePicoContainer _channelContainer = PicoContainerFactory
                .createChildContainer(container_);

        // create identifier
        final int _channelID = createChannelIdentifier();
        IFactory _factory = new IFactory()
        {
            @Override
            public MutablePicoContainer getContainer()
            {
                return _channelContainer;
            }

            @Override
            public int getChannelID()
            {
                return _channelID;
            }

            @Override
            public void destroy()
            {
                container_.removeChildContainer(_channelContainer);
            }
        };

        _channelContainer.registerComponentInstance(IFactory.class, _factory);
        return _channelContainer;
    }
}