/*
 * Copyright (c) 2010 Yahoo! Inc. All rights reserved.
 * 
 * Licensed 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. See accompanying LICENSE file.
 */

package voldemort.performance.benchmark.generator;

import java.util.Random;

/**
 * A generator of a Zipfian distribution. It produces a sequence of items, such
 * that some items are more popular than others, according to a zipfian
 * distribution.
 * 
 * Note that the popular items will be clustered together, e.g. item 0 is the
 * most popular, item 1 the second most popular, and so on (or min is the most
 * popular, min+1 the next most popular, etc.)
 * 
 */
public class ZipfianGenerator extends IntegerGenerator {

    public static final double ZIPFIAN_CONSTANT = 0.99;

    private long items, base, countZeta;
    private double zipfianConstant, alpha, zetan, eta, theta, zeta2theta;
    private Random random;
    boolean allowItemCountDecrease = false;

    public ZipfianGenerator(long items) {
        this(0, items - 1);
    }

    public ZipfianGenerator(long min, long max) {
        this(min, max, ZIPFIAN_CONSTANT);
    }

    public ZipfianGenerator(long items, double zipfianConstant) {
        this(0, items - 1, zipfianConstant);
    }

    public ZipfianGenerator(long min, long max, double zipfianConstant) {
        this(min, max, zipfianConstant, zetaStatic(max - min + 1, zipfianConstant));
    }

    public ZipfianGenerator(long min, long max, double zipfianConstant, double zetan) {
        this.items = max - min + 1;
        this.base = min;
        this.zipfianConstant = zipfianConstant;
        this.random = new Random();
        this.theta = this.zipfianConstant;
        this.zeta2theta = zeta(2, theta);
        this.alpha = 1.0 / (1.0 - theta);
        this.zetan = zetan;
        this.countZeta = items;
        this.eta = (1 - Math.pow(2.0 / items, 1 - theta)) / (1 - zeta2theta / zetan);
        nextInt();
    }

    double zeta(long n, double theta) {
        countZeta = n;
        return zetaStatic(n, theta);
    }

    static double zetaStatic(long n, double theta) {
        return zetaStatic(0, n, theta, 0);
    }

    double zeta(long st, long n, double theta, double initialSum) {
        countZeta = n;
        return zetaStatic(st, n, theta, initialSum);
    }

    static double zetaStatic(long st, long n, double theta, double initialSum) {
        double sum = initialSum;
        for(long i = st; i < n; i++) {
            sum += 1 / (Math.pow(i + 1, theta));
        }
        return sum;
    }

    public int nextInt(int itemcount) {
        return (int) nextLong(itemcount);
    }

    public long nextLong(long itemCount) {
        // from "Quickly Generating Billion-Record Synthetic Databases"
        if(itemCount != countZeta) {

            synchronized(this) {
                if(itemCount > countZeta) {
                    zetan = zeta(countZeta, itemCount, theta, zetan);
                    eta = (1 - Math.pow(2.0 / items, 1 - theta)) / (1 - zeta2theta / zetan);
                } else if((itemCount < countZeta) && (allowItemCountDecrease)) {
                    zetan = zeta(itemCount, theta);
                    eta = (1 - Math.pow(2.0 / items, 1 - theta)) / (1 - zeta2theta / zetan);
                }
            }
        }
        double u = random.nextDouble();
        double uz = u * zetan;
        if(uz < 1.0) {
            return 0;
        }

        if(uz < 1.0 + Math.pow(0.5, theta)) {
            return 1;
        }

        long ret = base + (long) ((itemCount) * Math.pow(eta * u - eta + 1, alpha));
        setLastInt((int) ret);
        return ret;
    }

    @Override
    public int nextInt() {
        return (int) nextLong(items);
    }

    public long nextLong() {
        return nextLong(items);
    }
}
