#ifndef DRAW_H
#define DRAW_H

#include <string>
#include <cmath>
#include <ogdf/basic/GraphAttributes.h>
#include <QtSvg/QtSvg>
#include <QPicture>

#include "ogdffunctions.h"

using namespace std;

const double LVL_DIST = 0.1;
const double NODE_RADIUS = 10.0;
const double NODE_RADIUS_HALF = NODE_RADIUS / 2.0;
const QPen NODE_PEN = QPen(Qt::black);
const QBrush NODE_BRUSH = QBrush(Qt::black);
const QPen EDGE_PEN = QPen(Qt::black);
const QBrush EDGE_BRUSH = QBrush(QColor(255, 255, 255, 1.0));


QPointF from_dpoint(const ogdf::DPoint& pt)
{
    return QPointF(pt.m_x, pt.m_y);
}

void draw_nodes(QPainter* pt, const ogdf::GraphAttributes& ga)
{
    pt->setBrush(NODE_BRUSH);
    pt->setPen(NODE_PEN);
    for(auto n : ga.constGraph().nodes)
        pt->drawEllipse(ga.x(n), ga.y(n), NODE_RADIUS, NODE_RADIUS);
}

double norm(const QPointF& pt)
{
    return sqrt(pt.x()*pt.x() + pt.y()*pt.y());
}

ogdf::DPoint node_offset(ogdf::node n, const ogdf::GraphAttributes& ga)
{
    return ogdf::DPoint(ga.x(n) + NODE_RADIUS_HALF, ga.y(n) + NODE_RADIUS_HALF);
}

QLineF edge_segment(ogdf::edge e, const ogdf::GraphAttributes& ga)
{
    return QLineF(from_dpoint(node_offset(e->source(), ga)), from_dpoint(node_offset(e->target(), ga)));
}

const double EPSILON = 0.0000000001;

const double SPRING_CONSTANT = 0.1;

const int CYCLES = 6;

const int SUBDVISIONS_START = 1;
const int SUBDVISIONS_END = 32;
const int SUBDVISIONS_MULT = 2;

const double STEP_SIZE_START = 0.08;
const double STEP_SIZE_END = 0.00125;
const double STEP_SIZE_MULT = 0.5;

const double ITERATIONS_START = 50.0;
const double ITERATIONS_END = 7.0;
const double ITERATIONS_MULT = 2.0/3.0;

typedef ogdf::EdgeArray<vector<QPointF>> Subdivisons;
typedef ogdf::EdgeArray<vector<QPointF>> Forces;

