/*
 * Copyright 2000-2010 JetBrains s.r.o.
 *
 * 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.jetbrains.idea.maven.server.embedder;

import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.ShutDownTracker;
import com.intellij.openapi.util.text.StringUtil;
import gnu.trove.THashSet;
import gnu.trove.TIntObjectHashMap;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TopDocs;
import org.apache.maven.archetype.catalog.Archetype;
import org.apache.maven.archetype.catalog.ArchetypeCatalog;
import org.apache.maven.archetype.source.ArchetypeDataSource;
import org.apache.maven.archetype.source.ArchetypeDataSourceException;
import org.apache.maven.artifact.manager.WagonManager;
import org.apache.maven.wagon.events.TransferEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.maven.model.MavenArchetype;
import org.jetbrains.idea.maven.model.MavenArtifactInfo;
import org.jetbrains.idea.maven.model.MavenId;
import org.jetbrains.idea.maven.server.*;
import org.sonatype.nexus.index.*;
import org.sonatype.nexus.index.context.IndexUtils;
import org.sonatype.nexus.index.context.IndexingContext;
import org.sonatype.nexus.index.updater.IndexUpdateRequest;
import org.sonatype.nexus.index.updater.IndexUpdater;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.*;

public class Maven2ServerIndexerImpl extends MavenRemoteObject implements MavenServerIndexer {
  private Maven2ServerEmbedderImpl myEmbedder;
  private final NexusIndexer myIndexer;
  private final IndexUpdater myUpdater;
  private final ArtifactContextProducer myArtifactContextProducer;

  private final TIntObjectHashMap<IndexingContext> myIndices = new TIntObjectHashMap<IndexingContext>();

  public Maven2ServerIndexerImpl() throws RemoteException {
    myEmbedder = Maven2ServerEmbedderImpl.create(new MavenServerSettings());

    myIndexer = myEmbedder.getComponent(NexusIndexer.class);
    myUpdater = myEmbedder.getComponent(IndexUpdater.class);
    myArtifactContextProducer = myEmbedder.getComponent(ArtifactContextProducer.class);

    ShutDownTracker.getInstance().registerShutdownTask(new Runnable() {
      @Override
      public void run() {
        release();
      }
    });
  }

  public int createIndex(@NotNull String indexId,
                         @NotNull String repositoryId,
                         @Nullable File file,
                         @Nullable String url,
                         @NotNull File indexDir) throws MavenServerIndexerException {
    try {
      IndexingContext context = myIndexer.addIndexingContextForced(indexId,
                                                                   repositoryId,
                                                                   file,
                                                                   indexDir,
                                                                   url,
                                                                   null, // repo update url
                                                                   NexusIndexer.FULL_INDEX);
      int id = System.identityHashCode(context);
      myIndices.put(id, context);
      return id;
    }
    catch (Exception e) {
      throw new MavenServerIndexerException(wrapException(e));
    }
  }

  public void releaseIndex(int id) throws MavenServerIndexerException {
    try {
      myIndexer.removeIndexingContext(getIndex(id), false);
    }
    catch (Exception e) {
      throw new MavenServerIndexerException(wrapException(e));
    }
  }

  @NotNull
  private IndexingContext getIndex(int id) {
    IndexingContext index = myIndices.get(id);
    if (index == null) throw new RuntimeException("Index not found for id: " + id);
    return index;
  }

  @Override
  public boolean indexExists(File dir) throws RemoteException {
    try {
      return IndexReader.indexExists(dir);
    }
    catch (Exception e) {
      Maven2ServerGlobals.getLogger().warn(e);
    }
    return false;
  }

  private String getRepositoryPathOrUrl(IndexingContext index) {
    File file = index.getRepository();
    return file == null ? index.getRepositoryUrl() : file.getPath();
  }

  private boolean isLocal(IndexingContext index) {
    return index.getRepository() != null;
  }

  public int getIndexCount() {
    return myIndexer.getIndexingContexts().size();
  }

  public void updateIndex(int id, MavenServerSettings settings, MavenServerProgressIndicator indicator) throws
                                                                                                        MavenServerIndexerException,
                                                                                                        MavenServerProcessCanceledException,
                                                                                                        RemoteException {
    IndexingContext index = getIndex(id);

    try {
      if (isLocal(index)) {
        File repository = index.getRepository();

        if (repository != null && repository.exists()) {
          indicator.setIndeterminate(true);
          try {
            myIndexer.scan(index, new MyScanningListener(indicator), false);
          }
          finally {
            indicator.setIndeterminate(false);
          }
        }
      }
      else {
        IndexUpdateRequest request = new IndexUpdateRequest(index);
        Maven2ServerEmbedderImpl embedder = Maven2ServerEmbedderImpl.create(settings);
        try {
          request.setResourceFetcher(new Maven2ServerIndexFetcher(index.getRepositoryId(),
                                                                  index.getRepositoryUrl(),
                                                                  embedder.getComponent(WagonManager.class),
                                                                  new TransferListenerAdapter(indicator) {
                                                                    @Override
                                                                    protected void downloadProgress(long downloaded, long total) {
                                                                      super.downloadProgress(downloaded, total);
                                                                      try {
                                                                        myIndicator.setFraction(((double)downloaded) / total);
                                                                      }
                                                                      catch (RemoteException e) {
                                                                        throw new RuntimeRemoteException(e);
                                                                      }
                                                                    }

                                                                    @Override
                                                                    public void transferCompleted(TransferEvent event) {
                                                                      super.transferCompleted(event);
                                                                      try {
                                                                        myIndicator.setText2("Processing indices...");
                                                                      }
                                                                      catch (RemoteException e) {
                                                                        throw new RuntimeRemoteException(e);
                                                                      }
                                                                    }
                                                                  }));
          myUpdater.fetchAndUpdateIndex(request);
        }
        finally {
          embedder.release();
        }
      }
    }
    catch (RuntimeRemoteException e) {
      throw e.getCause();
    }
    catch (ProcessCanceledException e) {
      throw new MavenServerProcessCanceledException();
    }
    catch (Exception e) {
      throw new MavenServerIndexerException(wrapException(e));
    }
  }

  public void processArtifacts(int indexId, MavenServerIndicesProcessor processor) throws MavenServerIndexerException {
    try {
      final int CHUNK_SIZE = 10000;

      IndexReader r = getIndex(indexId).getIndexReader();
      int total = r.numDocs();

      List<MavenId> result = new ArrayList<MavenId>(Math.min(CHUNK_SIZE, total));
      for (int i = 0; i < total; i++) {
        if (r.isDeleted(i)) continue;

        Document doc = r.document(i);
        String uinfo = doc.get(SEARCH_TERM_COORDINATES);
        if (uinfo == null) continue;
        List<String> parts = StringUtil.split(uinfo, "|");
        String groupId = parts.get(0);
        String artifactId = parts.get(1);
        String version = parts.get(2);
        if (groupId == null || artifactId == null || version == null) continue;

        result.add(new MavenId(groupId, artifactId, version));

        if (result.size() == CHUNK_SIZE) {
          processor.processArtifacts(result);
          result.clear();
        }
      }

      if (!result.isEmpty()) {
        processor.processArtifacts(result);
      }
    }
    catch (Exception e) {
      throw new MavenServerIndexerException(wrapException(e));
    }
  }

  public MavenId addArtifact(int indexId, File artifactFile) throws MavenServerIndexerException {
    try {
      IndexingContext index = getIndex(indexId);
      ArtifactContext artifactContext = myArtifactContextProducer.getArtifactContext(index, artifactFile);
      if (artifactContext == null) return null;

      addArtifact(myIndexer, index, artifactContext);

      org.sonatype.nexus.index.ArtifactInfo a = artifactContext.getArtifactInfo();
      return new MavenId(a.groupId, a.artifactId, a.version);
    }
    catch (Exception e) {
      throw new MavenServerIndexerException(wrapException(e));
    }
  }

  public static void addArtifact(NexusIndexer indexer, IndexingContext index, ArtifactContext artifactContext)
    throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    indexer.addArtifactToIndex(artifactContext, index);
    // this hack is necessary to invalidate searcher's and reader's cache (may not be required then lucene or nexus library change
    Method m = index.getClass().getDeclaredMethod("closeReaders");
    m.setAccessible(true);
    m.invoke(index);
  }

  public Set<MavenArtifactInfo> search(int indexId, Query query, int maxResult) throws MavenServerIndexerException {
    try {
      IndexingContext index = getIndex(indexId);

      TopDocs docs = null;
      try {
        BooleanQuery.setMaxClauseCount(Integer.MAX_VALUE);
        docs = index.getIndexSearcher().search(query, null, maxResult);
      }
      catch (BooleanQuery.TooManyClauses ignore) {
        // this exception occurs when too wide wildcard is used on too big data.
      }

      if (docs == null || docs.scoreDocs.length == 0) return Collections.emptySet();

      Set<MavenArtifactInfo> result = new THashSet<MavenArtifactInfo>();

      for (int i = 0; i < docs.scoreDocs.length; i++) {
        int docIndex = docs.scoreDocs[i].doc;
        Document doc = index.getIndexReader().document(docIndex);
        ArtifactInfo a = IndexUtils.constructArtifactInfo(doc, index);
        if (a == null) continue;

        a.repository = getRepositoryPathOrUrl(index);
        result.add(Maven2ModelConverter.convertArtifactInfo(a));
      }
      return result;
    }
    catch (Exception e) {
      throw new MavenServerIndexerException(wrapException(e));
    }
  }

  public Collection<MavenArchetype> getArchetypes() throws RemoteException {
    Set<MavenArchetype> result = new THashSet<MavenArchetype>();
    doCollectArchetypes("internal-catalog", result);
    doCollectArchetypes("nexus", result);
    return result;
  }

  private void doCollectArchetypes(String roleHint, Set<MavenArchetype> result) throws RemoteException {
    try {
      ArchetypeDataSource source = myEmbedder.getComponent(ArchetypeDataSource.class, roleHint);
      ArchetypeCatalog catalog = source.getArchetypeCatalog(new Properties());

      for (Archetype each : (Iterable<? extends Archetype>)catalog.getArchetypes()) {
        result.add(Maven2ModelConverter.convertArchetype(each));
      }
    }
    catch (ArchetypeDataSourceException e) {
      Maven2ServerGlobals.getLogger().warn(e);
    }
  }

  public void release() {
    try {
      myEmbedder.release();
    }
    catch (Exception e) {
      throw rethrowException(e);
    }
  }

  private static class MyScanningListener implements ArtifactScanningListener {
    private final MavenServerProgressIndicator p;

    public MyScanningListener(MavenServerProgressIndicator indicator) {
      p = indicator;
    }

    public void scanningStarted(IndexingContext ctx) {
      try {
        if (p.isCanceled()) throw new ProcessCanceledException();
      }
      catch (RemoteException e) {
        throw new RuntimeRemoteException(e);
      }
    }

    public void scanningFinished(IndexingContext ctx, ScanningResult result) {
      try {
        if (p.isCanceled()) throw new ProcessCanceledException();
      }
      catch (RemoteException e) {
        throw new RuntimeRemoteException(e);
      }
    }

    public void artifactError(ArtifactContext ac, Exception e) {
    }

    public void artifactDiscovered(ArtifactContext ac) {
      try {
        if (p.isCanceled()) throw new ProcessCanceledException();
        ArtifactInfo info = ac.getArtifactInfo();
        p.setText2(info.groupId + ":" + info.artifactId + ":" + info.version);
      }
      catch (RemoteException e) {
        throw new RuntimeRemoteException(e);
      }
    }
  }
}
