/*
 * HyperbolicGenerator.cpp
 *
 *      Authors: Mustafa zdayi and Moritz v. Looz
 *
 * This generator contains algorithms described in two publications.
 *
 * For T=0, the relevant publication is
 * "Generating massive complex networks with hyperbolic geometry faster in practice" by
 * Moritz von Looz, Mustafa zdayi, Sren Laue and Henning Meyerhenke, presented at HPEC 2016.
 *
 * For T>0, it is
 * "Querying Probabilistic Neighborhoods in Spatial Data Sets Efficiently" by Moritz von Looz
 * and Henning Meyerhenke, presented at IWOCA 2016.
 *
 * The model of hyperbolic random graphs is presented in
 * "Hyperbolic geometry of complex networks. Physical Review E, 82:036106, Sep 2010." by
 *   Dmitri Krioukov, Fragkiskos Papadopoulos, Maksim Kitsak, Amin Vahdat, and Marian Boguna
 *
 */

#include <algorithm>
#include <omp.h>
#include <random>

#include <networkit/auxiliary/Parallel.hpp>
#include <networkit/auxiliary/Random.hpp>
#include <networkit/generators/HyperbolicGenerator.hpp>
#include <networkit/generators/quadtree/Quadtree.hpp>
#include <networkit/graph/GraphBuilder.hpp>

