/**************************************************************************************
 * Copyright (c) Jonas Bonér, Alexandre Vasseur. All rights reserved.                 *
 * http://aspectwerkz.codehaus.org                                                    *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the QPL license       *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.extension.persistence.jisp;

import java.lang.reflect.Method;

import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Properties;
import java.util.List;

import java.io.File;
import java.io.Serializable;
import java.io.IOException;

import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock;
import EDU.oswego.cs.dl.util.concurrent.WriterPreferenceReadWriteLock;

import com.coyotegulch.jisp.KeyObject;
import com.coyotegulch.jisp.ObjectIndex;
import com.coyotegulch.jisp.BTreeIndex;
import com.coyotegulch.jisp.BTreeObjectIterator;
import com.coyotegulch.jisp.IndexedObjectDatabase;

import org.codehaus.aspectwerkz.extension.persistence.definition.PersistenceDefinition;
import org.codehaus.aspectwerkz.extension.persistence.definition.IndexDefinition;
import org.codehaus.aspectwerkz.extension.persistence.definition.PersistenceManagerDefinition;
import org.codehaus.aspectwerkz.extension.persistence.definition.PersistentObjectDefinition;
import org.codehaus.aspectwerkz.extension.persistence.definition.IndexRefDefinition;
import org.codehaus.aspectwerkz.extension.persistence.AbstractPersistenceManager;
import org.codehaus.aspectwerkz.extension.persistence.PersistenceManager;
import org.codehaus.aspectwerkz.extension.persistence.Persistable;
import org.codehaus.aspectwerkz.extension.persistence.Index;
import org.codehaus.aspectwerkz.extension.definition.Definition;
import org.codehaus.aspectwerkz.extension.objectfactory.ObjectFactory;
import org.codehaus.aspectwerkz.extension.objectfactory.DefaultObjectFactory;
import org.codehaus.aspectwerkz.exception.WrappedRuntimeException;

/**
 * An implementation of the PersistenceManager interface using JISP.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér</a>
 */
