/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.search.impl.lucene;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.alfresco.error.AlfrescoRuntimeException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.FilterIndexReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.index.TermPositions;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.OpenBitSet;


/**
 * An index reader that filters documents from another.
 * 
 * @author andyh
 *
 */        
public class FilterIndexReaderByStringId extends FilterIndexReader
{
    private static Log s_logger = LogFactory.getLog(FilterIndexReaderByStringId.class);

    private OpenBitSet deletedDocuments;
    private final Set<String> deletions;
    private final Set<String> containerDeletions;
    private final boolean deleteNodesOnly;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    private final String id;

    /**
     * Apply the filter
     * 
     * @param id String
     * @param reader IndexReader
     * @param deleteNodesOnly boolean
     */
    public FilterIndexReaderByStringId(String id, IndexReader reader, Set<String> deletions, Set<String> containerDeletions, boolean deleteNodesOnly)
    {
        super(reader);
        reader.incRef();
        this.id = id;
        this.deletions = deletions;
        this.containerDeletions = containerDeletions;
        this.deleteNodesOnly = deleteNodesOnly;
        
        if (s_logger.isDebugEnabled())
        {
            s_logger.debug("Applying deletions FOR "+id +" (the index ito which these are applied is the previous one ...)");
        }

    }
    
    public OpenBitSet getDeletedDocuments()
    {
        lock.readLock().lock();
        try
        {
            if (deletedDocuments != null)
            {
                return deletedDocuments;
            }
        }
        finally
        {
            lock.readLock().unlock();
        }
        lock.writeLock().lock();
        try
        {
            if (deletedDocuments != null)
            {
                return deletedDocuments;
            }
            deletedDocuments = new OpenBitSet(in.maxDoc());

            Searcher searcher = new IndexSearcher(in);
            for (String stringRef : deletions)
            {
                if (!deleteNodesOnly || containerDeletions.contains(stringRef))
                {
                    TermDocs td = in.termDocs(new Term("ID", stringRef));
                    while (td.next())
                    {
                        deletedDocuments.set(td.doc());
                    }
                    td.close();                        
                }
                else
                {
                    boolean found = false;
                    TermDocs td = in.termDocs(new Term("LEAFID", stringRef));
                    while (td.next())
                    {
                        deletedDocuments.set(td.doc());
                        found = true;
                    }
                    td.close();
                    // For backward compatibility, use old method of locating non-container docs
                    if (!found)
                    {
                        TermQuery query = new TermQuery(new Term("ID", stringRef));
                        Hits hits = searcher.search(query);
                        if (hits.length() > 0)
                        {
                            for (int i = 0; i < hits.length(); i++)
                            {
                                Document doc = hits.doc(i);
                                // Exclude all containers except the root (which is also a node!)
                                Field path = doc.getField("PATH");
                                if (path == null || path.stringValue().length() == 0)
                                {
                                    deletedDocuments.set(hits.id(i));
                                    // There should only be one thing to delete
                                    // break;
                                }
                            }
                        }
                    }
                }
            }
            // searcher does not need to be closed, the reader is live 

            for (String stringRef : containerDeletions)
            {
                TermDocs td = in.termDocs(new Term("ANCESTOR", stringRef));
                while (td.next())
                {
                    deletedDocuments.set(td.doc());
                }
                td.close();
            }
            return deletedDocuments;
        }
        catch (IOException e)
        {
            s_logger.error("Error initialising "+id, e);
            throw new AlfrescoRuntimeException("Failed to find deleted documents to filter", e);
        }
        finally
        {
            lock.writeLock().unlock();
        }
        
    }

    // Prevent from actually setting the closed flag
    @Override
    protected void doClose() throws IOException
    {
        this.in.decRef();
    }        

    /**
     * Filter implementation
     * 
     * @author andyh
     *
     */
    public class FilterTermDocs implements TermDocs
    {
        protected TermDocs in;
        
