/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/store/org/apache/slide/store/tamino/jdomobjects/XTLock.java,v 1.2 2005/02/23 16:36:54 pnever Exp $
 * $Revision: 1.2 $
 * $Date: 2005/02/23 16:36:54 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed 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.slide.store.tamino.jdomobjects;

import com.softwareag.common.instrumentation.logging.Level;
import com.softwareag.common.instrumentation.logging.Logger;
import com.softwareag.common.instrumentation.logging.LoggerFactory;
import com.softwareag.common.instrumentation.logging.LoggerUtil;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.slide.common.Domain;
import org.apache.slide.store.tamino.common.IDescriptors;
import org.apache.slide.store.tamino.common.XTLockSettings;
import org.apache.slide.store.tamino.common.XTLockedException;
import org.apache.slide.util.ClassName;
import org.apache.slide.util.XAssertionFailed;

/**
 ** Transient locking which persists until commit/rollback. TLock is a shared lock (aka
 ** 'ReadWriterLock'), it permits any number of readers as long as there is no writer.
 ** Writer have exclusive access. Acquired anytime a descriptor is accessed. Released
 ** once during commit or abort.</p>
 **
 ** <p>If a Descriptors is write-locked, a working copy for the respective thread will
 ** be created in the delta cache. </p>
 **
 ** <p>TLock is a helper class for XGlobalCache - nobody else ever should know about it.
 **
 ** TODO: improve performance by merging GlobalCache and DeltaCache.
 **/
public class XTLock {
    private static final String LOGNAME = LoggerUtil.getThisClassName();
    private static final String CLASSNAME = new ClassName(LOGNAME).getPlainName();
    private static final Logger logger = LoggerFactory.getLogger(LOGNAME + ".tlock");
    
    // lock types
    // Caution: the lock type is relative to the current thread. E.g. if thread A holds a
    // write Lock W, W's lock type releated to thread B is NO_LOCK.
    public static final int NO_LOCK = 0;
    public static final int READ_LOCK = 1;
    public static final int WRITE_LOCK = 2;
    
    
    private XTLockSettings tlockSettings;
    private final Object sync;
    
    /** never null */
    private IDescriptors readerDesc;
    
    /** never null if write-locked */
    private IDescriptors writerDesc;
    
    private int waitingWriter;
    
    /** List of threads. Never contains activeWriter **/
    private final List activeReader;
    
    private Thread activeWriter;
    
    public XTLock(Object sync, XTLockSettings tlockSettings, IDescriptors desc) {
        if (desc == null) {
            throw new XAssertionFailed();
        }
        this.tlockSettings = tlockSettings;
        this.sync = sync;
        
        this.readerDesc = desc;
        this.readerDesc.setReadOnly(true);
        this.writerDesc = null;
        this.waitingWriter = 0;
        this.activeReader = new ArrayList();
        this.activeWriter = null;
    }
    
    public Thread getActiveWriter() {
        return activeWriter;
    }
    
    public String getUuri() {
        return readerDesc.getUuri();
    }
    
    /**
     ** Obtains read or write lock for the current thread.
     ** The lock will persist til end of transaction.
     **
     ** @return current thread's lock type previously hold by current thread
     **/
    public int acquire(int type) throws XTLockedException {
        switch (type) {
            case NO_LOCK:
                return getType();
            case READ_LOCK:
                return acquireReader();
            case WRITE_LOCK:
                return acquireWriter();
            default:
                throw new XAssertionFailed("acquire failed." + type + " is invalid");
        }
    }
    
    public IDescriptors getDescriptors() {
        int type;
        
        type = getType();
        switch (type) {
            case NO_LOCK:
            case READ_LOCK:
                return readerDesc;
            case WRITE_LOCK:
                return writerDesc;
            default:
                throw new XAssertionFailed("getDescriptors failed." + type + " is invalid");
        }
    }
    
    public int getType() {
        return getType(Thread.currentThread());
    }
    
    public int getType(Thread current) {
        if (activeWriter == current) {
            return WRITE_LOCK;
        }
        if (activeReader.contains(current)) {
            return READ_LOCK;
        }
        return NO_LOCK;
    }
    
    /**
     ** @return current thread's lock type that has been released by this method.
     **/
    public int release(boolean commit) {
        Thread current;
        
        current = Thread.currentThread();
        if (current == activeWriter) {
            activeWriter = null;
            if (writerDesc == null) {
                throw new XAssertionFailed(toString());
            }
            if (commit) {
                readerDesc = writerDesc;
            }
            writerDesc = null;
            logSuccess("-w");
            sync.notifyAll();
            return WRITE_LOCK;
        }
        if (activeReader.remove(current)) {
            logSuccess("-r");
            sync.notifyAll();
            return READ_LOCK;
        }
        return NO_LOCK;
    }
    
    /**
     ** @return true if there is any thread with lock type != NO_LOCK
     **/
    public boolean isLocked() {
        return activeWriter != null || activeReader.size() > 0;
    }
    
