/**
 * 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.
 */
package org.apache.blur.filter;

import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.blur.log.Log;
import org.apache.blur.log.LogFactory;
import org.apache.lucene.index.AtomicReader;
import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.SegmentReader;
import org.apache.lucene.search.BitsFilteredDocIdSet;
import org.apache.lucene.search.DocIdSet;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.Filter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Bits;

public class FilterCache extends Filter {

  private static final Log LOG = LogFactory.getLog(FilterCache.class);

  private final Map<Object, DocIdSet> _cache = Collections.synchronizedMap(new WeakHashMap<Object, DocIdSet>());
  private final Map<Object, Object> _lockMap = Collections.synchronizedMap(new WeakHashMap<Object, Object>());
  private final Filter _filter;
  private final String _id;
  private final AtomicLong _hits = new AtomicLong();
  private final AtomicLong _misses = new AtomicLong();

  public FilterCache(String id, Filter filter) {
    _id = id;
    _filter = filter;
  }

  @Override
  public DocIdSet getDocIdSet(AtomicReaderContext context, Bits acceptDocs) throws IOException {
    AtomicReader reader = context.reader();
    Object key = reader.getCoreCacheKey();
    DocIdSet docIdSet = _cache.get(key);
    if (docIdSet != null) {
      _hits.incrementAndGet();
      return BitsFilteredDocIdSet.wrap(docIdSet, acceptDocs);
    }
    // This will only allow a single instance be created per reader per filter
    Object lock = getLock(key);
    synchronized (lock) {
      SegmentReader segmentReader = getSegmentReader(reader);
      if (segmentReader == null) {
        LOG.warn("Could not find SegmentReader from [{0}]", reader);
        return _filter.getDocIdSet(context, acceptDocs);
      }
      Directory directory = getDirectory(segmentReader);
      if (directory == null) {
        LOG.warn("Could not find Directory from [{0}]", segmentReader);
        return _filter.getDocIdSet(context, acceptDocs);
      }
      _misses.incrementAndGet();
      String segmentName = segmentReader.getSegmentName();
      docIdSet = docIdSetToCache(_filter.getDocIdSet(context, null), reader, segmentName, directory);
      _cache.put(key, docIdSet);
      return BitsFilteredDocIdSet.wrap(docIdSet, acceptDocs);
    }
  }

  private synchronized Object getLock(Object key) {
    Object lock = _lockMap.get(key);
    if (lock == null) {
      lock = new Object();
      _lockMap.put(key, lock);
    }
    return lock;
  }

  private DocIdSet docIdSetToCache(DocIdSet docIdSet, AtomicReader reader, String segmentName, Directory directory)
      throws IOException {
    if (docIdSet == null) {
      // this is better than returning null, as the nonnull result can be cached
      return DocIdSet.EMPTY_DOCIDSET;
    } else if (docIdSet.isCacheable()) {
      return docIdSet;
    } else {
      final DocIdSetIterator it = docIdSet.iterator();
      // null is allowed to be returned by iterator(),
      // in this case we wrap with the empty set,
      // which is cacheable.
      if (it == null) {
        return DocIdSet.EMPTY_DOCIDSET;
      } else {
        final IndexFileBitSet bits = new IndexFileBitSet(reader.maxDoc(), _id, segmentName, directory);
        if (!bits.exists()) {
          bits.create(it);
        }
        bits.load();
        return bits;
      }
    }
  }

  private Directory getDirectory(SegmentReader reader) {
    return reader.directory();
  }

  private SegmentReader getSegmentReader(AtomicReader reader) {
    if (reader instanceof SegmentReader) {
      return (SegmentReader) reader;
    }
    return null;
  }

  public long getHits() {
    return _hits.get();
  }

  public long getMisses() {
    return _misses.get();
  }

  @Override
  public String toString() {
    return "FilterCache(" + _id + "," + _filter + ")";
  }
}