/* $Id$ */

/**
* 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.manifoldcf.crawler.reprioritizationtracker;

import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.crawler.interfaces.*;
import org.apache.manifoldcf.crawler.system.Logging;

import java.nio.charset.StandardCharsets;
import java.util.*;

/** This class tracks cluster-wide reprioritization operations.
* These operations are driven forward by whatever thread needs them,
* and are completed if those processes die by the threads that clean up
* after the original process.
*/
public class ReprioritizationTracker implements IReprioritizationTracker
{
  public static final String _rcsid = "@(#)$Id$";

  protected final static String trackerWriteLock = "_REPR_TRACKER_LOCK_";
  protected final static String trackerProcessIDResource = "_REPR_TRACKER_PID_";
  protected final static String trackerReproIDResource = "_REPR_TRACKER_RID_";
  protected final static String trackerMinimumDepthResource = "_REPR_MINDEPTH_";
  
  /** Lock manager */
  protected final ILockManager lockManager;
  protected final IBinManager binManager;

  /** Preload requests */
  protected final Map<PreloadKey,PreloadRequest> preloadRequests = new HashMap<PreloadKey,PreloadRequest>();
  /** Preload values */
  protected final Map<PreloadKey,PreloadedValues> preloadedValues = new HashMap<PreloadKey,PreloadedValues>();
    
  /** Constructor.
  */
  public ReprioritizationTracker(IThreadContext threadContext)
    throws ManifoldCFException
  {
    lockManager = LockManagerFactory.make(threadContext);
    binManager = BinManagerFactory.make(threadContext);
  }
  
  /** Start a reprioritization activity.
  *@param processID is the process ID of the process performing/waiting for the prioritization
  * to complete.
  *@param reproID is the reprocessing thread ID
  */
  @Override
  public void startReprioritization(String processID, String reproID)
    throws ManifoldCFException
  {
    lockManager.enterWriteLock(trackerWriteLock);
    try
    {
      String currentProcessID = readProcessID();
      if (currentProcessID != null)
      {
        // Already a reprioritization in progress.
        return;
      }
      writeProcessID(processID);
      writeReproID(reproID);
      try
      {
        binManager.reset();
      }
      catch (Throwable e)
      {
        writeProcessID(null);
        writeReproID(null);
        if (e instanceof Error)
          throw (Error)e;
        else if (e instanceof RuntimeException)
          throw (RuntimeException)e;
        else if (e instanceof ManifoldCFException)
          throw (ManifoldCFException)e;
        else
          throw new RuntimeException("Unknown exception: "+e.getClass().getName()+": "+e.getMessage(),e);
      }
      writeMinimumDepth(0.0);
    }
    finally
    {
      lockManager.leaveWriteLock(trackerWriteLock);
    }
  }
  
  
  /** Complete a reprioritization activity.  Prioritization will be marked as complete
  * only if the processID matches the one that started the current reprioritization.
  *@param reproID is the process ID of the process completing the prioritization.
  */
  @Override
  public void doneReprioritization(String reproID)
    throws ManifoldCFException
  {
    lockManager.enterWriteLock(trackerWriteLock);
    try
    {
      String currentProcessID = readProcessID();
      String currentReproID = readReproID();
      if (currentProcessID != null && currentReproID != null && currentReproID.equals(reproID))
      {
        // Null out the fields
        writeProcessID(null);
        writeReproID(null);
      }
    }
    finally
    {
      lockManager.leaveWriteLock(trackerWriteLock);
    }
  }
  
  /** Check if the specified processID is the one performing reprioritization.
  *@param processID is the process ID to check.
  *@return the repro ID if the processID is confirmed to be the one.
  */
  @Override
  public String isSpecifiedProcessReprioritizing(String processID)
    throws ManifoldCFException
  {
    lockManager.enterWriteLock(trackerWriteLock);
    try
    {
      String currentProcessID = readProcessID();
      String currentReproID = readReproID();
      if (currentProcessID != null && currentReproID != null && currentProcessID.equals(processID))
        return currentReproID;
      return null;
    }
    finally
    {
      lockManager.leaveWriteLock(trackerWriteLock);
    }
  }
  