void bundle_edges(ogdf::EdgeArray<QPainterPath>& edge_paths, const ogdf::GraphAttributes& ga, const ogdf::EdgeArray<int>& levels)
{
    using namespace placeholders;

    double total_length = 0.0;
    for(auto e : ga.constGraph().edges)
        total_length = total_length + edge_segment(e, ga).length();

    ogdf::EdgeArray<ogdf::EdgeArray<double>> compatibility(ga.constGraph());
    for(auto e : ga.constGraph().edges)
        compatibility[e].init(ga.constGraph(), 0.0);

    ogdf::EdgeArray<double> straight_lengths(ga.constGraph(), 0);
    for(auto e : ga.constGraph().edges)
        straight_lengths[e] = edge_segment(e, ga).length();

    auto length = [&](ogdf::edge e, const Subdivisons& subdivisions)
    {
        double total_length = 0.0;
        for(unsigned int i = 1; i < subdivisions[e].size(); ++i)
            total_length += QLineF(subdivisions[e][i], subdivisions[e][i-1]).length();

        return total_length;
    };

    auto angle_compatibility = [&](ogdf::edge e, ogdf::edge f)
    {
        return abs(cos(3.141/180.0 * edge_segment(e, ga).angleTo(edge_segment(f, ga))));
    };

    auto length_compatibility = [&](ogdf::edge e, ogdf::edge f)
    {
        auto el = straight_lengths[e];
        auto fl = straight_lengths[f];
        auto l = (el + fl)/2.0;

        return 2.0/(l/min(el,fl) + max(el,fl)/l);
    };

    auto distance_compatibility = [&](ogdf::edge e, ogdf::edge f)
    {
        auto el = edge_segment(e, ga);
        auto fl = edge_segment(f, ga);
        auto l = (el.length() + fl.length())/2.0;

        return l/(l + norm(el.pointAt(0.5) - fl.pointAt(0.5)));
    };

    auto parallel_compatibility = [&](ogdf::edge e, ogdf::edge f)
    {

        return 1.0;
    };

    auto compute_compatibility = [&](ogdf::edge e, ogdf::edge f)
    {
//        cout << angle_compatibility(e,f) << " * " << length_compatibility(e,f) << " * " << distance_compatibility(e,f) << " * " << parallel_compatibility(e,f) << endl;
        return angle_compatibility(e,f) * length_compatibility(e,f) * distance_compatibility(e,f) * parallel_compatibility(e,f);
    };

    auto compute_compatibilities = [&]()
    {
        for(auto e : ga.constGraph().edges)
            for(auto f : ga.constGraph().edges)
                compatibility[e][f] = compute_compatibility(e,f);
    };

    auto init_subdivisions = [&](Subdivisons& subdivisions, const int& P)
    {
        double p_add= 1.0/(P + 1.0);

        for(auto e : ga.constGraph().edges)
        {
            auto start = from_dpoint(node_offset(e->source(), ga));
            auto end = from_dpoint(node_offset(e->target(), ga));

            subdivisions[e].push_back(start);

            QLineF se(start,end);
            for(double p = p_add; p < 1.0; p = p + p_add)
                subdivisions[e].push_back(se.pointAt(p));

            subdivisions[e].push_back(end);
        }
    };

    auto update_subdivisions = [&](Subdivisons& subdivisions, const int& P)
    {
        for(auto e : ga.constGraph().edges)
        {
            vector<QPointF> new_subdivision;
            new_subdivision.push_back(from_dpoint(node_offset(e->source(), ga)));

            double new_segment_length = length(e, subdivisions)/ ((double)(P + 1));
            double current_segment_length = new_segment_length;

            for(unsigned int i = 1; i < subdivisions[e].size(); ++i)
            {
                QLineF segment(subdivisions[e][i - 1], subdivisions[e][i]);
                double segment_length = segment.length();

                while(segment_length - current_segment_length > 0.00000001)
                {
                    double pos = current_segment_length/segment_length;

                    new_subdivision.push_back(segment.pointAt(pos));

                    segment_length -= current_segment_length;
                    current_segment_length = new_segment_length;
                }

                current_segment_length -= segment_length;
            }

            new_subdivision.push_back(from_dpoint(node_offset(e->target(), ga)));
            subdivisions[e] = new_subdivision;
        }
    };

    auto spring_force = [&](ogdf::edge e, const Subdivisons& subdivisions, const int& p, const double& kP)
    {
        auto prev = subdivisions[e][p - 1];
        auto current = subdivisions[e][p];
        auto next = subdivisions[e][p + 1];

        auto pt = prev + next - 2.0*current;

        return kP * pt;
    };

    auto electrostatic_force = [&](ogdf::edge e, const Subdivisons& subdivisions, const int& p)
    {
        QPointF sum_norms(0.0,0.0);
        for(auto f : ga.constGraph().edges)
        {
            if(e->index() != f->index() && isgreater(compatibility[e][f], 0.05))
            {
                auto n = norm(subdivisions[e][p] - subdivisions[f][p]);
                if(n == 0.0)
                    n = 0.0000001;

                sum_norms = sum_norms + (subdivisions[f][p] - subdivisions[e][p]) * (compatibility[e][f]/n);
            }
        }

        return sum_norms;
    };

    auto calculate_forces = [&](Forces& forces, const Subdivisons& subdivisions, const int& P, const double& S)
    {
        for(auto e : ga.constGraph().edges)
        {
            forces[e].push_back(QPointF(0.0,0.0));
            double kP = SPRING_CONSTANT / (straight_lengths[e] * (double)(P + 1));

            for(int i = 1; i < P + 1; ++i)
            {
                auto sf = spring_force(e, subdivisions, i, kP);
                auto ef = electrostatic_force(e, subdivisions, i);

                forces[e].push_back(S * (sf + ef));
            }

            forces[e].push_back(QPointF(0.0,0.0));
        }
    };

    auto apply_forces = [&](const Forces& forces, Subdivisons& subdivisions)
    {
        for(auto e : ga.constGraph().edges)
            for(unsigned int i = 0; i < subdivisions[e].size(); ++i)
                subdivisions[e][i] = subdivisions[e][i] + forces[e][i];
    };

    auto create_paths = [&](const Subdivisons& subdivisions)
    {
        for(auto e : ga.constGraph().edges)
        {
            assert(subdivisions[e].size() >= 2);

            edge_paths[e].moveTo(subdivisions[e][0]);
            for(unsigned int i = 1; i < subdivisions[e].size(); ++i)
                edge_paths[e].lineTo(subdivisions[e][i]);
        }
    };

    Subdivisons subdivisions(ga.constGraph());
    double I = ITERATIONS_START;
    double S = STEP_SIZE_START;
    double P = SUBDVISIONS_START;

    init_subdivisions(subdivisions, P);
    compute_compatibilities();

    for(int c = CYCLES; c > 0; --c)
    {
        for(int i = 0; i <= I; ++i)
        {
            Forces forces(ga.constGraph());
            calculate_forces(forces, subdivisions, P, S);
            apply_forces(forces, subdivisions);
        }

        I = I * ITERATIONS_MULT;
        S = S * STEP_SIZE_MULT;
        P = P * SUBDVISIONS_MULT;

        update_subdivisions(subdivisions, P);
    }

    create_paths(subdivisions);
}

