package org.hypergraphdb.transaction;

import java.util.List;

import org.hypergraphdb.util.HGSortedSet;
import org.hypergraphdb.util.RefCountedMap;
import org.hypergraphdb.util.RefResolver;

public class TxCacheSet<Key, E> extends TxSet<E>
{
    private Key key;
    private RefCountedMap<Key,SetTxBox<E>> writeMap;    
    private RefResolver<Key, ? extends HGSortedSet<E>> loader; 
    
    @SuppressWarnings("unchecked")
    VBoxBody<HGSortedSet<E>> insertBody(long txNumber, HGSortedSet<E> x)
    {
        txManager.COMMIT_LOCK.lock();
        try
        {
            if (txNumber >= txManager.mostRecentRecord.transactionNumber) // is this the latest value
            {
                // a marker that we have loaded some older values for older, but still running tx
                if (S.body.version == -1)
                    S.body = S.makeNewBody(x, txNumber, S.body.next);
                else if (S.body.version < txNumber) // otherwise we just add as the newest body with the current tx number
                    S.body = S.makeNewBody(x, txNumber, S.body);
                else // s.body.version == txNumber, can't be bigger since we are the last TX
                	// This means we are re-creating/restoring a set previously deleted in the current transaction
                	// so we just replace with the new value provided, the previous state associated with that TX should
                	// be wiped out
                	S.body = S.makeNewBody(x, txNumber, S.body.next);
                return S.body;
            }
            // if not current, we must insert it into the list of bodies 
            else
            {
                if (S.body.version == -1)
                {
                    // We want to avoid endlessly loading the same version from disk in case
                    // we always load into a transaction that doesn't have the latest number, but
                    // the value hasn't actually changed. The 'S.loadedAt' field marks the transaction
                    // number when the box was first loaded so if there are no commits between S.loadedAt
                    // and now (which a top version of -1 indicates) then the latest possible version 
                    // is S.loadedAt. So if txNumber>=S.loadedAt we should
                    // mark this version as the latest
                    if (txNumber >= ((CacheSetTxBox<Key, E>)S).loadedAt)
                    {
                        S.body = S.makeNewBody(x, ((CacheSetTxBox<Key, E>)S).loadedAt, S.body.next);
                        return S.body;
                    }
                }
                
                VBoxBody<HGSortedSet<E>> currentBody = S.body;
                while (currentBody.next != null && currentBody.next.version > txNumber)
                    currentBody = currentBody.next;
                // Could happen that we unnecessarily loaded the same set twice due to race conditions
                // so at least we avoid storing it twice.
                if (currentBody.next != null && currentBody.next.version == txNumber)
                    return currentBody.next;
                // we need to insert b/w currentBody and currentBody.next
                VBoxBody<HGSortedSet<E>> newBody = S.makeNewBody(x, txNumber, currentBody.next);
                currentBody.setNext(newBody);
                return newBody;
            }               
        }
        finally
        {
            txManager.COMMIT_LOCK.unlock();
        }        
    }
    
    VBoxBody<HGSortedSet<E>> load(long txNumber)
    {
        HGSortedSet<E> x = loader.resolve(key);
        return insertBody(txNumber, x);
        //return x;
    }
    
    @Override
    HGSortedSet<E> read()
    {
        HGTransaction tx = txManager.getContext().getCurrent();
        if (tx == null)
            return S.body.value;
        HGSortedSet<E> x = tx.getLocalValue(S);        
        if (x == null) // no local value, we get the correct version if loaded or return null if not
        {
            VBoxBody<HGSortedSet<E>> b = S.body;
            // if the current transaction is not older than the top body we return it:
            // it will be null if we need a load from disk or the latest committed value
            // which would be the correct version
            if (b.version <= tx.getNumber())
            {
                if (b.value == null)
                    b = load(tx.getNumber());
            }
            else
            {
                // else try to find the exact same version as the current transaction                
                while (b.version > tx.getNumber() && b.next != null)
                    b = b.next;
                if (b.version != tx.getNumber())
                    b = load(tx.getNumber());
            }            
            if (!tx.isReadOnly())
                tx.bodiesRead.put(S, b);
            return b.value;
        }
        else 
        {
            return x == HGTransaction.NULL_VALUE ? null : x;
        }
    }
    