  /** Assess the current minimum depth.
  * This method is called to provide information about the priorities of the documents being currently
  * queued.  It is the case that it is unoptimal to assign document priorities that are fundamentally higher than this value,
  * because then the new documents will be preferentially queued, and the goal of distributing documents across bins will not be
  * adequately met.
  *@param binNamesSet is the current set of priorities we see on the queuing operation.
  */
  @Override
  public void assessMinimumDepth(Double[] binNamesSet)
    throws ManifoldCFException
  {
    double newMinPriority = Double.MAX_VALUE;
    for (Double binValue : binNamesSet)
    {
      if (binValue.doubleValue() < newMinPriority)
        newMinPriority = binValue.doubleValue();
    }

    if (newMinPriority != Double.MAX_VALUE)
    {

      lockManager.enterWriteLock(trackerWriteLock);
      try
      {
        String processID = readProcessID();
        if (processID == null)
        {
          double currentMinimumDepth = readMinimumDepth();

          // Convert minPriority to minDepth.
          // Note that this calculation does not take into account anything having to do with connection rates, throttling,
          // or other adjustment factors.  It allows us only to obtain the "raw" minimum depth: the depth without any
          // adjustments.
          double newMinDepth = Math.exp(newMinPriority)-1.0;

          if (newMinDepth > currentMinimumDepth)
          {
            writeMinimumDepth(newMinDepth);
            if (Logging.scheduling.isDebugEnabled())
              Logging.scheduling.debug("Setting new minimum depth value to "+new Double(currentMinimumDepth).toString());
          }
          else
          {
            if (newMinDepth < currentMinimumDepth && Logging.scheduling.isDebugEnabled())
              Logging.scheduling.debug("Minimum depth value seems to have been set too high too early! currentMin = "+new Double(currentMinimumDepth).toString()+"; queue value = "+new Double(newMinDepth).toString());
          }
        }
      }
      finally
      {
        lockManager.leaveWriteLock(trackerWriteLock);
      }
    }
  }

  /** Retrieve current minimum depth.
  *@return the current minimum depth to use.
  */
  @Override
  public double getMinimumDepth()
    throws ManifoldCFException
  {
    lockManager.enterReadLock(trackerWriteLock);
    try
    {
      return readMinimumDepth();
    }
    finally
    {
      lockManager.leaveReadLock(trackerWriteLock);
    }
  }
  
  /** Note preload amounts.
  */
  @Override
  public void addPreloadRequest(String connectorClass, String binName, double weightedMinimumDepth)
  {
    final PreloadKey pk = new PreloadKey(connectorClass, binName);
    PreloadRequest pr = preloadRequests.get(pk);
    if (pr == null)
    {
      pr = new PreloadRequest(weightedMinimumDepth);
      preloadRequests.put(pk,pr);
    }
    else
      pr.updateRequest(weightedMinimumDepth);
  }
  
  
  /** Preload bin values.  Call this OUTSIDE of a transaction.
  */
  @Override
  public void preloadBinValues()
    throws ManifoldCFException
  {
    for (PreloadKey pk : preloadRequests.keySet())
    {
      PreloadRequest pr = preloadRequests.get(pk);
      double[] newValues = binManager.getIncrementBinValuesInTransaction(pk.connectorClass, pk.binName, pr.getWeightedMinimumDepth(), pr.getRequestCount());
      PreloadedValues pv = new PreloadedValues(newValues);
      preloadedValues.put(pk,pv);
    }
    preloadRequests.clear();
  }
  
  /** Clear any preload requests.
  */
  @Override
  public void clearPreloadRequests()
  {
    preloadRequests.clear();
  }
  
  /** Clear remaining preloaded values.
  */
  @Override
  public void clearPreloadedValues()
  {
    preloadedValues.clear();
  }

  /** Get a bin value.
  *@param connectorClass is the connector class name.
  *@param binName is the bin name.
  *@param weightedMinimumDepth is the minimum depth to use.
  *@return the bin value.
  */
  @Override
  public double getIncrementBinValue(String connectorClass, String binName, double weightedMinimumDepth)
    throws ManifoldCFException
  {
    final PreloadKey key = new PreloadKey(connectorClass,binName);
    PreloadedValues pv = preloadedValues.get(key);
    if (pv != null)
    {
      Double rval = pv.getNextValue();
      if (rval != null)
        return rval.doubleValue();
    }
    return binManager.getIncrementBinValues(connectorClass, binName, weightedMinimumDepth,1)[0];
  }
  
  // Protected methods
  
  /** Read process ID.
  *@return processID, or null if none.
  */
  protected String readProcessID()
    throws ManifoldCFException
  {
    byte[] processIDData = lockManager.readData(trackerProcessIDResource);
    if (processIDData == null)
      return null;
    else
      return new String(processIDData, StandardCharsets.UTF_8);

  }
  
