/*
 * 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.solr.update;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.util.Bits;
import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.Hash;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrCore;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.util.RTimer;
import org.apache.solr.util.RefCounted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** @lucene.internal */
public class IndexFingerprint implements MapSerializable {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  private long maxVersionSpecified;
  private long maxVersionEncountered;
  // this actually means max versions used in computing the hash.
  // we cannot change this now because it changes back-compat
  private long maxInHash;
  private long versionsHash;
  private long numVersions;
  private long numDocs;
  private long maxDoc;

  public IndexFingerprint() {
    // default constructor
  }
  
  public IndexFingerprint (long maxVersionSpecified)  {
    this.maxVersionSpecified = maxVersionSpecified;
  }
  
  public long getMaxVersionSpecified() {
    return maxVersionSpecified;
  }

  public long getMaxVersionEncountered() {
    return maxVersionEncountered;
  }

  public long getMaxInHash() {
    return maxInHash;
  }

  public long getVersionsHash() {
    return versionsHash;
  }

  public long getNumVersions() {
    return numVersions;
  }

  public long getNumDocs() {
    return numDocs;
  }

  public long getMaxDoc() {
    return maxDoc;
  }

  /** Opens a new realtime searcher and returns it's (possibly cached) fingerprint */
  public static IndexFingerprint getFingerprint(SolrCore core, long maxVersion) throws IOException {
    RTimer timer = new RTimer();
    core.getUpdateHandler().getUpdateLog().openRealtimeSearcher();
    RefCounted<SolrIndexSearcher> newestSearcher = core.getUpdateHandler().getUpdateLog().uhandler.core.getRealtimeSearcher();
    try {
      IndexFingerprint f = newestSearcher.get().getIndexFingerprint(maxVersion);
      final double duration = timer.stop();
      log.info("IndexFingerprint millis:{} result:{}",duration, f);
      return f;
    } finally {
      if (newestSearcher != null) {
        newestSearcher.decref();
      }
    }
  }
  
  @SuppressWarnings({"unchecked"})
  public static IndexFingerprint getFingerprint(SolrIndexSearcher searcher, LeafReaderContext ctx, Long maxVersion)
      throws IOException {
    SchemaField versionField = VersionInfo.getAndCheckVersionField(searcher.getSchema());
    ValueSource vs = versionField.getType().getValueSource(versionField, null);
    @SuppressWarnings({"rawtypes"})
    Map funcContext = ValueSource.newContext(searcher);
    vs.createWeight(funcContext, searcher);
    
    IndexFingerprint f = new IndexFingerprint();
    f.maxVersionSpecified = maxVersion;
    f.maxDoc = ctx.reader().maxDoc();
    f.numDocs = ctx.reader().numDocs();
    
    int maxDoc = ctx.reader().maxDoc();
    Bits liveDocs = ctx.reader().getLiveDocs();
    FunctionValues fv = vs.getValues(funcContext, ctx);
    for (int doc = 0; doc < maxDoc; doc++) {
      if (liveDocs != null && !liveDocs.get(doc)) continue;
      long v = fv.longVal(doc);
      f.maxVersionEncountered = Math.max(v, f.maxVersionEncountered);
      if (v <= f.maxVersionSpecified) {
        f.maxInHash = Math.max(v, f.maxInHash);
        f.versionsHash += Hash.fmix64(v);
        f.numVersions++;
      }
    }
    
    return f;
  }
  
  
  public static IndexFingerprint reduce(IndexFingerprint acc, IndexFingerprint f2) {
    // acc should have maxVersionSpecified already set in it using IndexFingerprint(long maxVersionSpecified) constructor
    acc.maxDoc = Math.max(acc.maxDoc, f2.maxDoc);
    acc.numDocs += f2.numDocs;
    acc.maxVersionEncountered = Math.max(acc.maxVersionEncountered, f2.maxVersionEncountered);
    acc.maxInHash = Math.max(acc.maxInHash, f2.maxInHash);
    acc.versionsHash += f2.versionsHash;
    acc.numVersions += f2.numVersions;

    return acc;
  }

  /** returns 0 for equal, negative if f1 is less recent than f2, positive if more recent */
  public static int compare(IndexFingerprint f1, IndexFingerprint f2) {
    int cmp;

    // NOTE: some way want number of docs in index to take precedence over highest version (add-only systems for sure)

    // if we're comparing all of the versions in the index, then go by the highest encountered.
    if (f1.maxVersionSpecified == Long.MAX_VALUE) {
      cmp = Long.compare(f1.maxVersionEncountered, f2.maxVersionEncountered);
      if (cmp != 0) return cmp;
    }

    // Go by the highest version under the requested max.
    cmp = Long.compare(f1.maxInHash, f2.maxInHash);
    if (cmp != 0) return cmp;

    // go by who has the most documents in the index
    cmp = Long.compare(f1.numVersions, f2.numVersions);
    if (cmp != 0) return cmp;

    // both have same number of documents, so go by hash
    cmp = Long.compare(f1.versionsHash, f2.versionsHash);
    return cmp;
  }

  @Override
  public Map<String, Object> toMap(Map<String, Object> map) {
    map.put("maxVersionSpecified", maxVersionSpecified);
    map.put("maxVersionEncountered", maxVersionEncountered);
    map.put("maxInHash", maxInHash);
    map.put("versionsHash", versionsHash);
    map.put("numVersions", numVersions);
    map.put("numDocs", numDocs);
    map.put("maxDoc", maxDoc);
    return map;
  }

  private static long getLong(@SuppressWarnings({"rawtypes"})Map m, String key, long def) {
    Object oval = m.get(key);
    return oval != null ? ((Number)oval).longValue() : def;
  }

  /**
   * Create an IndexFingerprint object from a deserialized generic object (Map or NamedList)
   */
  public static IndexFingerprint fromObject(Object o) {
    if (o instanceof IndexFingerprint) return (IndexFingerprint) o;
    @SuppressWarnings({"rawtypes"})
    Map map = null;
    if (o instanceof Map) {
      map = (Map) o;
    } else if (o instanceof NamedList) {
      map = ((NamedList) o).asShallowMap();
    } else {
      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown type " + o);
    }
    IndexFingerprint f = new IndexFingerprint();
    f.maxVersionSpecified = getLong(map, "maxVersionSpecified", Long.MAX_VALUE);
    f.maxVersionEncountered = getLong(map, "maxVersionEncountered", -1);
    f.maxInHash = getLong(map, "maxInHash", -1);
    f.versionsHash = getLong(map, "versionsHash", -1);
    f.numVersions = getLong(map, "numVersions", -1);
    f.numDocs = getLong(map, "numDocs", -1);
    f.maxDoc = getLong(map, "maxDoc", -1);
    return f;
  }

  @Override
  public String toString() {
    return toMap(new LinkedHashMap<>()).toString();
  }

}