/*
 * 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.sdo;

import java.util.List;
import java.util.Map;

import javax.persistence.PersistenceException;

import org.apache.openjpa.sdo.mapping.PropertyMapping;
import org.apache.openjpa.sdo.mapping.TypeMapping;
import org.apache.openjpa.util.OptimisticException;

import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Type;
import commonj.sdo.helper.DataFactory;

/**
 * Converts SDO DataObject to PersistenceCapable instance and vice versa.
 * Conversion is essentially copying data from one represnetaion to another.
 * Two aspects are a) maintaining indentity/version correspondence between
 * DataObject and PersistenceCapable representation b) closure of the instance
 * being converted. 
 * 
 * @author Pinaki Poddar
 * @since 0.2.0
 *
 */
public class SDOInstanceMapping {
	SDOMappingRepository repos;
	SDOEntityManager em;
	BiDiMap<DataObject, Object> closure = new BiDiMap<DataObject, Object>();

	public SDOInstanceMapping(SDOEntityManager em, SDOMappingRepository repos) {
		this.repos = repos;
		this.em = em;
	}
	
	/**
	 * Populates a DataObject from the field values of the given object.  
	 */
	public DataObject populateDataObject(Object pc) {
		Class cls = pc.getClass();
		Type type = repos.getSDOType(cls, true);
		DataObject data = (DataObject) closure.getReverse(pc);
		if (data != null) {
			sync(pc, data);
			return data;
		}
		data = newDataObject(type, pc);
		TypeMapping tm = repos.getMapping(type, true);
		List properties = data.getInstanceProperties();
		for (Object p:properties) {
			Property property = (Property)p;
			PropertyMapping pm = tm.getMapping(property);
			try {
				pm.populateDataObject(data, pc, this);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return data;
	}
	
	/**
	 * Populates a cached or new Object with the given DataObject.
	 * 
	 */
	public Object populatePC(DataObject data) {
		Type type = data.getType();
		Class cls = repos.getJavaClass(data.getType(), true);
		
		Object pc = findCached(data);
		if (pc != null) {
			sync(pc, data);
			return pc;
		}
		
		pc = newPC(cls, data);
		cache(data, pc);
		TypeMapping mapping = repos.getMapping(type, true);
		try {
			List properties = data.getInstanceProperties();
			for (Object p: properties) {
				Property property = (Property)p;
				if (!data.isSet(property)) {
					continue;
				} 
				PropertyMapping pm = mapping.getMapping(property);
				pm.populatePC(pc, data, this);
			} 
		} catch (Exception ex) {
			throw new PersistenceException(ex);
		}
		return pc;
	}
	
	/**
	 * Create a new instance of the given Class and synchronize identity/version
	 * information with the given DataObject.
	 */
	Object newPC(Class cls, DataObject data) {
		try {
			Object pc = cls.newInstance();
			sync(pc, data);
			return pc;
		} catch (Exception e) {
			throw new PersistenceException(e);
		} 
	}
	
	/**
	 * Create a new DataObject of the given Type and synchronize identity/version
	 * information with the given object.
	 */
	DataObject newDataObject(Type type, Object pc) {
		try {
			DataObject data = DataFactory.INSTANCE.create(type); 
			sync(pc, data);
			return data;
		} catch (Exception e) {
			throw new PersistenceException(e);
		} 
	}
	
	/**
	 * Get the closure of DataObjects and PersistenceCapable instances.
	 */
	public Map getClosure() {
		return closure;
	}
	
	/**
	 * Clears the state of this receiver.
	 *
	 */
	public void reset() {
		closure.clear();
	}

	/**
	 * Find a PersistenceCapable instance given a DataObject. Looks in its own
	 * closure and if not found looks up in the managed cahche.
	 * Can be null.
	 */
	Object findCached(DataObject data) {
		Object pc = closure.get(data);
		if (pc == null)
			return em.findCached(data);
		return pc;
	}
	
	/**
	 * Find a DataObject given a PersistenceCapable instance. Looks in its own
	 * closure and if not found looks up in the managed cahche.
	 * Can be null.
	 */
	DataObject findCached(Object pc) {
		DataObject data = (DataObject)closure.getReverse(pc);
		if (data == null) 
			return em.findCached(pc);
		return data;
	}
	
	/**
	 * Caches the pair in its own closure and also in the managed cache.
	 * @param data
	 * @param pc
	 */
	void cache(DataObject data, Object pc) {
		closure.put(data, pc);
		em.cache(data, pc);
	}
	
	/**
	 * Checks id/version of PersistenceCapable instance with the id/version values 
	 * from the given DataObject.
	 */
	void sync(Object pc, DataObject data) {
		Object idValue = ImplHelper.getIdValue(data);
		Object version = ImplHelper.getVersion(data);
		if (idValue != null) { // existing Data
			int pcversion = (Integer)em.getVersion(pc); 
			if (pcversion!=0 && !em.getVersion(pc).equals(version))
				throw new OptimisticException(pc);
		} else {
			try {
				repos.getMapping(data.getType(), true).getIdentityMapping()
					.populateDataObject(data, pc, this);
				repos.getMapping(data.getType(), true).getVersionMapping()
					.populateDataObject(data, pc, this);
			} catch (Exception e) {
				throw new PersistenceException(e);
			}
		}
	}


}