  /** Write process ID.
  *@param processID is the process ID to write.
  */
  protected void writeProcessID(String processID)
    throws ManifoldCFException
  {
    if (processID == null)
      lockManager.writeData(trackerProcessIDResource, null);
    else
    {
        byte[] processIDData = processID.getBytes(StandardCharsets.UTF_8);
        lockManager.writeData(trackerProcessIDResource, processIDData);
    }
  }

  /** Read repriotization ID.
  *@return reproID, or null if none.
  */
  protected String readReproID()
    throws ManifoldCFException
  {
    byte[] reproIDData = lockManager.readData(trackerReproIDResource);
    if (reproIDData == null)
      return null;
    else
      return new String(reproIDData, StandardCharsets.UTF_8);

  }
  
  /** Write repro ID.
  *@param reproID is the repro ID to write.
  */
  protected void writeReproID(String reproID)
    throws ManifoldCFException
  {
    if (reproID == null)
      lockManager.writeData(trackerReproIDResource, null);
    else
    {
        byte[] reproIDData = reproID.getBytes(StandardCharsets.UTF_8);
        lockManager.writeData(trackerReproIDResource, reproIDData);
    }
  }

  /** Read minimum depth.
  *@return the minimum depth.
  */
  protected double readMinimumDepth()
    throws ManifoldCFException
  {
    byte[] data = lockManager.readData(trackerMinimumDepthResource);
    if (data == null || data.length != 8)
      return 0.0;
    long dataLong = (((long)data[0]) & 0xffL) +
      ((((long)data[1]) << 8) & 0xff00L) +
      ((((long)data[2]) << 16) & 0xff0000L) +
      ((((long)data[3]) << 24) & 0xff000000L) +
      ((((long)data[4]) << 32) & 0xff00000000L) +
      ((((long)data[5]) << 40) & 0xff0000000000L) +
      ((((long)data[6]) << 48) & 0xff000000000000L) +
      ((((long)data[7]) << 56) & 0xff00000000000000L);

    return Double.longBitsToDouble(dataLong);
  }
  
  /** Write minimum depth.
  *@param depth the minimum depth.
  */
  protected void writeMinimumDepth(double depth)
    throws ManifoldCFException
  {
    long dataLong = Double.doubleToLongBits(depth);
    byte[] data = new byte[8];
    data[0] = (byte)(dataLong & 0xffL);
    data[1] = (byte)((dataLong >> 8) & 0xffL);
    data[2] = (byte)((dataLong >> 16) & 0xffL);
    data[3] = (byte)((dataLong >> 24) & 0xffL);
    data[4] = (byte)((dataLong >> 32) & 0xffL);
    data[5] = (byte)((dataLong >> 40) & 0xffL);
    data[6] = (byte)((dataLong >> 48) & 0xffL);
    data[7] = (byte)((dataLong >> 56) & 0xffL);
    lockManager.writeData(trackerMinimumDepthResource,data);
  }
  
  /** A preload request */
  protected static class PreloadRequest
  {
    protected double weightedMinimumDepth;
    protected int requestCount;
    
    public PreloadRequest(double weightedMinimumDepth)
    {
      this.weightedMinimumDepth = weightedMinimumDepth;
      this.requestCount = 1;
    }
    
    public void updateRequest(double weightedMinimumDepth)
    {
      if (this.weightedMinimumDepth < weightedMinimumDepth)
        this.weightedMinimumDepth = weightedMinimumDepth;
      requestCount++;
    }
    
    public double getWeightedMinimumDepth()
    {
      return weightedMinimumDepth;
    }
    
    public int getRequestCount()
    {
      return requestCount;
    }
  }
  
  /** A set of preloaded values */
  protected static class PreloadedValues
  {
    protected double[] values;
    protected int valueIndex;
    
    public PreloadedValues(double[] values)
    {
      this.values = values;
      this.valueIndex = valueIndex;
    }
    
    public Double getNextValue()
    {
      if (valueIndex == values.length)
        return null;
      return new Double(values[valueIndex++]);
    }
  }
  
  /** Connector class name, bin name pair */
  protected static class PreloadKey
  {
    public final String connectorClass;
    public final String binName;
    
    public PreloadKey(final String connectorClass, final String binName) {
      this.connectorClass = connectorClass;
      this.binName = binName;
    }
    
    public int hashCode() {
      return (connectorClass == null ? 0 : connectorClass.hashCode()) + (binName == null ? 0 : binName.hashCode());
    }
    
    public boolean equals(final Object o) {
      if (!(o instanceof PreloadKey))
        return false;
      final PreloadKey pk = (PreloadKey)o;
      return connectorClass.equals(pk.connectorClass) &&
        (binName == null ? "" : binName).equals(pk.binName);
    }
  }
  
}

