package org.apache.lucene.facet.index;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.PayloadProcessorProvider;
import org.apache.lucene.index.Term;
import org.apache.lucene.store.Directory;

import org.apache.lucene.facet.index.params.CategoryListParams;
import org.apache.lucene.facet.index.params.FacetIndexingParams;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter.OrdinalMap;
import org.apache.lucene.util.encoding.IntDecoder;
import org.apache.lucene.util.encoding.IntEncoder;

/**
 * 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.
 */

/**
 * A {@link PayloadProcessorProvider} for updating facets ordinal references,
 * based on an ordinal map. You should use this code in conjunction with merging
 * taxonomies - after you merge taxonomies, you receive an {@link OrdinalMap}
 * which maps the 'old' payloads to the 'new' ones. You can use that map to
 * re-map the payloads which contain the facets information (ordinals) either
 * before or while merging the indexes.
 * <p>
 * For re-mapping the ordinals before you merge the indexes, do the following:
 * 
 * <pre>
 * // merge the old taxonomy with the new one.
 * OrdinalMap map = LuceneTaxonomyWriter.addTaxonomies();
 * int[] ordmap = map.getMap();
 * 
 * // re-map the ordinals on the old directory.
 * Directory oldDir;
 * FacetsPayloadProcessorProvider fppp = new FacetsPayloadProcessorProvider(
 *     oldDir, ordmap);
 * IndexWriterConfig conf = new IndexWriterConfig(VER, ANALYZER);
 * conf.setMergePolicy(new ForceOptimizeMergePolicy());
 * IndexWriter writer = new IndexWriter(oldDir, conf);
 * writer.setPayloadProcessorProvider(fppp);
 * writer.forceMerge(1);
 * writer.close();
 * 
 * // merge that directory with the new index.
 * IndexWriter newWriter; // opened on the 'new' Directory
 * newWriter.addIndexes(oldDir);
 * newWriter.commit();
 * </pre>
 * 
 * For re-mapping the ordinals during index merge, do the following:
 * 
 * <pre>
 * // merge the old taxonomy with the new one.
 * OrdinalMap map = LuceneTaxonomyWriter.addTaxonomies();
 * int[] ordmap = map.getMap();
 * 
 * // Add the index and re-map ordinals on the go
 * IndexReader r = IndexReader.open(oldDir);
 * IndexWriterConfig conf = new IndexWriterConfig(VER, ANALYZER);
 * IndexWriter writer = new IndexWriter(newDir, conf);
 * writer.setPayloadProcessorProvider(fppp);
 * writer.addIndexes(r);
 * writer.commit();
 * </pre>
 * <p>
 * <b>NOTE:</b> while the second example looks simpler, IndexWriter may trigger
 * a long merge due to addIndexes. The first example avoids this perhaps
 * unneeded merge, as well as can be done separately (e.g. on another node)
 * before the index is merged.
 * 
 * @lucene.experimental
 */
public class FacetsPayloadProcessorProvider extends PayloadProcessorProvider {
  
  private final Directory workDir;
  
  private final ReaderPayloadProcessor dirProcessor;

  /**
   * Construct FacetsPayloadProcessorProvider with FacetIndexingParams
   * 
   * @param dir the {@link Directory} containing the segments to update
   * @param ordinalMap an array mapping previous facets ordinals to new ones
   * @param indexingParams the facets indexing parameters
   */
  public FacetsPayloadProcessorProvider(Directory dir, int[] ordinalMap,
                                        FacetIndexingParams indexingParams) {
    workDir = dir;
    dirProcessor = new FacetsDirPayloadProcessor(indexingParams, ordinalMap);
  }
  
  @Override
  public ReaderPayloadProcessor getReaderProcessor(IndexReader reader) throws IOException {
    return (workDir == reader.directory()) ? dirProcessor : null;
  }
  
  public static class FacetsDirPayloadProcessor extends ReaderPayloadProcessor {
    
    private final Map<Term, CategoryListParams> termMap = new HashMap<Term, CategoryListParams>(1);
    
    private final int[] ordinalMap;
    
    /**
     * Construct FacetsDirPayloadProcessor with custom FacetIndexingParams
     * @param ordinalMap an array mapping previous facets ordinals to new ones
     * @param indexingParams the facets indexing parameters
     */
    protected FacetsDirPayloadProcessor(FacetIndexingParams indexingParams, int[] ordinalMap) {
      this.ordinalMap = ordinalMap;
      for (CategoryListParams params: indexingParams.getAllCategoryListParams()) {
        termMap.put(params.getTerm(), params);
      }
    }
    
    @Override
    public PayloadProcessor getProcessor(Term term) throws IOException {
      CategoryListParams params = termMap.get(term);
      if (params == null) {
        return null;
      }
      return new FacetsPayloadProcessor(params, ordinalMap);
    }

  }
  
  /** A PayloadProcessor for updating facets ordinal references, based on an ordinal map */
  public static class FacetsPayloadProcessor extends PayloadProcessor {
    
    private final IntEncoder encoder;
    private final IntDecoder decoder;
    private final int[] ordinalMap;
    private final ByteArrayOutputStream os = new ByteArrayOutputStream();
    
    /**
     * @param params defines the encoding of facet ordinals as payload
     * @param ordinalMap an array mapping previous facets ordinals to new ones
     */
    protected FacetsPayloadProcessor(CategoryListParams params, int[] ordinalMap) {
      encoder = params.createEncoder();
      decoder = encoder.createMatchingDecoder();
      this.ordinalMap = ordinalMap;
    }
    
    @Override
    public int payloadLength() throws IOException {
      return os.size();
    }

    @Override
    public byte[] processPayload(byte[] payload, int start, int length) throws IOException {
      InputStream is = new ByteArrayInputStream(payload, start, length);
      decoder.reInit(is);
      os.reset();
      encoder.reInit(os);
      long ordinal;
      while ((ordinal = decoder.decode()) != IntDecoder.EOS) {
        int newOrdinal = ordinalMap[(int)ordinal];
        encoder.encode(newOrdinal);      
      }
      encoder.close();
      return os.toByteArray();
    }
  }
  
}
