package iac;
import java.io.*;
import java.util.*;
import org.apache.lucene.index.*;
import org.apache.lucene.document.*;
import org.apache.lucene.search.*;
import org.apache.lucene.analysis.*;

/** Rules:
 *  Once created, searchers are valid until closed.
 *  If doing an update, searchers must not be created between
 * close of the
 * reader
 *      that deletes and close of the writer that adds.
 *  Writers may be reused until Reader or Searcher is needed.
 *  Readers may be reused until Writer or Searcher is needed.
 *  Searchers may be reused until the index is changed.
 *  There may be only one of a Reader and/or Writer at a time.
 *  If you get it, release it.
 *
 *
 */
// -- From the static class I made factory class. For every index (path) one
//    instance is mapped.
// -- getSearcher returns ManagedSearcher objects so you can simply call close
//    method intsead of releaseSearcher
// -- releaseSearcher is not public
//
// maybe we should decouple the factory and access control logic
//
// questions are:
// how to configure IndexWriter (analyzer, mergeFactor etc.)
// should we use Directory objects instead of path (how to manage readers/writers/searchers of RAMDirector)
// could we decouple Searcher managment and Writer/Reader managment?
// do we need static Maps? no. how to make handle checkout infos

public class IndexAccessControl implements SearcherListener {
    private Analyzer analyzer;
    private File path;
    
    private static final Map WRITER_PATHS = new HashMap(); // path -> CheckoutInfo
    private static final Map SEARCHER_PATHS = new HashMap(); // path -> Searcher
    private static final Map OLD_SEARCHERS = new HashMap(); // Searcher -> CheckoutInfo
    private static final Map instances = new HashMap();     // path --> instance of this class
    
    public static IndexAccessControl getInstance(String path) {
        return getInstance(new File(path));
    }
    /** Creates a new instance; for every path one instance is allowed */
    public static IndexAccessControl getInstance(File path) {
        
        synchronized(instances) {
            String key = path.getAbsolutePath();
            IndexAccessControl iac = (IndexAccessControl) instances.get(key);
            if(iac == null) {
                // creating new
                iac = new IndexAccessControl(path, new SimpleAnalyzer());
                instances.put(key, iac);
            }
            return iac;
        }
    }
    
    
    /** singleton */
    private IndexAccessControl(File path, Analyzer analyzer) {
        this.path = path;
        this.analyzer = analyzer;
    }
    
    /** get for adding documents.
     * blocks: readers until released
     */
    public IndexWriter getWriter() throws IOException {
        IndexWriter writer = null;
        
        // String sync = path.getAbsolutePath().intern();
        // synchronized (sync) // sync on specific index
        // because of the one to one mapping between paths and instances we need only
        // to obtain the monitor of this object
        synchronized(this) {
            do {
                CheckoutInfo info =
                (CheckoutInfo)WRITER_PATHS.get(path);
                if (info != null) // may already have a writer, use it
                {
                    if (info.writer != null) // yup, have a writer
                    {
                        info.checkoutCount++;
                        writer = info.writer;
                    }
                    else // not a writer, it must be a reader, wait for it to finish to try again
                    {
                        try {
                            info.wait(); // wait for info to be released
                        }
                        catch (InterruptedException e) {
                            // TODO: Will this ever happen?
                            e.printStackTrace();
                            return null;
                        }
                    }
                }
                else // no writer, create one
                {
                    boolean missing = !path.exists();
                    if (missing) path.mkdir();
                    writer = new IndexWriter(path, analyzer,
/*create*/missing);
                    writer.mergeFactor = 2;
                    info = new CheckoutInfo(writer);
                }
            }
            while (writer == null);
        }
        return writer;
    }
    
    public void releaseWriter(IndexWriter writer) throws
    IOException {
        
        synchronized (this) {
            CheckoutInfo info = (CheckoutInfo)WRITER_PATHS.get(path);
            if (info != null && writer == info.writer) // writer was checked out
            {
                if (info.checkoutCount > 1) // writer has other references
                {
                    info.checkoutCount--;
                    writer = null; // avoid close()
                }
                else // last reference to writer
                {
                    WRITER_PATHS.remove(path);
                    writer = info.writer;
                    info.notify(); // notify waiters to try again
                }
            }
        }
        // close the writer (unless it still has checkouts)
        if (writer != null) writer.close();
    }
    
    CheckoutInfo searcherInfo = null;
    // info about Searcher that should be closed because the index has benn 
    // modified since this searcher was created
    CheckoutInfo oldSearcherInfo = null;
    