    @Override
    HGSortedSet<E> write()
    {
        List<LogEntry> log = txManager.getContext().getCurrent().getAttribute(S);
        if (log == null) // should we copy-on-write or have we done so already?
        {
            HGSortedSet<E> readOnly = read(); // S.getForWrite();
            HGSortedSet<E> writeable = cloneSet(readOnly);
            S.put(writeable);
            writeMap.put(key, S);
        }
        return S.get();
    }
    
    public TxCacheSet(final HGTransactionManager txManager, 
                      final HGSortedSet<E> backingSet, 
                      final Key key,
                      final RefResolver<Key, ? extends HGSortedSet<E>> loader,
                      final RefCountedMap<Key, SetTxBox<E>> writeMap)
    {
        this.txManager = txManager;
        this.key = key;
        this.loader = loader;
        this.writeMap = writeMap;
        HGTransaction tx = txManager.getContext().getCurrent();
        if (tx == null)
        {
            S = new CacheSetTxBox<Key, E>(txManager, backingSet, this);
            return;
        }        
        S = writeMap.get(key);         
        if (S == null)
        {
            S = new CacheSetTxBox<Key, E>(txManager, backingSet, this);
            
            // we need to tag the body with the current transaction's version
            // since body.version is final, we replace with a new body
            S.body = S.makeNewBody(backingSet, tx.getNumber(), null); 
            // if this is an old transaction, we need to put null at the top of the body
            // list so the latest will get reloaded if needed
            if (tx.getNumber() < txManager.mostRecentRecord.transactionNumber)
                S.body = S.makeNewBody(null, -1, S.body);
        }
        else
        {
        	// This means we have deleted the set for that key in this transaction and now we are restoring it,
        	// that is creating a new set associated with the same key.
        	// We do re-attached to the CacheSetTxBox, but we have to wipe out the state associated
        	// with this transaction before the set was deleted. This means replacing the body
        	// for this exact TX version with the new backing set and also deleting the local
        	// version in the transaction object.
        	//
        	// Or is it possible we have evicted the set from the cache, but it's still
        	// valid and there might changes in the TX boxesWritten that we want to preserve?
        	// How to distinguish b/w delete sets from storage and evicted from the cache?
        	// TODO: this is an open problem!!!        	
    		insertBody(tx.getNumber(), backingSet);
    		tx.removeBoxValue(S);
        }
    }   
    
    public static class CacheSetTxBox<Key, E> extends SetTxBox<E>
    {
        long loadedAt;
        
        CacheSetTxBox(final HGTransactionManager txManager, 
                      final HGSortedSet<E> backingSet,
                      final TxSet<E> thisSet)
        {
            super(txManager, backingSet, thisSet);
            loadedAt = txManager.mostRecentRecord.transactionNumber;
        }
        
        @SuppressWarnings("unchecked")
        HGSortedSet<E> getLastCommitted(HGTransaction tx)
        {
            TxCacheSet<Key, E> s = (TxCacheSet<Key, E>)thisSet;            
            HGSortedSet<E> lastCommitted = super.getLastCommitted(tx);                
            return  (lastCommitted == null) ?  s.load(tx.getNumber()).value : lastCommitted;
        }
        
        @Override
        public VBoxBody<HGSortedSet<E>> commit(HGTransaction tx, HGSortedSet<E> newvalue, long txNumber)
        {
            VBoxBody<HGSortedSet<E>> latest = super.commit(tx, newvalue, txNumber);
            // check if we have the special "old value" marker hanging in the second place of
            // the list of bodies after the commit, and if so unlink it
            if (latest.next != null && latest.next.version == -1)
                latest.setNext(latest.next.next);
            return latest;
        }
        
        @SuppressWarnings("unchecked")
        @Override
        public void finish(HGTransaction tx)
        {
            if (tx.getAttribute(this) != null)
            {
                TxCacheSet<Key, E> s = (TxCacheSet<Key, E>)thisSet;
                s.writeMap.remove(s.key);
            }
        }        
    }
}