public class JispPersistenceManager
        extends AbstractPersistenceManager implements Serializable {

    public static final boolean CREATE = true;
    public static final boolean LOAD = false;
    public static final int INDEX_ORDER = 23;

    /**
     * The sole instance of the persistence manager.
     */
    protected final static PersistenceManager s_soleInstance =
            new JispPersistenceManager();

    /**
     * The JISP database.
     */
    protected IndexedObjectDatabase m_database;

    /**
     * A read-write lock to ensure ACID:ity.
     */
    protected ReadWriteLock m_readWriteLock = new WriterPreferenceReadWriteLock();
//    protected final ReadWriteLock m_readWriteLock = new FIFOReadWriteLock();

    /**
     * Holds the indexes for each persistent object.
     */
    protected Map m_indexes = new HashMap();

    /**
     * Maps each index name to a the JISP object index.
     */
    protected Map m_jispIndexes = new HashMap();

    /**
     * Maps each index name to a the JISP object index type.
     */
    protected Map m_jispIndexTypes = new HashMap();

    /**
     * A list with the definitions for all the indexes added to
     * this persistence manager.
     */
    protected List m_indexDefinitions = new ArrayList();

    /**
     * The name of the database
     */
    protected String m_databaseName;

    /**
     * The full URL for the database
     */
    protected String m_databaseUrl = null;

    /**
     * The name of the database directory.
     */
    protected String m_databaseDir = null;

    /**
     * The name of the index directory.
     */
    protected String m_indexDir = null;

    /**
     * Marks if a new database should be created each time the service starts.
     */
    protected boolean m_createNewDbOnStartup = false;

    /**
     * Returns the one A only instance of the JispPersistenceManager.
     * Singleton.
     *
     * @return the instance
     */
    public static PersistenceManager getInstance() {
        return s_soleInstance;
    }

    /**
     * Initializes the persistence manager.
     * Creates the database A the indexes (BTree A Hash indexes).
     *
     * @param loader the classloader to use
     * @param definition the persistence definition
     */
    public synchronized void initialize(final ClassLoader loader,
                                        final Definition definition) {
        if (m_initialized) return;
        if (definition == null) throw new IllegalArgumentException("definition can not be null");

        m_loader = loader;
        m_definition = (PersistenceDefinition)definition;

        PersistenceManagerDefinition persistenceManagerDef = null;
        for (Iterator it = m_definition.getPersistenceManagers().iterator(); it.hasNext();) {
            PersistenceManagerDefinition def = (PersistenceManagerDefinition)it.next();
            if (def.getClassName().equals(getClass().getName())) {
                persistenceManagerDef = def;
                break;
            }
        }
        if (persistenceManagerDef == null) throw new RuntimeException("no definition specified for persistence manager: " + getClass().getName());

        m_indexDefinitions = getIndexes(persistenceManagerDef);
        if (m_indexDefinitions.size() == 0) throw new RuntimeException("no indexes specified for persistence manager");

        final Properties properties = persistenceManagerDef.getProperties();
        m_databaseDir = properties.getProperty(
                PersistenceManagerDefinition.DATABASE_DIR);
        m_databaseName = properties.getProperty(
                PersistenceManagerDefinition.DATABASE_NAME);
        m_indexDir = properties.getProperty(
                PersistenceManagerDefinition.INDEX_DIR);

        final StringBuffer buf = new StringBuffer();
        buf.append(m_databaseDir);
        buf.append(File.separator);
        buf.append(m_databaseName);
        m_databaseUrl = buf.toString();

        final File db = new File(m_databaseUrl);

        if (m_createNewDbOnStartup || !db.exists()) {
            createDatabaseDir(m_databaseDir);
            createDatabase(db, m_indexDefinitions);
        }
        else {
            loadDatabase(m_indexDefinitions);
        }

        loadDomainObjectConfigurations(m_definition);

        m_indexes = Collections.unmodifiableMap(m_indexes);
        m_jispIndexes = Collections.unmodifiableMap(m_jispIndexes);
        m_jispIndexTypes = Collections.unmodifiableMap(m_jispIndexTypes);

        m_initialized = true;
    }

    /**
     * Stores/Updates an object in the database.
     * If the object already exists in the database it makes an update.
     *
     * @param obj the object to store/update
     */
    public void store(final Object obj) {
        if (notInitialized()) throw new IllegalStateException("persistence manager is not initialized");
        if (obj == null) throw new IllegalArgumentException("object to store can not be null");
        if (!(obj instanceof Persistable)) throw new IllegalArgumentException("object to store does not implement Persistable");

        Class klass = obj.getClass();
        List keys = (List)m_indexes.get(klass.getName());
        KeyObject[] keyArray = new KeyObject[m_indexDefinitions.size()];

        int i = 0;
        for (Iterator it = keys.iterator(); it.hasNext(); i++) {
            try {
                final Index index = (Index)it.next();

                final Method method = index.getMethod();
                final Class indexFieldType = index.getFieldType();
                final Class indexType = (Class)m_jispIndexTypes.get(index.getIndexName());

                final Object indexFieldValue = method.invoke(obj, new Object[]{});

                ObjectFactory factory = new DefaultObjectFactory(
                        indexType,
                        new Class[]{indexFieldType},
                        new Object[]{indexFieldValue});

                keyArray[i] = (KeyObject)factory.newInstance();
            }
            catch (Exception e) {
                throw new WrappedRuntimeException(e);
            }
        }

        // fill up the key array with null keys if needed
        for (; i < m_indexDefinitions.size(); i++) {
            keyArray[i] = keyArray[0].makeNullKey();
        }

        try {
            m_readWriteLock.writeLock().acquire();
            try {
                m_database.write(keyArray, (Persistable)obj);
            }
            finally {
                m_readWriteLock.writeLock().release();
            }
        }
        catch (InterruptedException e) {
            notifyAll();
            Thread.currentThread().interrupt();
            throw new WrappedRuntimeException(e);
        }
        catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
    }

    /**
     * Finds an object from the database by its key.
     *
     * @param klass the class of the object to find
     * @param key the key of the object to find
     * @return the object
     */
    public Object retrieve(final Class klass, final Object key) {
        if (notInitialized()) throw new IllegalStateException("persistence manager is not initialized");
        if (klass == null || key == null) throw new IllegalArgumentException("class or index can not be null");

        final List indexes = (List)m_indexes.get(klass.getName());
        String indexName = null;
        for (Iterator it = indexes.iterator(); it.hasNext();) {
            Index index = (Index)it.next();
            if (index.getFieldType() == key.getClass()) {
                indexName = index.getIndexName();
            }
        }

        if (indexName == null) throw new RuntimeException("no such index for class specified");

        final ObjectIndex index = (ObjectIndex)m_jispIndexes.get(indexName);
        final Class indexType = (Class)m_jispIndexTypes.get(indexName);

        Object obj = null;
        try {
            ObjectFactory factory = new DefaultObjectFactory(
                    indexType, new Class[]{key.getClass()}, new Object[]{key});

            m_readWriteLock.readLock().acquire();
            try {
                obj = m_database.read(
                        (KeyObject)factory.newInstance(), index, m_loader);
            }
            finally {
                m_readWriteLock.readLock().release();
            }
        }
        catch (InterruptedException e) {
            notifyAll();
            Thread.currentThread().interrupt();
            throw new WrappedRuntimeException(e);
        }
        catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
        return obj;
    }

    /**
     * Retrieves all objects within a specific range.
     *
     * @param klass the class of the object to find
     * @param from index to start range from
     * @param to index to end range at
     * @return a collection with all the objects within the range
     */
    public List retrieveAllInRange(final Class klass,
                                   final Object from,
                                   final Object to) {
        if (notInitialized()) throw new IllegalStateException("persistence manager is not initialized");
        if (!(from instanceof Comparable) || !(to instanceof Comparable))
            throw new RuntimeException("from A to index must implement the Comparable interface");
        if (!from.getClass().equals(to.getClass()))
            throw new RuntimeException("from A to index must be of the same type");

        final List col = new ArrayList();
        final List keys = (List)m_indexes.get(klass.getName());

        String indexNameFrom = null;
        String indexNameTo = null;
        Method keyMethodTo = null;

        for (Iterator it = keys.iterator(); it.hasNext();) {
            Index index = (Index)it.next();

            if (index.getFieldType() == from.getClass()) {
                indexNameFrom = index.getIndexName();
            }
            if (index.getFieldType() == to.getClass()) {
                indexNameTo = index.getIndexName();
                keyMethodTo = index.getMethod();
            }
        }
        if (indexNameFrom == null || indexNameTo == null) {
            throw new RuntimeException("no such index for class specified");
        }

        final ObjectIndex indexFrom = (ObjectIndex)m_jispIndexes.get(indexNameFrom);
        final ObjectIndex indexTo = (ObjectIndex)m_jispIndexes.get(indexNameTo);
        final Class indexTypeFrom = (Class)m_jispIndexTypes.get(indexNameFrom);
        final Class indexTypeTo = (Class)m_jispIndexTypes.get(indexNameTo);

        if (!(indexFrom instanceof BTreeIndex) || !(indexTo instanceof BTreeIndex)) {
            throw new RuntimeException("from A to index must both be of type BTreeIndex");
        }
        if (!indexTypeFrom.equals(indexTypeTo)) {
            throw new RuntimeException("from A to index must be of the same index type");
        }

        try {
            ObjectFactory fromFactory = new DefaultObjectFactory(
                    indexTypeFrom,
                    new Class[]{from.getClass()},
                    new Object[]{from});

            m_readWriteLock.readLock().acquire();
            try {
                final BTreeObjectIterator iterator =
                        m_database.createIterator((BTreeIndex)indexFrom);

                if (!iterator.moveTo((KeyObject)fromFactory.newInstance())) {
                    throw new RuntimeException("record " + from + " does not exist");
                }
                do {
                    final Object item = iterator.getObject();
                    if (item != null) {
                        col.add(item);
                    }
                    else {
                        break;
                    }
                    final Object keyFieldValue =
                            keyMethodTo.invoke(item, new Object[]{});
                    if (((Comparable)keyFieldValue).compareTo(to) == 0) {
                        break;
                    }
                }
                while (iterator.moveNext());
            }
            finally {
                m_readWriteLock.readLock().release();
            }
        }
        catch (InterruptedException e) {
            notifyAll();
            Thread.currentThread().interrupt();
            throw new WrappedRuntimeException(e);
        }
        catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
        return col;
    }

    /**
     * Removes an object from the database.
     *
     * @param obj the object to remove
     */
    public void remove(final Object obj) {
        if (notInitialized()) throw new IllegalStateException("persistence manager is not initialized");
        if (obj == null) throw new IllegalArgumentException("object to remove can not be null");
        if (!(obj instanceof Persistable)) throw new IllegalArgumentException("object to remove does not implement Persistable");

        final Class klass = obj.getClass();

        final List keys = getKeysForPersistentObject(klass);
        final KeyObject[] keyArray = new KeyObject[keys.size()];

        int i = 0;
        for (Iterator it = keys.iterator(); it.hasNext(); i++) {
            try {
                final Index index = (Index)it.next();

                final Method method = index.getMethod();
                final Class indexFieldType = index.getFieldType();
                final Class indexType =
                        (Class)m_jispIndexTypes.get(index.getIndexName());

                final Object indexFieldValue = method.invoke(obj, new Object[]{});

                ObjectFactory factory = new DefaultObjectFactory(
                        indexType,
                        new Class[]{indexFieldType},
                        new Object[]{indexFieldValue});

                keyArray[i] = (KeyObject)factory.newInstance();
            }
            catch (Exception e) {
                throw new WrappedRuntimeException(e);
            }
        }
        try {
            m_readWriteLock.writeLock().acquire();
            try {
                m_database.remove(keyArray);
            }
            finally {
                m_readWriteLock.writeLock().release();
            }
        }
        catch (InterruptedException e) {
            notifyAll();
            Thread.currentThread().interrupt();
            throw new WrappedRuntimeException(e);
        }
        catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
    }

    /**
     * Returns all the index definitions for this persistence manager.
     *
     * @param persistenceManagerDef the persistence definition
     * @return a list with all the index definition
     */
    protected List getIndexes(PersistenceManagerDefinition persistenceManagerDef) {
        final List btreeIndexes = new ArrayList();
        final List indexRefDefinitions = persistenceManagerDef.getIndexRefs();
        for (Iterator it = indexRefDefinitions.iterator(); it.hasNext();) {
            String indexName = ((IndexRefDefinition)it.next()).getName();
            List indexDefinitions = m_definition.getIndexes();
            for (Iterator it2 = indexDefinitions.iterator(); it2.hasNext();) {
                IndexDefinition indexDefinition = (IndexDefinition)it2.next();
                if (indexDefinition.getName().equals(indexName)) {
                    btreeIndexes.add(indexDefinition);
                }
            }
        }
        return btreeIndexes;
    }

    /**
     * Gets the domain object definition from the server definition.
     *
     * @param def the persistence definition
     */
    protected void loadDomainObjectConfigurations(final PersistenceDefinition def) {
        if (def == null) throw new IllegalArgumentException("persistence definition can not be null");

        final List persistentObjectDefs = def.getPersistentObjects();

        for (Iterator it1 = persistentObjectDefs.iterator(); it1.hasNext();) {
            final PersistentObjectDefinition persistentObjectDef =
                    (PersistentObjectDefinition)it1.next();

            final List indexRefs = persistentObjectDef.getIndexRefs();
            final String persistentObjectTypeName = persistentObjectDef.getClassName();
            if (indexRefs.size() == 0) throw new RuntimeException("no indexes specified for persistent object: " + persistentObjectTypeName);

            Class persistentObjectType = null;
            try {
                persistentObjectType = m_loader.loadClass(persistentObjectTypeName);
            }
            catch (ClassNotFoundException e) {
                continue;
            }

            m_indexes.put(persistentObjectTypeName, new ArrayList());

            for (Iterator it2 = indexRefs.iterator(); it2.hasNext();) {
                IndexRefDefinition index = (IndexRefDefinition)it2.next();

                final String indexName = index.getName();
                final String indexMethodName = index.getMethod();
                try {
                    final Method method = persistentObjectType.getDeclaredMethod(
                            indexMethodName, new Class[]{});

                    Class indexFieldType = method.getReturnType();

                    if (indexFieldType.getName().equals(long.class.getName())) {
                        indexFieldType = Long.class;
                    }
                    else if (indexFieldType.getName().equals(int.class.getName())) {
                        indexFieldType = Integer.class;
                    }
                    else if (indexFieldType.getName().equals(double.class.getName())) {
                        indexFieldType = Double.class;
                    }
                    else if (indexFieldType.getName().equals(float.class.getName())) {
                        indexFieldType = Float.class;
                    }
                    else if (indexFieldType.getName().equals(short.class.getName())) {
                        indexFieldType = Short.class;
                    }
                    else if (indexFieldType.getName().equals(boolean.class.getName())) {
                        throw new RuntimeException("index field can not be boolean");
                    }
                    ((List)m_indexes.get(persistentObjectTypeName)).add(
                            new Index(method, indexFieldType, indexName));
                }
                catch (Exception e) {
                    throw new WrappedRuntimeException(e);
                }
            }
        }
    }

    /**
     * Creates a new jisp database.
     *
     * @param btreeIndexes
     */
    protected void createDatabase(final File db, final List btreeIndexes) {
        if (db == null || btreeIndexes == null) throw new IllegalArgumentException("db or index definition can not be null");

        try {
            if (db.exists()) {
                db.delete();
            }
            m_database = new IndexedObjectDatabase(
                    m_databaseUrl, CREATE, m_loader);

            final List indexes = createBtreeIndexes(btreeIndexes);
            for (Iterator it = indexes.iterator(); it.hasNext();) {
                m_database.attachIndex((BTreeIndex)it.next());
            }
        }
        catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
    }

    /**
     * Loads the jisp database.
     *
     * @param btreeIndexes
     */
    protected void loadDatabase(final List btreeIndexes) {
        if (btreeIndexes == null) throw new IllegalArgumentException("index definition can not be null");

        try {
            m_database = new IndexedObjectDatabase(m_databaseUrl, LOAD, m_loader);

            final List indexes = loadBtreeIndexes(btreeIndexes);
            for (Iterator it = indexes.iterator(); it.hasNext();) {
                m_database.attachIndex((BTreeIndex)it.next());
            }
        }
        catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
    }

    /**
     * Creates the btree indexes.
     *
     * @param btreeIndexes
     * @return the indexes
     * @throws java.lang.ClassNotFoundException
     * @throws java.io.IOException
     */
    protected List createBtreeIndexes(final List btreeIndexes)
            throws ClassNotFoundException, IOException {
        if (btreeIndexes == null) throw new IllegalArgumentException("index configurations can not be null");

        final List indexes = new ArrayList();
        for (Iterator it = btreeIndexes.iterator(); it.hasNext();) {

            final IndexDefinition indexDef = (IndexDefinition)it.next();
            final String jispIndexType = getJispIndexType(indexDef.getType());

            ObjectFactory factory = new DefaultObjectFactory(jispIndexType);

            final StringBuffer indexFile = new StringBuffer();
            indexFile.append(m_indexDir);
            indexFile.append(File.separator);
            indexFile.append(indexDef.getName());

            File killit = new File(indexFile.toString());
            if (killit.exists()) killit.delete();

            final BTreeIndex index = new BTreeIndex(
                    indexFile.toString(),
                    INDEX_ORDER,
                    (KeyObject)factory.newInstance(),
                    false, m_loader);

            indexes.add(index);

            m_jispIndexes.put(indexDef.getName(), index);

            final Class indexType = m_loader.loadClass(
                    getJispIndexType(indexDef.getType()));

            m_jispIndexTypes.put(indexDef.getName(), indexType);
        }
        return indexes;
    }

    /**
     * Loads the btree indexes.
     *
     * @param btreeIndexes
     * @return the indexes
     * @throws java.lang.ClassNotFoundException
     * @throws java.io.IOException
     */
    protected List loadBtreeIndexes(final List btreeIndexes)
            throws ClassNotFoundException, IOException {
        if (btreeIndexes == null) throw new IllegalArgumentException("index configurations can not be null");

        final List indexes = new ArrayList();

        for (Iterator it = btreeIndexes.iterator(); it.hasNext();) {

            final IndexDefinition indexDef = (IndexDefinition)it.next();

            final StringBuffer indexFile = new StringBuffer();
            indexFile.append(m_databaseDir);
            indexFile.append(File.separator);
            indexFile.append(indexDef.getName());

            final BTreeIndex index = new BTreeIndex(indexFile.toString(), m_loader);
            indexes.add(index);

            m_jispIndexes.put(indexDef.getName(), index);

            final Class indexType = m_loader.loadClass(
                    getJispIndexType(indexDef.getType()));

            m_jispIndexTypes.put(indexDef.getName(), indexType);
        }
        return indexes;
    }

    /**
     * Returns the mapped JISP index type.
     *
     * @todo support more JISP index types than String A Long
     *
     * @param type the index type
     * @return the JISP index type
     */
    protected static String getJispIndexType(final String type) {
        final String indexType;
        if (type.equals("java.lang.String")) {
            indexType = "com.coyotegulch.jisp.StringKey32";
        }
        else if (type.equals("java.lang.Long")) {
            indexType = "org.codehaus.aspectwerkz.extension.persistence.jisp.LongKey";
        }
        else {
            throw new RuntimeException(type + " is not a valid JISP index type");
        }
        return indexType;
    }

    /**
     * Returns the keys to the object.
     *
     * @param klass the class of the object of interest
     * @return the keys
     */
    protected List getKeysForPersistentObject(final Class klass) {
        return (List)m_indexes.get(klass.getName());
    }

    /**
     * Create the database dir (if it does not exist).
     *
     * @param databaseDir the meta-data dir
     */
    protected static void createDatabaseDir(final String databaseDir) {
        File dir = new File(databaseDir);
        if (!dir.exists()) {
            if (!dir.mkdirs()) {
                throw new RuntimeException("could not create directory " + databaseDir);
            }
        }
    }

    /**
     *  Non-public constructor to prevent instantiability.
     */
    protected JispPersistenceManager() {
    }
}