namespace NetworKit {

/**
 * Construct a generator for n nodes and m edges
 */
HyperbolicGenerator::HyperbolicGenerator(count n, double avgDegree, double plexp, double T) {
    nodeCount = n;
    if (plexp <= 2)
        throw std::runtime_error("Exponent of power-law degree distribution must be > 2");
    if (T < 0 || T == 1)
        // Really necessary? Graphs with T=1 can be generated, only their degree is not controllable
        throw std::runtime_error("Temperature must be non-negative and not 1.");
    if (avgDegree >= n)
        throw std::runtime_error("Average Degree must be at most n-1");
    if (T < 1) {
        alpha = 0.5 * (plexp - 1);
    } else {
        alpha = 0.5 * (plexp - 1) / T;
    }

    R = HyperbolicSpace::getTargetRadius(n, n * avgDegree / 2, alpha, T);
    temperature = T;
    initialize();
}

void HyperbolicGenerator::initialize() {
    if (temperature == 0) {
        capacity = 1000;
    } else {
        capacity = 10;
    }
    theoreticalSplit = false;
    threadtimers.resize(omp_get_max_threads());
    balance = 0.5;
}

Graph HyperbolicGenerator::generate() {
    return generate(nodeCount, R, alpha, temperature);
}

Graph HyperbolicGenerator::generate(count n, double R, double alpha, double T) {
    assert(R > 0);
    vector<double> angles(n);
    vector<double> radii(n);

    // sample points randomly
    HyperbolicSpace::fillPointsSorted(angles, radii, R, alpha);

    INFO("Generated Points");
    return generate(angles, radii, R, T);
}

Graph HyperbolicGenerator::generateCold(const vector<double> &angles, const vector<double> &radii,
                                        double R) {
    const count n = angles.size();
    assert(radii.size() == n);

    for (index i = 0; i < n; i++) {
        assert(radii[i] < R);
    }

    vector<index> permutation(n);
#pragma omp parallel for
    for (omp_index i = 0; i < static_cast<omp_index>(n); i++) {
        permutation[i] = i;
    }

    // can probably be parallelized easily, but doesn't bring much benefit
    Aux::Parallel::sort(
        permutation.begin(), permutation.end(), [&angles, &radii](index i, index j) {
            return angles[i] < angles[j] || (angles[i] == angles[j] && radii[i] < radii[j]);
        });

    vector<double> bandRadii = getBandRadii(n, R);
    // Initialize empty bands
    vector<vector<Point2DWithIndex<double>>> bands(bandRadii.size() - 1);
// Put points to bands
#pragma omp parallel for
    for (omp_index j = 0; j < static_cast<omp_index>(bands.size()); j++) {
        for (index i = 0; i < n; i++) {
            double alias = permutation[i];
            if (radii[alias] >= bandRadii[j] && radii[alias] <= bandRadii[j + 1]) {
                bands[j].emplace_back(angles[alias], radii[alias], alias);
            }
        }
    }

    const count bandCount = bands.size();
    const double coshR = std::cosh(R);
    assert(radii.size() == n);

    Aux::Timer bandTimer;
    bandTimer.start();

    // 1.Extract band angles to use them later, can create a band class to handle this more
    // elegantly
    vector<vector<double>> bandAngles(bandCount);
#pragma omp parallel for
    for (omp_index i = 0; i < static_cast<omp_index>(bandCount); i++) {
        const count currentBandSize = bands[i].size();
        bandAngles[i].resize(currentBandSize);
        for (index j = 0; j < currentBandSize; j++) {
            bandAngles[i][j] = bands[i][j].getX();
        }
        if (!std::is_sorted(bandAngles[i].begin(), bandAngles[i].end())) {
            throw std::runtime_error("Points in bands must be sorted.");
        }
    }
    bandTimer.stop();
    INFO("Extracting band angles took ", bandTimer.elapsedMilliseconds(), " milliseconds.");

    // 2.Insert edges
    Aux::Timer timer;
    timer.start();
    vector<double> empty;
    GraphBuilder result(n, false, false);

#pragma omp parallel
    {
        index id = omp_get_thread_num();
        threadtimers[id].start();
#pragma omp for schedule(guided) nowait
        for (omp_index i = 0; i < static_cast<omp_index>(n); i++) {
            const double coshr = std::cosh(radii[i]);
            const double sinhr = std::sinh(radii[i]);
            count expectedDegree = (4 / PI) * n * std::exp(-(radii[i]) / 2);
            vector<index> near;
            near.reserve(expectedDegree * 1.1);
            Point2DWithIndex<double> pointV(angles[i], radii[i], i);
            for (index j = 0; j < bandCount; j++) {
                if (bandRadii[j + 1] > radii[i]) {
                    double minTheta, maxTheta;
                    std::tie(minTheta, maxTheta) =
                        getMinMaxTheta(angles[i], radii[i], bandRadii[j], R);
                    vector<Point2DWithIndex<double>> neighborCandidates =
                        getPointsWithinAngles(minTheta, maxTheta, bands[j], bandAngles[j]);

                    const count sSize = neighborCandidates.size();
                    for (index w = 0; w < sSize; w++) {
                        double deltaPhi =
                            PI - std::abs(PI - std::abs(angles[i] - neighborCandidates[w].getX()));
                        if (coshr * std::cosh(neighborCandidates[w].getY())
                                - sinhr * std::sinh(neighborCandidates[w].getY())
                                      * std::cos(deltaPhi)
                            <= coshR) {
                            if (neighborCandidates[w].getIndex() != i) {
                                near.push_back(neighborCandidates[w].getIndex());
                            }
                        }
                    }
                }
            }
            for (index j : near) {
                if (j >= n)
                    ERROR("Node ", j, " prospective neighbor of ", i,
                          " does not actually exist. Oops.");
                if (radii[j] > radii[i] || (radii[j] == radii[i] && angles[j] < angles[i]))
                    result.addHalfEdge(i, j);
            }
        }
        threadtimers[id].stop();
    }
    timer.stop();
    INFO("Generating Edges took ", timer.elapsedMilliseconds(), " milliseconds.");
    return result.completeGraph();
}

Graph HyperbolicGenerator::generate(const vector<double> &angles, const vector<double> &radii,
                                    double R, double T) {
    if (T < 0)
        throw std::runtime_error("Temperature cannot be negative.");
    if (T == 0)
        return generateCold(angles, radii, R);
    assert(T > 0);

    /**
     * fill Quadtree
     */
    Aux::Timer timer;
    timer.start();
    index n = angles.size();
    assert(radii.size() == n);

    assert(alpha > 0);
    Quadtree<index, false> quad(R, theoreticalSplit, alpha, capacity, balance);

    for (index i = 0; i < n; i++) {
        assert(radii[i] < R);
        quad.addContent(i, angles[i], radii[i]);
    }

    quad.trim();
    timer.stop();
    INFO("Filled Quadtree, took ", timer.elapsedMilliseconds(), " milliseconds.");

    assert(quad.size() == n);

    bool anglesSorted = std::is_sorted(angles.begin(), angles.end());

    // now define lambda
    double beta = 1 / T;
    assert(beta == beta);
    auto edgeProb = [beta, R](double distance) -> double {
        return 1 / (std::exp(beta * (distance - R) / 2) + 1);
    };

    // get Graph
    GraphBuilder result(n, false, false); // no direct swap with probabilistic graphs
    count totalCandidates = 0;
#pragma omp parallel for reduction(+ : totalCandidates)
    for (omp_index i = 0; i < static_cast<omp_index>(n); i++) {
        vector<index> near;
        totalCandidates += quad.getElementsProbabilistically(
            HyperbolicSpace::polarToCartesian(angles[i], radii[i]), edgeProb, anglesSorted, near);
        for (index j : near) {
            if (j >= n)
                ERROR("Node ", j, " prospective neighbor of ", i,
                      " does not actually exist. Oops.");
            if (j > i) {
                result.addHalfEdge(i, j);
            }
        }
    }
    DEBUG("Candidates tested: ", totalCandidates);
    return result.completeGraph();
}
} // namespace NetworKit