    /** get for search. */
    public Searcher getSearcher() throws IOException {
        ManagedSearcher is;
        synchronized(this) {
            if (searcherInfo == null || searcherInfo.searcher == null || IndexReader.lastModified(path) > searcherInfo.creationTime) 
            {
                // need new searcher but first check whether we have an old one
                if ( searcherInfo != null && searcherInfo.searcher != null ) {
                    // that means : IndexReader.lastModified(path) > searcherInfo.creationTime
                    // this searcher won't be returned any more but others may be using it
                    if (searcherInfo.checkoutCount > 1) // searcher has other references
                    {
                        searcherInfo.checkoutCount--;
                        oldSearcherInfo = searcherInfo;
                    }
                    else // last reference to searcher
                    {
                        //System.out.println(Thread.currentThread().getName() + " getSearcher() last reference: close");
                        searcherInfo.searcher.getRealSearcher().close();
                    }
                }
                
                is = new ManagedSearcher(this, new IndexSearcher(IndexReader.open(path)));
                searcherInfo = new CheckoutInfo(is);
            }  
            else 
            {
                // use existing searcher
                is = searcherInfo.searcher;
                searcherInfo.checkoutCount++;
            }
        }
        return is;
    }
    
    public void searcherClosed(SearcherEvent event) throws IOException {
        //System.out.println(Thread.currentThread().getName() + " searcherClosed event received");
        releaseSearcher((ManagedSearcher) event.getSource());
    }
    
    void releaseSearcher(ManagedSearcher searcher) throws IOException {
/*        
        synchronized (this) {
            CheckoutInfo info = searcherInfo;
            if (info == null || searcher != info.searcher) // this isn't the info we're looking for
            {
                info = oldSearcherInfo;
            }
            if (info != null) // found a searcher
            {
                if (info.checkoutCount > 1) // searcher has other references
                {
                    info.checkoutCount--;
                }
                else // last reference to searcher
                {
                    System.out.println(Thread.currentThread().getName() + " releaseSearcher() last reference: close");
                    info.searcher.getRealSearcher().close();
                }
            }
            else // can't find searcher, just close it
            {
                // I think this is failure
                throw new RuntimeException("huups!");
                // searcher.close();
            }
        }
 */
        synchronized (this) {
            CheckoutInfo info = searcherInfo;
            
            if (info == null || info.searcher == null || searcher != info.searcher) // this isn't the info we're looking for
            {
                info = oldSearcherInfo;
            }
            if (info != null && info.searcher != null) // found a searcher
            {
                if (info.checkoutCount > 1) // searcher has other references
                {
                    info.checkoutCount--;
                }
                else // last reference to searcher
                {
                    System.out.println(Thread.currentThread().getName() + " releaseSearcher() last reference: close");
                    info.searcher.getRealSearcher().close();
                    info.searcher = null;
                }
            }
            else // can't find searcher, just close it
            {
                // I think this is failure
                throw new RuntimeException("huups!");
                // searcher.close();
            }
        }
    }
    
    /** get for deleting documents.
     * blocks: writers until released
     */
    public IndexReader getReader() throws IOException {
        IndexReader reader = null;
        
        synchronized (this) // sync on specific index
        {
            do {
                CheckoutInfo info =
                (CheckoutInfo)WRITER_PATHS.get(path);
                if (info != null) // may already have a reader, use it
                {
                    if (info.reader != null) // yup, have a reader
                    {
                        info.checkoutCount++;
                        reader = info.reader;
                    }
                    else // not a reader, it must be a writer, wait for it to finish to try again
                    {
                        try {
                            info.wait(); // wait for info to be released
                        }
                        catch (InterruptedException e) {
                            // TODO: Will this ever happen?
                            e.printStackTrace();
                            return null;
                        }
                    }
                }
                else // no reader, create one
                {
                    reader = IndexReader.open(path);
                    info = new CheckoutInfo(reader);
                }
            }
            while (reader == null);
        }
        return reader;
    }
    
    public void releaseReader(IndexReader reader) throws
    IOException {
        
        synchronized (this) {
            CheckoutInfo info = (CheckoutInfo)WRITER_PATHS.get(path);
            if (info != null && reader == info.reader) // reader was checked  out
            {
                if (info.checkoutCount > 1) // reader has other references
                {
                    info.checkoutCount--;
                    reader = null; // avoid close()
                }
                else // last reference to reader
                {
                    WRITER_PATHS.remove(path);
                    reader = info.reader;
                    info.notify(); // notify waiters to try again
                }
            }
        }
        // close the reader (unless it still has checkouts)
        if (reader != null) reader.close();
    }
    
    /** used for updates to make sure nobody else grabs a
     * writer or reader
     * between
     *  release and get operations.
     */
    public IndexWriter releaseReaderAndGetWriter(IndexReader reader) throws IOException {
        
        synchronized (this) {
            releaseReader(reader);
            return getWriter();
        }
    }
    
    
    private static class CheckoutInfo {
        CheckoutInfo(IndexWriter writer) {
            this.writer = writer;
        }
        CheckoutInfo(IndexReader reader) {
            this.reader = reader;
        }
        CheckoutInfo(ManagedSearcher searcher) {
            this.searcher = searcher;
            this.creationTime = System.currentTimeMillis();
        }
        
        public IndexReader reader;
        public IndexWriter writer;
        public ManagedSearcher searcher;
        public int checkoutCount = 1;
        public long creationTime;
    }
    
}