void draw_edges(QPainter* pt, ogdf::Graph& g, ogdf::GraphAttributes& ga, const ogdf::NodeArray<int>& node_pos, const ogdf::EdgeArray<int>& levels, const ogdf::EdgeArray<bool>& edge_direction)
{
    using namespace placeholders;

    auto on_circle = [&](ogdf::edge e) {int offset = abs(node_pos[e->source()] - node_pos[e->target()]); return offset == 1 || offset + 1 == g.numberOfNodes();};

    pt->setBrush(EDGE_BRUSH);
    pt->setPen(EDGE_PEN);
    auto C = find_center(ga);
    auto R = get_radius(ga);
    auto lvl_dist = R * LVL_DIST;

    auto point = [](const double& perc, const ogdf::DPoint& C, const double& D, const ogdf::DPoint& norm) {return C + D * perc * norm;};
    auto point_on_circle = [](const double& perc, const ogdf::DPoint& C, const double& s_a, const double& e_a, const double& L, const bool& dir)
    {
        auto arc_length = dir ? abs(e_a - s_a) : 2*3.141 - abs(e_a - s_a);
        auto a = s_a + arc_length * perc;
        return C + L * ogdf::DPoint(cos(a), sin(a));
    };

    vector<ogdf::edge> to_hide;
    for(auto e : g.edges)
        if(levels[e] != 0 || on_circle(e))
            to_hide.push_back(e);

    ogdf::Graph::HiddenEdgeSet outside(g);
    for(auto e : to_hide)
        outside.hide(e);

    ogdf::EdgeArray<QPainterPath> interior_paths(g);
    bundle_edges(interior_paths, ga, levels);

    outside.restore();

    for(auto e : ga.constGraph().edges)
    {
        if(on_circle(e))
            pt->drawLine(edge_segment(e, ga));
        else if(levels[e] == 0)
            pt->drawPath(interior_paths[e]);
        else
        {
            QPainterPath path;

            auto s = node_pos[e->source()] < node_pos[e->target()] ? e->source() : e->target();
            auto t = node_pos[e->source()] > node_pos[e->target()] ? e->source() : e->target();
            auto S = node_offset(s, ga);
            auto T = node_offset(t, ga);

            auto dir = edge_direction[e];
            auto sign = dir ? 1 : -1;
            auto D = R + lvl_dist * levels[e];

            auto S_norm = S / (S - C).norm();
            auto T_norm = T / (T - C).norm();

            auto start = bind(point, _1, C, D, S_norm);
            auto end = bind(point, _1, C, D, T_norm);

            ogdf::DSegment cs(C, start(1.0));
            ogdf::DSegment ce(C, end(1.0));
            auto s_angle = get_rad(cs.dx(), cs.dy()).second;
            auto t_angle = get_rad(ce.dx(), ce.dy()).second;
            auto poc = bind(point_on_circle, _1, C, s_angle, t_angle, cs.length(), dir);

            path.moveTo(from_dpoint(S));
            path.lineTo(from_dpoint(start(0.9)));
            path.quadTo(from_dpoint(start(1.0)), from_dpoint(poc(sign * 0.05)));

            for(double t = sign * 0.05; abs(t) < 0.95; t += sign * 0.01)
                path.lineTo(from_dpoint(poc(t)));

            path.quadTo(from_dpoint(end(1.0)), from_dpoint(end(0.9)));
            path.lineTo(from_dpoint(T));
            pt->drawPath(path);
        }
    }
}

void draw_graph_to_svg(ogdf::Graph& g, ogdf::GraphAttributes& ga, const ogdf::NodeArray<int> node_pos, const ogdf::EdgeArray<int>& levels, const ogdf::EdgeArray<bool>& edge_direction, const string& filename)
{
    QSvgGenerator gen;
    gen.setFileName(QString::fromStdString(filename));
    gen.setSize(QSize(2000, 2000));
    QPainter pt;

    pt.begin(&gen);
    pt.translate(1000, 1000);
    draw_nodes(&pt, ga);
    draw_edges(&pt, g, ga, node_pos, levels, edge_direction);
    pt.end();
}

#endif // DRAW_H
