package org.apache.archiva.indexer.search;

/*
 * 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.List;
import java.util.Map;
import java.util.Set;

import org.apache.archiva.indexer.util.SearchUtil;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.maven.archiva.configuration.ArchivaConfiguration;
import org.apache.maven.archiva.configuration.Configuration;
import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.nexus.index.ArtifactInfo;
import org.sonatype.nexus.index.FlatSearchRequest;
import org.sonatype.nexus.index.FlatSearchResponse;
import org.sonatype.nexus.index.NexusIndexer;
import org.sonatype.nexus.index.context.IndexingContext;
import org.sonatype.nexus.index.context.UnsupportedExistingLuceneIndexException;

/**
 * RepositorySearch implementation which uses the Nexus Indexer for searching.
 */
public class NexusRepositorySearch
    implements RepositorySearch
{
    private static final Logger log = LoggerFactory.getLogger( NexusRepositorySearch.class ); 
                                                              
    private NexusIndexer indexer;
    
    private ArchivaConfiguration archivaConfig;
    
    public NexusRepositorySearch( NexusIndexer indexer, ArchivaConfiguration archivaConfig )
    {
        this.indexer = indexer;
        this.archivaConfig = archivaConfig;
    }

    /**
     * @see RepositorySearch#search(String, List, String, SearchResultLimits, List)
     */
    public SearchResults search( String principal, List<String> selectedRepos, String term, SearchResultLimits limits,
                                 List<String> previousSearchTerms )
        throws RepositorySearchException
    {   
        addIndexingContexts( selectedRepos );
        
        // since upgrade to nexus 2.0.0, query has changed from g:[QUERIED TERM]* to g:*[QUERIED TERM]*
        //      resulting to more wildcard searches so we need to increase max clause count
        BooleanQuery.setMaxClauseCount( Integer.MAX_VALUE );
        BooleanQuery q = new BooleanQuery();
        
        if( previousSearchTerms == null || previousSearchTerms.isEmpty() )
        {            
            constructQuery( term, q );
        }
        else
        {   
            for( String previousTerm : previousSearchTerms )
            {
                BooleanQuery iQuery = new BooleanQuery();
                constructQuery( previousTerm, iQuery );
                
                q.add( iQuery, Occur.MUST );
            }
            
            BooleanQuery iQuery = new BooleanQuery();
            constructQuery( term, iQuery );
            q.add( iQuery, Occur.MUST );
        }      
                    
        return search( limits, q );
    }
    
    /**
     * @see RepositorySearch#search(String, SearchFields, SearchResultLimits)
     */
    public SearchResults search( String principal, SearchFields searchFields, SearchResultLimits limits )
        throws RepositorySearchException
    {
        if( searchFields.getRepositories() == null )
        {
            throw new RepositorySearchException( "Repositories cannot be null." );
        }
        
        addIndexingContexts( searchFields.getRepositories() );
        
        BooleanQuery q = new BooleanQuery();
        if( searchFields.getGroupId() != null && !"".equals( searchFields.getGroupId() ) )
        {   
            q.add( indexer.constructQuery( ArtifactInfo.GROUP_ID, searchFields.getGroupId() ), Occur.MUST );
        }
        
        if( searchFields.getArtifactId() != null && !"".equals( searchFields.getArtifactId() ) )
        {
            q.add( indexer.constructQuery( ArtifactInfo.ARTIFACT_ID, searchFields.getArtifactId() ), Occur.MUST );
        }
        
        if( searchFields.getVersion() != null && !"".equals( searchFields.getVersion() ) )
        {
            q.add( indexer.constructQuery( ArtifactInfo.VERSION, searchFields.getVersion() ), Occur.MUST );
        }
        
        if( searchFields.getPackaging() != null && !"".equals( searchFields.getPackaging() ) )
        {
            q.add( indexer.constructQuery( ArtifactInfo.PACKAGING, searchFields.getPackaging() ), Occur.MUST );
        }
        
        if( searchFields.getClassName() != null && !"".equals( searchFields.getClassName() ) )
        {
            q.add( indexer.constructQuery( ArtifactInfo.NAMES, searchFields.getClassName() ), Occur.MUST );
        }
        
        if( q.getClauses() == null || q.getClauses().length <= 0 )
        {
            throw new RepositorySearchException( "No search fields set." );
        }
        
        return search( limits, q );        
    }

    private SearchResults search( SearchResultLimits limits, BooleanQuery q )
        throws RepositorySearchException
    {
        try
        {
            FlatSearchRequest request = new FlatSearchRequest( q );
            FlatSearchResponse response = indexer.searchFlat( request );
            
            if( response == null || response.getTotalHits() == 0 )
            {
                SearchResults results = new SearchResults();
                results.setLimits( limits );
                return results;
            }
            
            return convertToSearchResults( response, limits );
        }
        catch ( IOException e )
        {
            throw new RepositorySearchException( e );
        }
        finally
        {
            Map<String, IndexingContext> indexingContexts = indexer.getIndexingContexts();
            Set<String> keys = indexingContexts.keySet();
            for( String key : keys )
            {
                try                
                {   
                    indexer.removeIndexingContext( indexingContexts.get( key ), false );
                    log.debug( "Indexing context '" + key + "' removed from search." );
                }
                catch ( IOException e )
                {
                    log.warn( "IOException occurred while removing indexing content '" + key  + "'." );
                    continue;
                }
            }            
        }
    }

    private void constructQuery( String term, BooleanQuery q )
    {
        q.add( indexer.constructQuery( ArtifactInfo.GROUP_ID, term ), Occur.SHOULD );
        q.add( indexer.constructQuery( ArtifactInfo.ARTIFACT_ID, term ), Occur.SHOULD );
        q.add( indexer.constructQuery( ArtifactInfo.VERSION, term ), Occur.SHOULD );
        q.add( indexer.constructQuery( ArtifactInfo.PACKAGING, term ), Occur.SHOULD );
        q.add( indexer.constructQuery( ArtifactInfo.NAMES, term ), Occur.SHOULD );        
    }
       
    
    private void addIndexingContexts( List<String> selectedRepos )
    {
        for( String repo : selectedRepos )
        {
            try
            {
                Configuration config = archivaConfig.getConfiguration();
                ManagedRepositoryConfiguration repoConfig = config.findManagedRepositoryById( repo );
                
                if( repoConfig != null )
                {
                    String indexDir = repoConfig.getIndexDir();
                    File indexDirectory = null;
                    if( indexDir != null && !"".equals( indexDir ) )
                    {
                        indexDirectory = new File( repoConfig.getIndexDir() );
                    }
                    else
                    {
                        indexDirectory = new File( repoConfig.getLocation(), ".indexer" );
                    }
                    
                    IndexingContext context =
                        indexer.addIndexingContext( repoConfig.getId(), repoConfig.getId(), new File( repoConfig.getLocation() ),
                                                    indexDirectory, null, null, NexusIndexer.FULL_INDEX );
                    context.setSearchable( repoConfig.isScanned() );
                }
                else
                {
                    log.warn( "Repository '" + repo + "' not found in configuration." );
                }
            }
            catch ( UnsupportedExistingLuceneIndexException e )
            {                
                log.warn( "Error accessing index of repository '" + repo + "' : " + e.getMessage() );
                continue;
            }
            catch ( IOException e )
            {                
                log.warn( "IO error occured while accessing index of repository '" + repo + "' : " + e.getMessage() );
                continue;
            }
        }
    }

    private SearchResults convertToSearchResults( FlatSearchResponse response, SearchResultLimits limits )
    {   
        SearchResults results = new SearchResults();
        Set<ArtifactInfo> artifactInfos = response.getResults();
        
        for ( ArtifactInfo artifactInfo : artifactInfos )
        {
            String id = SearchUtil.getHitId( artifactInfo.groupId, artifactInfo.artifactId );
            Map<String, SearchResultHit> hitsMap = results.getHitsMap();

            SearchResultHit hit = hitsMap.get( id );
            if ( hit != null )
            {
                hit.addVersion( artifactInfo.version );
            }
            else
            {
                hit = new SearchResultHit();
                hit.setArtifactId( artifactInfo.artifactId );
                hit.setGroupId( artifactInfo.groupId );
                // do we still need to set the repository id even though we're merging everything?
                //hit.setRepositoryId( artifactInfo.repository );
                hit.setUrl( artifactInfo.repository + "/" + artifactInfo.fname );
                if( !hit.getVersions().contains( artifactInfo.version ) )
                {
                    hit.addVersion( artifactInfo.version );
                }
            }

            results.addHit( id, hit );
        }
        
        results.setTotalHits( results.getHitsMap().size() );
        results.setLimits( limits );
        
        if( limits == null || limits.getSelectedPage() == SearchResultLimits.ALL_PAGES )
        {   
            return results;
        }
        else
        {
            return paginate( results );            
        }        
    }

    private SearchResults paginate( SearchResults results )
    {
        SearchResultLimits limits = results.getLimits();
        SearchResults paginated = new SearchResults();  
        
        int fetchCount = limits.getPageSize();
        int offset = ( limits.getSelectedPage() * limits.getPageSize() );
        
        if( fetchCount > results.getTotalHits() )
        {
            fetchCount = results.getTotalHits();
        }
        
        // Goto offset.
        if ( offset < results.getTotalHits() )
        {
            // only process if the offset is within the hit count.
            for ( int i = 0; i < fetchCount; i++ )
            {
                // Stop fetching if we are past the total # of available hits.
                if ( offset + i >= results.getHits().size() )
                {
                    break;
                }
                
                SearchResultHit hit = results.getHits().get( ( offset + i ) );
                if( hit != null )
                {
                    String id = SearchUtil.getHitId( hit.getGroupId(), hit.getArtifactId() );
                    paginated.addHit( id, hit );
                }
                else
                {
                    break;
                }
            }
        }            
        paginated.setTotalHits( results.getTotalHits() );
        paginated.setLimits( limits );
        
        return paginated;
    }
}
