package org.apache.maven.index.context;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexFileNameFilter;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.maven.index.ArtifactInfo;
import org.apache.maven.index.artifact.GavCalculator;
import org.apache.maven.index.artifact.M2GavCalculator;
import org.codehaus.plexus.util.StringUtils;

/**
 * The default {@link IndexingContext} implementation.
 * 
 * @author Jason van Zyl
 * @author Tamas Cservenak
 */
public class DefaultIndexingContext
    extends AbstractIndexingContext
{
    /**
     * A standard location for indices served up by a webserver.
     */
    private static final String INDEX_DIRECTORY = ".index";

    public static final String FLD_DESCRIPTOR = "DESCRIPTOR";

    private static final String FLD_DESCRIPTOR_CONTENTS = "NexusIndex";

    private static final String FLD_IDXINFO = "IDXINFO";

    private static final String VERSION = "1.0";

    private static final Term DESCRIPTOR_TERM = new Term( FLD_DESCRIPTOR, FLD_DESCRIPTOR_CONTENTS );

    private Directory indexDirectory;

    private File indexDirectoryFile;

    private String id;

    private boolean searchable;

    private String repositoryId;

    private File repository;

    private String repositoryUrl;

    private String indexUpdateUrl;

    private NexusIndexWriter indexWriter;

    private SearcherManager searcherManager;

    private Date timestamp;

    private List<? extends IndexCreator> indexCreators;

    /**
     * Currently nexus-indexer knows only M2 reposes
     * <p>
     * XXX move this into a concrete Scanner implementation
     */
    private GavCalculator gavCalculator;

    private DefaultIndexingContext( String id,
                                    String repositoryId,
                                    File repository, //
                                    String repositoryUrl, String indexUpdateUrl,
                                    List<? extends IndexCreator> indexCreators, Directory indexDirectory,
                                    boolean reclaimIndex )
        throws ExistingLuceneIndexMismatchException, IOException
    {
        this.id = id;

        this.searchable = true;

        this.repositoryId = repositoryId;

        this.repository = repository;

        this.repositoryUrl = repositoryUrl;

        this.indexUpdateUrl = indexUpdateUrl;

        this.indexWriter = null;

        this.searcherManager = null;

        this.indexCreators = indexCreators;

        this.indexDirectory = indexDirectory;

        // eh?
        // Guice does NOT initialize these, and we have to do manually?
        // While in Plexus, all is well, but when in guice-shim,
        // these objects are still LazyHintedBeans or what not and IndexerFields are NOT registered!
        for ( IndexCreator indexCreator : indexCreators )
        {
            indexCreator.getIndexerFields();
        }

        this.gavCalculator = new M2GavCalculator();

        prepareIndex( reclaimIndex );
    }

    public DefaultIndexingContext( String id, String repositoryId, File repository, File indexDirectoryFile,
                                   String repositoryUrl, String indexUpdateUrl,
                                   List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
        throws IOException, ExistingLuceneIndexMismatchException
    {
        this( id, repositoryId, repository, repositoryUrl, indexUpdateUrl, indexCreators,
            FSDirectory.open( indexDirectoryFile ), reclaimIndex );

        this.indexDirectoryFile = indexDirectoryFile;
    }

    @Deprecated
    public DefaultIndexingContext( String id, String repositoryId, File repository, Directory indexDirectory,
                                   String repositoryUrl, String indexUpdateUrl,
                                   List<? extends IndexCreator> indexCreators, boolean reclaimIndex )
        throws IOException, ExistingLuceneIndexMismatchException
    {
        this( id, repositoryId, repository, repositoryUrl, indexUpdateUrl, indexCreators, indexDirectory, reclaimIndex );

        if ( indexDirectory instanceof FSDirectory )
        {
            this.indexDirectoryFile = ( (FSDirectory) indexDirectory ).getDirectory();
        }
    }

    public Directory getIndexDirectory()
    {
        return indexDirectory;
    }

    public File getIndexDirectoryFile()
    {
        return indexDirectoryFile;
    }

    private void prepareIndex( boolean reclaimIndex )
        throws IOException, ExistingLuceneIndexMismatchException
    {
        if ( IndexReader.indexExists( indexDirectory ) )
        {
            try
            {
                // unlock the dir forcibly
                if ( IndexWriter.isLocked( indexDirectory ) )
                {
                    IndexWriter.unlock( indexDirectory );
                }

                openAndWarmup();

                checkAndUpdateIndexDescriptor( reclaimIndex );
            }
            catch ( IOException e )
            {
                if ( reclaimIndex )
                {
                    prepareCleanIndex( true );
                }
                else
                {
                    throw e;
                }
            }
        }
        else
        {
            prepareCleanIndex( false );
        }

        timestamp = IndexUtils.getTimestamp( indexDirectory );
    }

    private void prepareCleanIndex( boolean deleteExisting )
        throws IOException
    {
        if ( deleteExisting )
        {
            closeReaders();

            // unlock the dir forcibly
            if ( IndexWriter.isLocked( indexDirectory ) )
            {
                IndexWriter.unlock( indexDirectory );
            }

            deleteIndexFiles( true );
        }

        openAndWarmup();

        if ( StringUtils.isEmpty( getRepositoryId() ) )
        {
            throw new IllegalArgumentException( "The repositoryId cannot be null when creating new repository!" );
        }

        storeDescriptor();
    }

    private void checkAndUpdateIndexDescriptor( boolean reclaimIndex )
        throws IOException, ExistingLuceneIndexMismatchException
    {
        if ( reclaimIndex )
        {
            // forcefully "reclaiming" the ownership of the index as ours
            storeDescriptor();
            return;
        }

        // check for descriptor if this is not a "virgin" index
        if ( getSize() > 0 )
        {
            final TopScoreDocCollector collector = TopScoreDocCollector.create( 1, false );
            final IndexSearcher indexSearcher = acquireIndexSearcher();
            try
            {
                indexSearcher.search( new TermQuery( DESCRIPTOR_TERM ), collector );

                if ( collector.getTotalHits() == 0 )
                {
                    throw new ExistingLuceneIndexMismatchException(
                        "The existing index has no NexusIndexer descriptor" );
                }

                if ( collector.getTotalHits() > 1 )
                {
                    // eh? this is buggy index it seems, just iron it out then
                    storeDescriptor();
                    return;
                }
                else
                {
                    // good, we have one descriptor as should
                    Document descriptor = indexSearcher.doc( collector.topDocs().scoreDocs[0].doc );
                    String[] h = StringUtils.split( descriptor.get( FLD_IDXINFO ), ArtifactInfo.FS );
                    // String version = h[0];
                    String repoId = h[1];

                    // // compare version
                    // if ( !VERSION.equals( version ) )
                    // {
                    // throw new UnsupportedExistingLuceneIndexException(
                    // "The existing index has version [" + version + "] and not [" + VERSION + "] version!" );
                    // }

                    if ( getRepositoryId() == null )
                    {
                        repositoryId = repoId;
                    }
                    else if ( !getRepositoryId().equals( repoId ) )
                    {
                        throw new ExistingLuceneIndexMismatchException( "The existing index is for repository " //
                            + "[" + repoId + "] and not for repository [" + getRepositoryId() + "]" );
                    }
                }
            }
            finally
            {
                releaseIndexSearcher( indexSearcher );
            }
        }
    }

    private void storeDescriptor()
        throws IOException
    {
        Document hdr = new Document();

        hdr.add( new Field( FLD_DESCRIPTOR, FLD_DESCRIPTOR_CONTENTS, Field.Store.YES, Field.Index.NOT_ANALYZED ) );

        hdr.add( new Field( FLD_IDXINFO, VERSION + ArtifactInfo.FS + getRepositoryId(), Field.Store.YES, Field.Index.NO ) );

        IndexWriter w = getIndexWriter();

        w.updateDocument( DESCRIPTOR_TERM, hdr );

        w.commit();
    }

    private void deleteIndexFiles( boolean full )
        throws IOException
    {
        if ( indexDirectory != null )
        {
            String[] names = indexDirectory.listAll();

            if ( names != null )
            {
                IndexFileNameFilter filter = IndexFileNameFilter.getFilter();

                for ( int i = 0; i < names.length; i++ )
                {
                    if ( filter.accept( null, names[i] ) )
                    {
                        indexDirectory.deleteFile( names[i] );
                    }
                }
            }

            if ( full )
            {
                if ( indexDirectory.fileExists( INDEX_PACKER_PROPERTIES_FILE ) )
                {
                    indexDirectory.deleteFile( INDEX_PACKER_PROPERTIES_FILE );
                }

                if ( indexDirectory.fileExists( INDEX_UPDATER_PROPERTIES_FILE ) )
                {
                    indexDirectory.deleteFile( INDEX_UPDATER_PROPERTIES_FILE );
                }
            }

            IndexUtils.deleteTimestamp( indexDirectory );
        }
    }

    // ==

    public boolean isSearchable()
    {
        return searchable;
    }

    public void setSearchable( boolean searchable )
    {
        this.searchable = searchable;
    }

    public String getId()
    {
        return id;
    }

    public void updateTimestamp()
        throws IOException
    {
        updateTimestamp( false );
    }

    public void updateTimestamp( boolean save )
        throws IOException
    {
        updateTimestamp( save, new Date() );
    }

    public void updateTimestamp( boolean save, Date timestamp )
        throws IOException
    {
        this.timestamp = timestamp;

        if ( save )
        {
            IndexUtils.updateTimestamp( indexDirectory, getTimestamp() );
        }
    }

    public Date getTimestamp()
    {
        return timestamp;
    }

    public int getSize()
        throws IOException
    {
        final IndexSearcher is = acquireIndexSearcher();
        try
        {
            return is.getIndexReader().numDocs();
        }
        finally
        {
            releaseIndexSearcher( is );
        }
    }

    public String getRepositoryId()
    {
        return repositoryId;
    }

    public File getRepository()
    {
        return repository;
    }

    public String getRepositoryUrl()
    {
        return repositoryUrl;
    }

    public String getIndexUpdateUrl()
    {
        if ( repositoryUrl != null )
        {
            if ( indexUpdateUrl == null || indexUpdateUrl.trim().length() == 0 )
            {
                return repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + INDEX_DIRECTORY;
            }
        }
        return indexUpdateUrl;
    }

    public Analyzer getAnalyzer()
    {
        return new NexusAnalyzer();
    }

    protected void openAndWarmup()
        throws IOException
    {
        // IndexWriter (close)
        if ( indexWriter != null )
        {
            indexWriter.close();

            indexWriter = null;
        }
        if ( searcherManager != null )
        {
            searcherManager.close();

            searcherManager = null;
        }

        this.indexWriter = new NexusIndexWriter( getIndexDirectory(), getWriterConfig() );
        this.indexWriter.commit(); // LUCENE-2386
        this.searcherManager = new SearcherManager( indexWriter, false, new NexusIndexSearcherFactory( this ) );
    }

    /**
     * Returns new IndexWriterConfig instance
     * 
     * @since 5.1
     */
    protected IndexWriterConfig getWriterConfig()
    {
        return NexusIndexWriter.defaultConfig();
    }

    public IndexWriter getIndexWriter()
        throws IOException
    {
        return indexWriter;
    }

    public IndexSearcher acquireIndexSearcher()
        throws IOException
    {
        // TODO: move this to separate thread to not penalty next incoming searcher
        searcherManager.maybeRefresh();
        return searcherManager.acquire();
    }

    public void releaseIndexSearcher( final IndexSearcher is )
        throws IOException
    {
        if ( is == null )
        {
            return;
        }
        searcherManager.release( is );
    }

    public void commit()
        throws IOException
    {
        getIndexWriter().commit();
    }

    public void rollback()
        throws IOException
    {
        getIndexWriter().rollback();
    }

    public synchronized void optimize()
        throws CorruptIndexException, IOException
    {
        getIndexWriter().optimize();
        commit();
    }

    public synchronized void close( boolean deleteFiles )
        throws IOException
    {
        if ( indexDirectory != null )
        {
            IndexUtils.updateTimestamp( indexDirectory, getTimestamp() );
            closeReaders();
            if ( deleteFiles )
            {
                deleteIndexFiles( true );
            }
            indexDirectory.close();
        }
        indexDirectory = null;
    }

    public synchronized void purge()
        throws IOException
    {
        closeReaders();
        deleteIndexFiles( true );
        openAndWarmup();
        try
        {
            prepareIndex( true );
        }
        catch ( ExistingLuceneIndexMismatchException e )
        {
            // just deleted it
        }
        rebuildGroups();
        updateTimestamp( true, null );
    }

    public synchronized void replace( Directory directory )
        throws IOException
    {
        final Date ts = IndexUtils.getTimestamp( directory );
        closeReaders();
        deleteIndexFiles( false );
        IndexUtils.copyDirectory( directory, indexDirectory );
        openAndWarmup();
        // reclaim the index as mine
        storeDescriptor();
        rebuildGroups();
        updateTimestamp( true, ts );
        optimize();
    }

    public synchronized void merge( Directory directory )
        throws IOException
    {
        merge( directory, null );
    }

    public synchronized void merge( Directory directory, DocumentFilter filter )
        throws IOException
    {
        final IndexSearcher s = acquireIndexSearcher();
        try
        {
            final IndexWriter w = getIndexWriter();
            final IndexReader directoryReader = IndexReader.open( directory, true );
            TopScoreDocCollector collector = null;
            try
            {
                int numDocs = directoryReader.maxDoc();
                for ( int i = 0; i < numDocs; i++ )
                {
                    if ( directoryReader.isDeleted( i ) )
                    {
                        continue;
                    }

                    Document d = directoryReader.document( i );
                    if ( filter != null && !filter.accept( d ) )
                    {
                        continue;
                    }

                    String uinfo = d.get( ArtifactInfo.UINFO );
                    if ( uinfo != null )
                    {
                        collector = TopScoreDocCollector.create( 1, false );
                        s.search( new TermQuery( new Term( ArtifactInfo.UINFO, uinfo ) ), collector );
                        if ( collector.getTotalHits() == 0 )
                        {
                            w.addDocument( IndexUtils.updateDocument( d, this, false ) );
                        }
                    }
                    else
                    {
                        String deleted = d.get( ArtifactInfo.DELETED );

                        if ( deleted != null )
                        {
                            // Deleting the document loses history that it was delete,
                            // so incrementals wont work. Therefore, put the delete
                            // document in as well
                            w.deleteDocuments( new Term( ArtifactInfo.UINFO, deleted ) );
                            w.addDocument( d );
                        }
                    }
                }

            }
            finally
            {
                directoryReader.close();
                commit();
            }

            rebuildGroups();
            Date mergedTimestamp = IndexUtils.getTimestamp( directory );

            if ( getTimestamp() != null && mergedTimestamp != null && mergedTimestamp.after( getTimestamp() ) )
            {
                // we have both, keep the newest
                updateTimestamp( true, mergedTimestamp );
            }
            else
            {
                updateTimestamp( true );
            }
            optimize();
        }
        finally
        {
            releaseIndexSearcher( s );
        }
    }

    private void closeReaders()
        throws CorruptIndexException, IOException
    {
        if ( searcherManager != null )
        {
            searcherManager.close();
            searcherManager = null;
        }
        if ( indexWriter != null )
        {
            indexWriter.close();
            indexWriter = null;
        }
    }

    public GavCalculator getGavCalculator()
    {
        return gavCalculator;
    }

    public List<IndexCreator> getIndexCreators()
    {
        return Collections.unmodifiableList( indexCreators );
    }

    // groups

    public synchronized void rebuildGroups()
        throws IOException
    {
        final IndexSearcher is = acquireIndexSearcher();
        try
        {
            final IndexReader r = is.getIndexReader();

            Set<String> rootGroups = new LinkedHashSet<String>();
            Set<String> allGroups = new LinkedHashSet<String>();

            int numDocs = r.maxDoc();

            for ( int i = 0; i < numDocs; i++ )
            {
                if ( r.isDeleted( i ) )
                {
                    continue;
                }

                Document d = r.document( i );

                String uinfo = d.get( ArtifactInfo.UINFO );

                if ( uinfo != null )
                {
                    ArtifactInfo info = IndexUtils.constructArtifactInfo( d, this );
                    rootGroups.add( info.getRootGroup() );
                    allGroups.add( info.groupId );
                }
            }

            setRootGroups( rootGroups );
            setAllGroups( allGroups );

            optimize();
        }
        finally
        {
            releaseIndexSearcher( is );
        }
    }

    public Set<String> getAllGroups()
        throws IOException
    {
        return getGroups( ArtifactInfo.ALL_GROUPS, ArtifactInfo.ALL_GROUPS_VALUE, ArtifactInfo.ALL_GROUPS_LIST );
    }

    public synchronized void setAllGroups( Collection<String> groups )
        throws IOException
    {
        setGroups( groups, ArtifactInfo.ALL_GROUPS, ArtifactInfo.ALL_GROUPS_VALUE, ArtifactInfo.ALL_GROUPS_LIST );
        commit();
    }

    public Set<String> getRootGroups()
        throws IOException
    {
        return getGroups( ArtifactInfo.ROOT_GROUPS, ArtifactInfo.ROOT_GROUPS_VALUE, ArtifactInfo.ROOT_GROUPS_LIST );
    }

    public synchronized void setRootGroups( Collection<String> groups )
        throws IOException
    {
        setGroups( groups, ArtifactInfo.ROOT_GROUPS, ArtifactInfo.ROOT_GROUPS_VALUE, ArtifactInfo.ROOT_GROUPS_LIST );
        commit();
    }

    protected Set<String> getGroups( String field, String filedValue, String listField )
        throws IOException, CorruptIndexException
    {
        final TopScoreDocCollector collector = TopScoreDocCollector.create( 1, false );
        final IndexSearcher indexSearcher = acquireIndexSearcher();
        try
        {
            indexSearcher.search( new TermQuery( new Term( field, filedValue ) ), collector );
            TopDocs topDocs = collector.topDocs();
            Set<String> groups = new LinkedHashSet<String>( Math.max( 10, topDocs.totalHits ) );
            if ( topDocs.totalHits > 0 )
            {
                Document doc = indexSearcher.doc( topDocs.scoreDocs[0].doc );
                String groupList = doc.get( listField );
                if ( groupList != null )
                {
                    groups.addAll( Arrays.asList( groupList.split( "\\|" ) ) );
                }
            }
            return groups;
        }
        finally
        {
            releaseIndexSearcher( indexSearcher );
        }
    }

    protected void setGroups( Collection<String> groups, String groupField, String groupFieldValue,
                              String groupListField )
        throws IOException, CorruptIndexException
    {
        final IndexWriter w = getIndexWriter();
        w.updateDocument( new Term( groupField, groupFieldValue ),
            createGroupsDocument( groups, groupField, groupFieldValue, groupListField ) );
    }

    protected Document createGroupsDocument( Collection<String> groups, String field, String fieldValue,
                                             String listField )
    {
        final Document groupDoc = new Document();
        groupDoc.add( new Field( field, //
            fieldValue, Field.Store.YES, Field.Index.NOT_ANALYZED ) );
        groupDoc.add( new Field( listField, //
            ArtifactInfo.lst2str( groups ), Field.Store.YES, Field.Index.NO ) );
        return groupDoc;
    }

    @Override
    public String toString()
    {
        return id + " : " + timestamp;
    }
}