        String id;

        /**
         * @param id String
         * @param in TermDocs
         */
        public FilterTermDocs(String id, TermDocs in)
        {
            this.in = in;
        }

        public void seek(Term term) throws IOException
        {
            // Seek is left to the base implementation
            in.seek(term);
        }

        public void seek(TermEnum termEnum) throws IOException
        {
            // Seek is left to the base implementation
            in.seek(termEnum);
        }

        public int doc()
        {
            // The current document info is valid in the base implementation
            return in.doc();
        }

        public int freq()
        {
            // The frequency is valid in the base implementation
            return in.freq();
        }

        public boolean next() throws IOException
        {
        	try
        	{
        	    if (!in.next())
        	    {
        	        return false;
        	    }
        	    OpenBitSet deletedDocuments = getDeletedDocuments();
        	    while (deletedDocuments.get(in.doc()))
        	    {
                    if (!in.next())
                    {
                        return false;
                    }        	        
        	    }
                // Not masked
                return true;
        	}
        	catch(IOException ioe)
        	{
        		s_logger.error("Error reading docs for "+id);
        		throw ioe;
        	}
        }

        public int read(int[] docs, int[] freqs) throws IOException
        {
            int[] innerDocs = new int[docs.length];
            int[] innerFreq = new int[docs.length];
            int count = in.read(innerDocs, innerFreq);

            // Is the stream exhausted
            if (count == 0)
            {
                return 0;
            }

            OpenBitSet deletedDocuments = getDeletedDocuments();
            while (allDeleted(innerDocs, count, deletedDocuments))
            {

                count = in.read(innerDocs, innerFreq);

                // Is the stream exhausted
                if (count == 0)
                {
                    return 0;
                }
            }

            // Add non deleted

            int insertPosition = 0;
            for (int i = 0; i < count; i++)
            {
                if (!deletedDocuments.get(innerDocs[i]))
                {
                    docs[insertPosition] = innerDocs[i];
                    freqs[insertPosition] = innerFreq[i];
                    insertPosition++;
                }
            }

            return insertPosition;
        }

        private boolean allDeleted(int[] docs, int fillSize, OpenBitSet deletedDocuments)
        {
            for (int i = 0; i < fillSize; i++)
            {
                if (!deletedDocuments.get(docs[i]))
                {
                    return false;
                }
            }
            return true;
        }

        public boolean skipTo(int i) throws IOException
        {
            if (!in.skipTo(i))
            {
                return false;
            }

            OpenBitSet deletedDocuments = getDeletedDocuments();
            while (deletedDocuments.get(in.doc()))
            {
                if (!in.next())
                {
                    return false;
                }
            }
            return true;
        }

        public void close() throws IOException
        {
            // Leave to internal implementation
            in.close();
        }
    }

    /** Base class for filtering {@code TermPositions} implementations. */
    public class FilterTermPositions extends FilterTermDocs implements TermPositions
    {

        TermPositions tp;
        
        /**
         * @param id String
         * @param in TermPositions
         */
        public FilterTermPositions(String id, TermPositions in)
        {
            super(id, in);
            tp = in;
        }

        public int nextPosition() throws IOException
        {
            return tp.nextPosition();
        }

        public byte[] getPayload(byte[] data, int offset) throws IOException
        {
            return tp.getPayload(data, offset);
        }

        public int getPayloadLength()
        {
            return tp.getPayloadLength();
        }

        public boolean isPayloadAvailable()
        {
            return tp.isPayloadAvailable();
        }
    }

    @Override
    public int numDocs()
    {
        return super.numDocs() - (int)getDeletedDocuments().cardinality();
    }

    @Override
    public TermDocs termDocs() throws IOException
    {
        return new FilterTermDocs(id, super.termDocs());
    }

    @Override
    public TermPositions termPositions() throws IOException
    {
        return new FilterTermPositions(id, super.termPositions());
    }
}
