package zemberek.core.math;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class LogMath {

  public static final double LOG_ZERO = -Math.log(Double.MAX_VALUE);
  public static final double LOG_ONE = 0;
  public static final double LOG_TEN = Math.log(10);
  public static final double LOG_TWO = Math.log(2);
  public static final double INVERSE_LOG_TWO = 1 / Math.log(2);

  public static final float LOG_ZERO_FLOAT = (float) -Math.log(Float.MAX_VALUE);
  public static final float LOG_ONE_FLOAT = 0;
  public static final float LOG_TEN_FLOAT = (float) Math.log(10);
  public static final float LOG_TWO_FLOAT = (float) Math.log(2);

  // Double value log sum lookup base Math.E
  public static final LogSumLookup LOG_SUM = new LogSumLookup(Math.E);
  // Float value log sum lookup base Math.E
  public static final LogSumLookupFloat LOG_SUM_FLOAT = new LogSumLookupFloat(Math.E);

  // Double value linear to Log value converter for base Math.E
  public static final LinearToLogConverter LINEAR_TO_LOG = new LinearToLogConverter(Math.E);
  // Float value linear to Log value converter for base Math.E
  public static final LinearToLogConverterFloat LINEAR_TO_LOG_FLOAT = new LinearToLogConverterFloat(
      Math.E);
  private static final double SPHINX_4_LOG_BASE = 1.0001;
  private static final double INVERSE_LOG_SPHINX_BASE = 1 / Math.log(SPHINX_4_LOG_BASE);

  // do not allow instantiation
  private LogMath() {
  }

  /**
   * Exact calculation of log(a+b) using log(a) and log(b) with formula <p><b>log(a+b) = log(b) +
   * log(1 + exp(log(b)-log(a)))</b> where log(b)>log(a)
   *
   * @param logA logarithm of A
   * @param logB logarithm of B
   * @return approximation of log(A+B)
   */
  public static double logSum(double logA, double logB) {
    if (Double.isInfinite(logA)) {
      return logB;
    }
    if (Double.isInfinite(logB)) {
      return logA;
    }
    if (logA > logB) {
      double dif = logA - logB;
      return dif >= 30d ? logA : logA + Math.log(1 + Math.exp(-dif));
    } else {
      double dif = logB - logA;
      return dif >= 30d ? logB : logB + Math.log(1 + Math.exp(-dif));
    }
  }

  /**
   * Exact calculation of log10(a+b) using log10(a) and log10(b) with formula <p><b>log10(a+b) =
   * log10(b) + log10(1 + 10^(log(b)-log(a)))</b> where log(b)>log(a)
   *
   * @param log10A 10 base logarithm of A
   * @param log10B 10 base logarithm of B
   * @return approximation of log(A+B)
   */
  public static double logSum10(double log10A, double log10B) {
    if (Double.isInfinite(log10A)) {
      return log10B;
    }
    if (Double.isInfinite(log10B)) {
      return log10A;
    }
    if (log10A > log10B) {
      double dif = log10A - log10B;
      return dif >= 30d ? log10A : log10A + Math.log10(1 + Math.pow(10, -dif));
    } else {
      double dif = log10B - log10A;
      return dif >= 30d ? log10B : log10B + Math.log10(1 + Math.pow(10, -dif));
    }
  }

  /**
   * Calculates exact logSum of log values using the <code> logSum(logA,logB) </code>
   *
   * @param logValues log values to use in logSum calculation.
   * @return </p>log(a+b) value approximation
   */
  public static double logSum(double... logValues) {
    double result = LOG_ZERO;
    for (double logValue : logValues) {
      result = logSum(result, logValue);
    }
    return result;
  }

  /**
   * Calculates logarithm in any base.
   */
  public static double log(double base, double val) {
    return Math.log(val) / Math.log(base);
  }

  /**
   * Calculates 2 base logarithm
   *
   * @param input value to calculate log
   * @return 2 base logarithm of the input
   */
  public static double log2(double input) {
    return Math.log(input) * INVERSE_LOG_TWO;
  }

  /**
   * convert a value which is in log10 base to Log base.
   *
   * @param log10Value loog10 value.
   * @return loge values
   */
  public static double log10ToLog(double log10Value) {
    return log10Value * LOG_TEN;
  }

  /**
   * Converts a log value to Sphinx4 log base. Can be used for comparison.
   *
   * @param logValue value in natural logarithm
   * @return value in Sphinx4 log base.
   */
  public static double toLogSphinx(double logValue) {
    return logValue * INVERSE_LOG_SPHINX_BASE;
  }

  /**
   * A lookup structure for approximate logSum calculation.
   */
  public static class LogSumLookupFloat {

    public static final float DEFAULT_SCALE = 1000f;
    public static final int DEFAULT_LOOKUP_SIZE = 5000;
    public final float scale;
    private final float[] lookup;

    public LogSumLookupFloat(double base) {
      this(base, DEFAULT_LOOKUP_SIZE, DEFAULT_SCALE);
    }

    public LogSumLookupFloat(double base, int lookupSize, float scale) {
      this.scale = scale;
      this.lookup = new float[lookupSize];
      for (int i = 0; i < lookup.length; i++) {
        lookup[i] = (float) log(base, 1.0 + Math.pow(base, (double) -i / scale));
      }
    }

    public void dump() {
      int chunk = 10;
      int start = 0;
      int end = chunk;
      System.out.println("{");
      while(end<lookup.length) {
        List<String> line = new ArrayList<>();

        for(int i = start; i<end; i++) {
          line.add(String.format(Locale.ENGLISH, "%.6f", lookup[i]));
        }

        String join = String.join(", ", line);
        if(end<lookup.length) {
          System.out.println(join + ",");
        } else {
          System.out.println(join);
        }

        start = end;
        end = start + chunk;
        if(end >= lookup.length) {
          end = lookup.length;
        }

      }
        System.out.println("};");

    }

    /**
     * Calculates an approximation of log(a+b) when log(a) and log(b) are given using the formula
     * <p><b>log(a+b) = log(b) + log(1 + exp(log(a)-log(b)))</b> where log(b)>log(a) <p>This method
     * is an approximation because it uses a lookup table for <b>log(1 + exp(log(b)-log(a)))</b>
     * part <p>This is useful for log-probabilities where values vary between -30 < log(p) <= 0
     * <p>if difference between values is larger than 20 (which means sum of the numbers will be
     * very close to the larger value in linear domain) large value is returned instead of the
     * logSum calculation because effect of the other value is negligible
     *
     * @param logA logarithm of A
     * @param logB logarithm of B
     * @return approximation of log(A+B)
     */
    public float lookup(float logA, float logB) {
      if (logA > logB) {
        final float dif =
            logA - logB; // logA-logB because during lookup calculation dif is multiplied with -1
        return dif >= 5f ? logA : logA + lookup[(int) (dif * scale)];
      } else {
        final float dif = logB - logA;
        return dif >= 5f ? logB : logB + lookup[(int) (dif * scale)];
      }
    }

    /**
     * Calculates approximate logSum of log values using the <code> logSum(logA,logB) </code>
     *
     * @param logValues log values to use in logSum calculation.
     * @return <p>log(a+b) value approximation
     */
    public float lookup(float... logValues) {
      float result = LOG_ZERO_FLOAT;
      for (float logValue : logValues) {
        result = lookup(result, logValue);
      }
      return result;
    }
  }

  /**
   * A lookup structure for approximate logSum calculation.
   */
  public static class LogSumLookup {

    public static final double DEFAULT_SCALE = 1000d;
    public static final int DEFAULT_LOOKUP_SIZE = 20000;
    public final double scale;
    private final double[] lookup;

    public LogSumLookup(double base) {
      this(base, DEFAULT_LOOKUP_SIZE, DEFAULT_SCALE);
    }

    public LogSumLookup(double base, int lookupSize, double scale) {
      this.scale = scale;
      this.lookup = new double[lookupSize];
      for (int i = 0; i < lookup.length; i++) {
        lookup[i] = log(base, 1.0 + Math.pow(base, (double) -i / scale));
      }
    }

    /**
     * Calculates an approximation of log(a+b) when log(a) and log(b) are given using the formula
     * <p><b>log(a+b) = log(b) + log(1 + exp(log(a)-log(b)))</b> where log(b)>log(a) <p>This method
     * is an approximation because it uses a lookup table for <b>log(1 + exp(log(b)-log(a)))</b>
     * part <p>This is useful for log-probabilities where values vary between -30 < log(p) <= 0
     * <p>if difference between values is larger than 20 (which means sum of the numbers will be
     * very close to the larger value in linear domain) large value is returned instead of the
     * logSum calculation because effect of the other value is negligible
     *
     * @param logA logarithm of A
     * @param logB logarithm of B
     * @return approximation of log(A+B)
     */
    public double lookup(double logA, double logB) {
      if (logA > logB) {
        final double dif =
            logA - logB; // logA-logB because during lookup calculation dif is multiplied with -1
        return dif >= 20d ? logA : logA + lookup[(int) (dif * scale)];
      } else {
        final double dif = logB - logA;
        return dif >= 20d ? logB : logB + lookup[(int) (dif * scale)];
      }
    }

    /**
     * Calculates approximate logSum of log values using the <code> logSum(logA,logB) </code>
     *
     * @param logValues log values to use in logSum calculation.
     * @return <p>log(a+b) value approximation
     */
    public double lookup(double... logValues) {
      double result = LOG_ZERO;
      for (double logValue : logValues) {
        result = lookup(result, logValue);
      }
      return result;
    }
  }

  /**
   * A converter class for converting linear values log values.
   */
  public static class LinearToLogConverter {

    public final double inverseLogBase;

    public LinearToLogConverter(double base) {
      if (base == 0) {
        throw new IllegalArgumentException("Base of the logarithm cannot be zero.");
      }
      this.inverseLogBase = (float) (1 / Math.log(base));
    }

    public double convert(double linear) {
      return Math.log(linear) * inverseLogBase;
    }

    public double[] convert(double... linear) {
      double[] result = new double[linear.length];
      for (int i = 0; i < linear.length; i++) {
        result[i] = convert(linear[i]);
      }
      return result;
    }

    public void convertInPlace(double... linear) {
      for (int i = 0; i < linear.length; i++) {
        linear[i] = convert(linear[i]);
      }
    }
  }

  /**
   * A converter class for converting linear values log values.
   */
  public static class LinearToLogConverterFloat {

    public final float inverseLogOfBase;

    public LinearToLogConverterFloat(double base) {
      if (base == 0) {
        throw new IllegalArgumentException("Base of the logarithm cannot be zero.");
      }
      this.inverseLogOfBase = (float) (1 / Math.log(base));
    }

    public float convert(float linear) {
      return (float) Math.log(linear) * inverseLogOfBase;
    }

    public float[] convert(float... linear) {
      float[] result = new float[linear.length];
      for (int i = 0; i < linear.length; i++) {
        result[i] = convert(linear[i]);
      }
      return result;
    }

    public void convertInPlace(float... linear) {
      for (int i = 0; i < linear.length; i++) {
        linear[i] = convert(linear[i]);
      }
    }
  }

    public static void main(String[] args) {
        LOG_SUM_FLOAT.dump();
    }
}
