/*
 * 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.kernel;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.openjpa.audit.Auditable;
import org.apache.openjpa.audit.AuditableOperation;
import org.apache.openjpa.audit.Auditor;
import org.apache.openjpa.enhance.PCRegistry;
import org.apache.openjpa.enhance.PCRegistry.RegisterClassListener;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.enhance.StateManager;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.event.LifecycleListener;
import org.apache.openjpa.event.TransactionEvent;
import org.apache.openjpa.event.TransactionListener;

/**
 * Controller for audit facility.
 * This controller performs the following basic duties:
 * <LI> Records auditable types at class loading time
 * <LI> Listens to instance life cycle changes and transaction.
 * <LI> Collects auditable instances on instance life cycle changes.
 * <LI> Delegates real auditing to the {@link Auditor} at transaction boundary.
 *
 * @author Pinaki Poddar
 *
 */
public class AuditManager extends InMemorySavepointManager
	implements TransactionListener, RegisterClassListener {

	private final Auditor _auditor;
	private final Set<Class<?>> _allTypes;
	private final Set<Class<?>> _newTypes;
	private final Set<Class<?>> _updateTypes;
	private final Set<Class<?>> _deleteTypes;
	private final Map<Broker, AuditCallback> _saved;
	private final ReentrantLock _lock = new ReentrantLock();

	public AuditManager(Auditor auditor) {
		super();
		if (auditor == null) {
			throw new NullPointerException("null auditor");
		}

		setPreFlush(false);
		_auditor = auditor;
		_allTypes = new HashSet<>();
		_newTypes = new HashSet<>();
		_updateTypes = new HashSet<>();
		_deleteTypes = new HashSet<>();
		_saved = new ConcurrentHashMap<>();
		PCRegistry.addRegisterClassListener(this);
	}

	/**
	 * Records all auditable classes in operation-specific sets.
	 */
	@Override
	public void register(Class<?> cls) {
		Auditable auditable = cls.getAnnotation(Auditable.class);
		if (auditable == null) {
			return;
		}

		List<AuditableOperation> events = Arrays.asList(auditable.values());
		if (events.contains(AuditableOperation.ALL) || events.contains(AuditableOperation.CREATE)) {
			_newTypes.add(cls);
			_allTypes.add(cls);
		}
		if (events.contains(AuditableOperation.ALL) || events.contains(AuditableOperation.UPDATE)) {
			_updateTypes.add(cls);
			_allTypes.add(cls);
		}
		if (events.contains(AuditableOperation.ALL) || events.contains(AuditableOperation.DELETE)) {
			_deleteTypes.add(cls);
			_allTypes.add(cls);
		}
	}

	public Auditor getAuditor() {
		return _auditor;
	}

	public Set<Class<?>> getAuditedTypes() {
		return Collections.unmodifiableSet(_allTypes);
	}

	/**
	 * -----------------------------------------------------------------------
	 *                 Transaction callbacks.
	 * -----------------------------------------------------------------------
	 */

	@Override
	public void afterBegin(TransactionEvent event) {
		_lock.lock();
		try {
			Broker broker = (Broker)event.getSource();
			AuditCallback cb = new AuditCallback(broker);
			broker.addLifecycleListener(cb, _allTypes.toArray(new Class<?>[_allTypes.size()]));
			_saved.put(broker, cb);
		} finally {
			_lock.unlock();
		}
	}

	@Override
	public void beforeCommit(TransactionEvent event) {
		_lock.lock();
		try {
			AuditCallback cb = _saved.get(event.getSource());
			if (cb != null) {
				cb.audit();
			}
		} finally {
			_lock.unlock();
		}
	}

	private void forget(Broker broker) {
		AuditCallback cb = _saved.remove(broker);
		if (cb != null) {
			broker.removeLifecycleListener(cb);
			cb.clear();
		}
	}

	@Override
	public void afterCommit(TransactionEvent event) {
		forget((Broker)event.getSource());
	}

	@Override
	public void afterRollback(TransactionEvent event) {
		forget((Broker)event.getSource());
	}

	@Override
	public void afterCommitComplete(TransactionEvent event) {
		forget((Broker)event.getSource());
	}

	@Override
	public void afterRollbackComplete(TransactionEvent event) {
		forget((Broker)event.getSource());
	}

	@Override
	public void beforeFlush(TransactionEvent event) {
	}

	@Override
	public void afterFlush(TransactionEvent event) {
	}

	@Override
	public void afterStateTransitions(TransactionEvent event) {
	}


	/**
	 * Support functions.
	 */

	/**
	 * Extracts the persistence capable instance from the source of the given event.
	 * @return null if an instance can not be extracted.
	 */
	protected PersistenceCapable getPersistenceCapable(LifecycleEvent evt) {
		Object source = evt.getSource();
		return source instanceof PersistenceCapable ? (PersistenceCapable)source : null;
	}

	/**
	 * Extracts the broker from the given persistence capable instance.
	 * @param pc a persistence capable instance
	 * @return null if a Broker can notbe extracted
	 */
	protected Broker getBroker(PersistenceCapable pc) {
		if (pc == null) return null;
		Object ctx = pc.pcGetGenericContext();
		return ctx instanceof Broker ? (Broker)ctx : null;
	}

	/**
	 * Gets an implementation.
	 */
	private StateManagerImpl getImpl(Object instance) {
		if (instance instanceof PersistenceCapable) {
			StateManager sm = ((PersistenceCapable)instance).pcGetStateManager();
			if (sm instanceof StateManagerImpl) {
				return (StateManagerImpl)sm;
			} else {
				return null;
			}
		} else {
			return null;
		}
	}

	/**
	 * Affirms if the given state manager is auditable for the given operation.
	 * @param op an auditable operation
	 */
	protected boolean isAuditable(AuditableOperation op, StateManagerImpl sm) {
		if (sm == null)
			return false;
		Class<?> cls  = sm.getMetaData().getDescribedType();
		return (op == AuditableOperation.ALL    && _allTypes.contains(cls)
			 ||	op == AuditableOperation.CREATE && _newTypes.contains(cls))
		     ||(op == AuditableOperation.UPDATE && _updateTypes.contains(cls))
		     ||(op == AuditableOperation.DELETE && _deleteTypes.contains(cls));
	}

	/**
	 * Listens to entity life cycle operations and saves them for auditing.
	 *
	 */
	private class AuditCallback implements LifecycleListener {
		private final Broker _broker;
		private final Map<StateManagerImpl,PersistenceCapable> _audits =
			new ConcurrentHashMap<>();

		AuditCallback(Broker broker) {
			_broker = broker;
		}

		void audit() {
			if (_audits.isEmpty()) return;
			Collection<Audited> news = new HashSet<>();
			Collection<Audited> updates = new HashSet<>();
			Collection<Audited> deletes = new HashSet<>();
			for (Map.Entry<StateManagerImpl,PersistenceCapable> e : _audits.entrySet()) {
				StateManagerImpl sm = e.getKey();
				Audited audited = new Audited(sm, e.getValue());
				if (sm.getPCState().isNew()) {
					news.add(audited);
				} else if (sm.getPCState().isDeleted()) {
					deletes.add(audited);
				} else if (sm.getPCState().isDirty()) {
					updates.add(audited);
				}
			}
			try {
				_auditor.audit(_broker, news, updates, deletes);
			} catch (Exception e) {
				if (_auditor.isRollbackOnError()) {
					throw new RuntimeException("dump", e);
				} else {
					e.printStackTrace();
				}
			}
		}

		/**
		 * Saves the source of the given event for auditing.
		 * @param op an auditable operation
		 * @param event the event
		 */
		protected void save(AuditableOperation op, LifecycleEvent event) {
			StateManagerImpl sm = getImpl(event.getSource());
			if (sm != null && !_audits.containsKey(sm) && isAuditable(op, sm)) {
				Broker broker = sm.getBroker();

				OpenJPASavepoint savepoint = newSavepoint("", broker);
				savepoint.save(Collections.singleton(sm));
				Map<StateManagerImpl, SavepointFieldManager> states = savepoint.getStates();
                Map.Entry<StateManagerImpl, SavepointFieldManager> e = states.entrySet().iterator().next();
				PersistenceCapable copy = e.getValue().getCopy();
				copy.pcReplaceStateManager(null);
				_audits.put(sm, copy);
			}
		}

		void clear() {
			_audits.clear();
		}

		/**
		 * Life-cycle callbacks
		 */
		@Override
		public void afterLoad(LifecycleEvent event) {
			save(AuditableOperation.ALL, event);
		}

		@Override
		public void afterPersist(LifecycleEvent event) {
			save(AuditableOperation.CREATE, event);
		}

		@Override
		public void beforeDelete(LifecycleEvent event) {
			save(AuditableOperation.DELETE, event);
		}

		@Override
		public void beforeDirty(LifecycleEvent event) {
			save(AuditableOperation.UPDATE, event);
		}

		@Override
		public void beforePersist(LifecycleEvent event) {
		}

		@Override
		public void afterRefresh(LifecycleEvent event) {
		}

		@Override
		public void beforeStore(LifecycleEvent event) {
			save(AuditableOperation.UPDATE, event);
		}

		@Override
		public void afterStore(LifecycleEvent event) {
		}

		@Override
		public void beforeClear(LifecycleEvent event) {
		}

		@Override
		public void afterClear(LifecycleEvent event) {
		}

		@Override
		public void afterDelete(LifecycleEvent event) {
		}

		@Override
		public void afterDirty(LifecycleEvent event) {
			save(AuditableOperation.UPDATE, event);
		}

		@Override
		public void beforeDirtyFlushed(LifecycleEvent event) {
		}

		@Override
		public void afterDirtyFlushed(LifecycleEvent event) {
		}

		@Override
		public void beforeDetach(LifecycleEvent event) {
		}

		@Override
		public void afterDetach(LifecycleEvent event) {
		}

		@Override
		public void beforeAttach(LifecycleEvent event) {
		}

		@Override
		public void afterAttach(LifecycleEvent event) {
			save(AuditableOperation.UPDATE, event);
		}

	}
}