    public String toString() {
        String activeWriterName = (activeWriter!=null)?activeWriter.getName():"NULL";
        List activeReaderList = new ArrayList(activeReader){
            public String toString(){
                String result = "[";
                Iterator iter = this.iterator();
                while (iter.hasNext()) {
                    result = result + ((Thread)iter.next()).getName() + ",";
                }
                return result + "]";
            }};
        
        return "TLock[uuri="+readerDesc.getUuri()+", hashCode="+Integer.toHexString(hashCode())
            +", writer="+activeWriterName+", reader="+activeReaderList+", waiting="+waitingWriter+"]";
    }
    
    //--
    
    /**
     ** Obtains a read lock for the current thread.
     ** The lock will persist till end of transaction.
     **
     ** @return lock type previously hold by current thread
     **/
    private int acquireReader() throws XTLockedException {
        Thread current;
        
        current = Thread.currentThread();
        if (current == activeWriter) {
            //logSuccess("acquireReader: already write locked");
            return WRITE_LOCK;
        }
        if (activeReader.contains(current)) {
            //logSuccess("acquireReader: already read locked");
            return READ_LOCK;
        }
        awaitReader();
        activeReader.add(current);
        logSuccess("+r");
        return NO_LOCK;
    }
    
    /**
     ** Obtains a write lock for the current thread.
     ** The lock will persist til end of transaction.
     **
     ** @return lock type previously hold by current thread
     **/
    private int acquireWriter() throws XTLockedException {
        Thread current;
        
        current = Thread.currentThread();
        if (current == activeWriter) {
            //logSuccess("acquireWriter: already write locked");
            return WRITE_LOCK;
        }
        waitingWriter++;
        try {
            awaitWriter();
        } finally {
            waitingWriter--;
        }
        activeWriter = current;
        if (writerDesc != null) {
            throw new XAssertionFailed(toString());
        }
        writerDesc = readerDesc.getClone(readerDesc.getUuri());
        writerDesc.setReadOnly(false);
        if (activeReader.remove(current)) {
            logSuccess("!w");
            return READ_LOCK;
        } else {
            logSuccess("+w");
            return NO_LOCK;
        }
    }
    
    private void awaitReader() throws XTLockedException {
        long started;
        long remaining;
        long timeout = tlockSettings.getTimeout();
        
        if (allowReader()) {
            return;
        }
        started = System.currentTimeMillis();
        for (remaining = timeout; remaining > 0; remaining = timeout - (System.currentTimeMillis() - started)) {
            logSuccess("?r");
            if( logger.isLoggable(Level.FINE) )
                logger.fine( "awaitReader has to wait for lock "+ toString() +
                                " [timeout: "+(remaining/1000)+" sec]" );
            try {
                sync.wait(remaining);
            } catch (InterruptedException e) {
                // fall through
            }
            if (allowReader()) {
                return;
            }
        }
        throw timedOut();
    }
    
    private void awaitWriter() throws XTLockedException {
        long started;
        long remaining;
        long timeout = tlockSettings.getTimeout();
        
        if (allowWriter()) {
            return;
        }
        started = System.currentTimeMillis();
        for (remaining = timeout; remaining > 0; remaining = timeout - (System.currentTimeMillis() - started)) {
            logSuccess("?w");
            if( logger.isLoggable(Level.FINE) )
                logger.fine( "awaitWriter has to wait for lock "+ toString() +
                                " [timeout: "+(remaining/1000)+" sec]" );
            try {
                sync.wait(remaining);
            } catch (InterruptedException e) {
                // fall through
            }
            if (allowWriter()) {
                return;
            }
        }
        throw timedOut();
    }
    
    private boolean allowReader() {
        if (tlockSettings.getIsolationLevel() == XTLockSettings.READ_COMMITTED) {
            return true;
        }
        return activeWriter == null && waitingWriter == 0;
    }
    
    private boolean allowWriter() {
        if (activeWriter != null) {
            return activeWriter == Thread.currentThread();
        }
        if (tlockSettings.getIsolationLevel() == XTLockSettings.READ_COMMITTED) {
            return true;
        }
        switch (activeReader.size()) {
            case 0:
                return true;
            case 1:
                return activeReader.get(0) == Thread.currentThread();
            default:
                return false;
        }
    }
    
    private XTLockedException timedOut() {
        if( logger.isLoggable(Level.FINE) )
            logger.fine( "TLock wait timed out: "+toString());
        
        logSuccess("##");
        return new XTLockedException( "TLock wait timed out: "+toString());
    }
    
    private void logSuccess(String operation) {
        if ("true".equalsIgnoreCase(Domain.getParameter("debug_tlock", "false"))) {
            printlnSuccess(operation);
        }
        if( logger.isLoggable(Level.FINE) ) {
            logger.fine(CLASSNAME, "["+Thread.currentThread().getName()+ "]", operation + " success:" + toString());
        }
    }
    
    
    
    private void printlnSuccess(String operation) {
        System.out.println("---< "+operation+" >--- ["+Thread.currentThread().getName()+"] "+toString());
    }
    
    
    
    
    
}

