/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.openjpa.event;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.lib.util.Closeable;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.concurrent.AbstractConcurrentEventManager;
import org.apache.openjpa.util.UserException;

/**
 * Manager that can be used to track and notify
 * {@link RemoteCommitListener}s on remote commit events. If remote events
 * are enabled, this manager should be installed as a transaction listener on
 * all brokers so that it knows when commits are made.
 *
 * @author Patrick Linskey
 * @author Abe White
 * @since 0.3.0
 */
public class RemoteCommitEventManager
    extends AbstractConcurrentEventManager
    implements EndTransactionListener, Closeable {

    
    private static final long serialVersionUID = 1L;

    private static final Localizer _loc = Localizer.forPackage
        (RemoteCommitEventManager.class);

    private final RemoteCommitProvider _provider;
    private boolean _transmitPersIds = false;

    /**
     * Constructor. Supply configuration.
     */
    public RemoteCommitEventManager(OpenJPAConfiguration conf) {
        _provider = conf.newRemoteCommitProviderInstance();
        if (_provider != null) {
            _provider.setRemoteCommitEventManager(this);
        }
    }

    /**
     * Return true if remote events are enabled.
     */
    public boolean areRemoteEventsEnabled() {
        return _provider != null;
    }

    /**
     * Return the {@link RemoteCommitProvider} that this manager uses.
     *
     * @since 0.3.1
     */
    public RemoteCommitProvider getRemoteCommitProvider() {
        return _provider;
    }

    /**
     * Whether the oids of added instances will be transmitted.
     */
    public boolean getTransmitPersistedObjectIds() {
        return _transmitPersIds;
    }

    /**
     * Whether the oids of added instances will be transmitted.
     */
    public void setTransmitPersistedObjectIds(boolean transmit) {
        _transmitPersIds = transmit;
    }

    /**
     * Adds an OpenJPA-internal listener to this RemoteCommitEventManager.
     * Listeners so registered will be fired before any that are registered
     * via {@link #addListener}. This means that the external listeners can
     * rely on internal caches and data structures being up-to-date by the
     * time that they are invoked.
     *
     * @since 1.0.0
     */
    public void addInternalListener(RemoteCommitListener listen) {
        if (_provider == null)
            throw new UserException(_loc.get("no-provider"));
        ((List) _listeners).add(0, listen);
    }

    public void addListener(RemoteCommitListener listen) {
        if (_provider == null)
            throw new UserException(_loc.get("no-provider"));
        super.addListener(listen);
    }

    /**
     * Close this manager and all registered listeners.
     */
    @Override
    public void close() {
        if (_provider != null) {
            _provider.close();
            Collection listeners = getListeners();
            for (Object listener : listeners) {
                ((RemoteCommitListener) listener).close();
            }
        }
    }

    @Override
    protected void fireEvent(Object event, Object listener) {
        RemoteCommitListener listen = (RemoteCommitListener) listener;
        RemoteCommitEvent ev = (RemoteCommitEvent) event;
        listen.afterCommit(ev);
    }

    /**
     * Fire an event to local listeners only notifying them of a detected
     * stale record.
     *
     * @since 1.0.0
     */
    public void fireLocalStaleNotification(Object oid) {
        RemoteCommitEvent ev = new RemoteCommitEvent(
            RemoteCommitEvent.PAYLOAD_LOCAL_STALE_DETECTION,
            null, null, Collections.singleton(oid), null);
        fireEvent(ev);
    }

    //////////////////////////////////////
    // TransactionListener implementation
    //////////////////////////////////////

    @Override
    public void afterCommit(TransactionEvent event) {
        if (_provider != null) {
            RemoteCommitEvent rce = createRemoteCommitEvent(event);
            if (rce != null)
                _provider.broadcast(rce);
        }
    }

    /**
     * Create a remote commit event from the given transaction event.
     */
    private RemoteCommitEvent createRemoteCommitEvent(TransactionEvent event) {
        Broker broker = (Broker) event.getSource();
        int payload;
        Collection persIds = null;
        Collection addClassNames = null;
        Collection updates = null;
        Collection deletes = null;

        if (broker.isTrackChangesByType()) {
            payload = RemoteCommitEvent.PAYLOAD_EXTENTS;
            addClassNames = toClassNames(event.getPersistedTypes());
            updates = toClassNames(event.getUpdatedTypes());
            deletes = toClassNames(event.getDeletedTypes());
            if (addClassNames == null && updates == null && deletes == null)
                return null;
        } else {
            Collection trans = event.getTransactionalObjects();
            if (trans.isEmpty())
                return null;

            payload = (_transmitPersIds)
                ? RemoteCommitEvent.PAYLOAD_OIDS_WITH_ADDS
                : RemoteCommitEvent.PAYLOAD_OIDS;
            Object oid;
            Object obj;
            OpenJPAStateManager sm;
            for (Object tran : trans) {
                obj = tran;
                sm = broker.getStateManager(obj);

                if (sm == null || !sm.isPersistent() || !sm.isDirty())
                    continue;
                if (sm.isNew() && sm.isDeleted())
                    continue;

                oid = sm.fetchObjectId();
                if (sm.isNew()) {
                    if (_transmitPersIds) {
                        if (persIds == null)
                            persIds = new ArrayList();
                        persIds.add(oid);
                    }
                    if (addClassNames == null)
                        addClassNames = new HashSet();
                    addClassNames.add(obj.getClass().getName());
                }
                else if (sm.isDeleted()) {
                    if (deletes == null)
                        deletes = new ArrayList();
                    deletes.add(oid);
                }
                else {
                    if (updates == null)
                        updates = new ArrayList();
                    updates.add(oid);
                }
            }
            if (addClassNames == null && updates == null && deletes == null)
                return null;
        }
        return new RemoteCommitEvent(payload, persIds, addClassNames, updates,
            deletes);
    }

    /**
     * Transform a collection of classes to class names.
     */
    private static Collection toClassNames(Collection clss) {
        if (clss.isEmpty())
            return null;

        List names = new ArrayList(clss);
        for (int i = 0; i < names.size(); i++)
            names.set(i, ((Class) names.get(i)).getName());
        return names;
    }

    @Override
    public void beforeCommit(TransactionEvent event) {
    }

    @Override
    public void afterRollback(TransactionEvent event) {
    }

    @Override
    public void afterCommitComplete(TransactionEvent event) {
    }

    @Override
    public void afterRollbackComplete(TransactionEvent event) {
    }

    @Override
    public void afterStateTransitions(TransactionEvent event)
	{
	}
}
