/* Programming contest management system
 * Copyright © 2012 Luca Wehrstedt <luca.wehrstedt@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

var Overview = new function () {
    var self = this;

    self.PAD_T = 20;
    self.PAD_B = 10;
    self.PAD_L = 10;
    self.PAD_R = 10;

    self.init = function () {
        var $elem = $("#Overview");

        self.width = $elem.width();
        self.height = $elem.height();

        self.paper = Raphael($elem[0], self.width, self.height);

        self.create_score_chart();

        self.update_score_axis();
        self.update_rank_axis();


        $(window).resize(function () {
            self.width = $elem.width();
            self.height = $elem.height();

            self.paper.setSize(self.width, self.height);

            self.update_score_chart(0);

            self.update_score_axis();
            self.update_rank_axis();

            self.update_markers(0);
        });


        DataStore.user_update.add(function (key, old_data, data) {
            if (old_data["markers"]) {
                data["markers"] = old_data["markers"];
                delete old_data["markers"];
            }
            if (old_data["marker_c_anim"]) {
                data["marker_c_anim"] = old_data["marker_c_anim"];
                delete old_data["marker_c_anim"];
            }
            if (old_data["marker_u_anim"]) {
                data["marker_u_anim"] = old_data["marker_u_anim"];
                delete old_data["marker_u_anim"];
            }
            if ($.inArray(old_data, self.user_list) != -1) {
                self.user_list.splice($.inArray(old_data, self.user_list), 1, data);
            }
        });

        DataStore.score_events.add(self.score_handler);
        DataStore.rank_events.add(self.rank_handler);
        DataStore.select_events.add(self.select_handler);


        // HEADERS ("Score" and "Rank")
        self.paper.setStart();
        self.paper.text(4, 10, "Score").attr("text-anchor", "start");
        self.paper.text(self.width - 4, 10, "Rank").attr("text-anchor", "end");
        var set = self.paper.setFinish();
        set.attr({"font-size": "12px", "fill": "#000000", "stroke": "none", "font-family": "sans-serif", "opacity": 0});

        $elem.mouseenter(function () {
            set.animate({"opacity": 1}, 1000);
        });

        $elem.mouseleave(function () {
            set.animate({"opacity": 0}, 1000);
        });


        // Load initial data.
        $.each(DataStore.users, function (u_id, user) {
            if (user["selected"] > 0)
            {
                self.user_list.push(user);
            }
        });
        self.user_list.sort(self.compare_users);
        self.update_markers(0);
    };


    /** SCORE & RANK AXIS */

    self.update_score_axis = function () {
        var d = Raphael.format("M {1},{3} L {1},{7} M {0},{4} L {2},{4} M {0},{5} L {2},{5} M {0},{6} L {2},{6}",
                               self.PAD_L - 4,
                               self.PAD_L,
                               self.PAD_L + 4,
                               self.PAD_T,
                               self.PAD_T + (self.height - self.PAD_T - self.PAD_B) * 0.25,
                               self.PAD_T + (self.height - self.PAD_T - self.PAD_B) * 0.50,
                               self.PAD_T + (self.height - self.PAD_T - self.PAD_B) * 0.75,
                               self.height - self.PAD_B);

        if (self.score_axis) {
            self.score_axis.attr("path", d);
        } else {
            self.score_axis = self.paper.path(d).attr(
                {"fill": "none", "stroke": "#b8b8b8", "stroke-width": 3, "stroke-linecap": "round"});
        }
    };


    self.update_rank_axis = function () {
        var d = Raphael.format("M {1},{3} L {1},{7} M {0},{4} L {2},{4} M {0},{5} L {2},{5} M {0},{6} L {2},{6}",
                               self.width - self.PAD_R - 4,
                               self.width - self.PAD_R,
                               self.width - self.PAD_R + 4,
                               self.PAD_T,
                               self.PAD_T + (self.height - self.PAD_T - self.PAD_B) / 12,
                               self.PAD_T + (self.height - self.PAD_T - self.PAD_B) / 4,
                               self.PAD_T + (self.height - self.PAD_T - self.PAD_B) / 2,
                               self.height - self.PAD_B);

        if (self.rank_axis) {
            self.rank_axis.attr("path", d);
        } else {
            self.rank_axis = self.paper.path(d).attr(
                {"fill": "none", "stroke": "#b8b8b8", "stroke-width": 3, "stroke-linecap": "round"});
        }
    };


    /** SCORE CHART */

    self.SCORE_STEPS = 15;

    // scores[0] contains the number of users with a score of zero
    // scores[i] (with i in [1..SCORE_STEPS]) contains the number of users with
    //     a score in the half-open interval [i * (max_score / SCORE_STEPS),
    //     (i+1) * (max_score / SCORE_STEPS)). for i == 0 the interval is open
    // scores[SCORE_STEPS+1] contins the number of user with the max_score
    // see also self.get_score_class()
    self.scores = new Array();

    for (var i = 0; i <= self.SCORE_STEPS + 1; i += 1) {
        self.scores.push(0);
    }


    self.make_path_for_score_chart = function () {
        // For each element of self.scores, we convert the number it contains
        // to a distance from the score axis and then create a smooth path that
        // passes on all those points.
        // To convert the number of users to a distance we use the following
        // formula (a parabola, open down):  d(x) = a * x^2 + b * x + c
        // with a, b and c parameters chosen such that:
        // - d(0) = 0;        - d'(0) = 3/2;
        // - d(max_users) = 3/4 * width (excluding padding);

        var max_users = DataStore.user_count;
        var a = (3/4 * (self.width - self.PAD_R - self.PAD_L) - 3/2 * max_users) / (max_users * max_users);
        var b = 3/2;
        var c = 0;

        var s_path = "";
        for (var i = 0; i <= self.SCORE_STEPS + 1; i += 1) {
            var x = self.PAD_L + a * self.scores[i] * self.scores[i] + b * self.scores[i] + c;
            var y = self.height - self.PAD_B - i * (self.height - self.PAD_T - self.PAD_B) / (self.SCORE_STEPS + 1);
            if (i == 0) {
                s_path += Raphael.format("M {0},{1} R", x, y);
            } else {
                s_path += Raphael.format(" {0},{1}", x, y);
            }
        }

        return s_path;
    };


    self.recompute = function () {
        // Recompute self.scores
        for (var i = 0; i <= self.SCORE_STEPS + 1; i += 1) {
            self.scores[i] = 0;
        }

        var users = DataStore.users;
        var max_score = DataStore.global_max_score;

        for (var u_id in users) {
            self.scores[self.get_score_class(users[u_id]["global"], max_score)] += 1;
        }
    };


    self.create_score_chart = function () {
        self.recompute();
        var s_path = self.make_path_for_score_chart();
        self.score_line = self.paper.path(s_path).attr({"fill": "none", "stroke": "#cccccc", "stroke-width": 2, "stroke-linecap": "round"});
        s_path += Raphael.format(" L {0},{1} {0},{2} Z", self.PAD_L, self.PAD_T, self.height - self.PAD_B);
        self.score_back = self.paper.path(s_path).attr({"fill": "0-#E4E4E4-#DADADB", "stroke": "none"});
        self.score_back.toBack();
    };


    self.update_score_chart = function (t) {
        var s_path = self.make_path_for_score_chart();
        self.score_line.animate({'path': s_path}, t);
        s_path += Raphael.format(" L {0},{1} {0},{2} Z", self.PAD_L, self.PAD_T, self.height - self.PAD_B);
        self.score_back.animate({'path': s_path}, t);
    };


    self.get_score_class = function (score, max_score) {
        if (score <= 0) {
            return 0;
        } else if (score >= max_score) {
            return self.SCORE_STEPS + 1;
        } else {
            return parseInt(score / max_score * self.SCORE_STEPS) + 1;
        }
    };


    /** MARKERS */


    // We keep a sorted list of user that represent the current order of the
    // selected users in the overview. In particular we sort using these keys:
    // - the global score
    // - the last name
    // - the first name
    // - the key
    self.user_list = new Array();


    // Compare two users. Returns -1 if "a < b" or +1 if "a >= b"
    // (where a < b means that a shoud go above b in the overview)
    self.compare_users = function (a, b) {
        if ((a["global"] > b["global"]) || ((a["global"] == b["global"]) &&
           ((a["l_name"] < b["l_name"]) || ((a["l_name"] == b["l_name"]) &&
           ((a["f_name"] < b["f_name"]) || ((a["f_name"] == b["f_name"]) &&
           (a["key"] <= b["key"]))))))) {
            return -1;
        } else {
            return +1;
        }
    };

    self.MARKER_PADDING = 2;
    self.MARKER_RADIUS = 2.5;
    self.MARKER_LABEL_WIDTH = 50;
    self.MARKER_LABEL_HEIGHT = 20;
    self.MARKER_ARROW_WIDTH = 20;
    self.MARKER_STROKE_WIDTH = 2;

    self.make_path_for_marker = function (s_h, u_h, r_h) {
        // The path is composed of a label (whose vertical center is at u_h,
        // self.MARKER_LABEL_WIDTH wide and self.MARKER_LABEL_HEIGHT high),
        // made of two horizontal (H) lines (for top and bottom), delimited on
        // the right by two straight lines (L) forming an arrow (which is
        // self.MARKER_ARROW_WIDTH wide), with its center at an height of r_h.
        // On the left two cubic bezier curves (C) start tangentially from the
        // label and end, still tangentially, on an elliptic arc (A), with its
        // center at an height of s_h and a radius of self.MARKER_RADIUS.
        // The path starts just above the arc, with the first cubic bezier.

        // TODO Most of these values are constants, no need to recompute
        // everything again every time.

        return Raphael.format("M {0},{5} C {1},{5} {1},{6} {2},{6} H {3} L {4},{7} {3},{8} H {2} C {1},{8} {1},{9} {0},{9} A {10},{10} 0 0,1 {0},{5} Z",
                              self.PAD_L,
                              (self.PAD_L + self.width - self.PAD_R - self.MARKER_ARROW_WIDTH - self.MARKER_LABEL_WIDTH) / 2,
                              self.width - self.PAD_R - self.MARKER_ARROW_WIDTH - self.MARKER_LABEL_WIDTH,
                              self.width - self.PAD_R - self.MARKER_ARROW_WIDTH,
                              self.width - self.PAD_R,
                              s_h - self.MARKER_RADIUS,
                              u_h - (self.MARKER_LABEL_HEIGHT - self.MARKER_STROKE_WIDTH) / 2,
                              r_h,
                              u_h + (self.MARKER_LABEL_HEIGHT - self.MARKER_STROKE_WIDTH) / 2,
                              s_h + self.MARKER_RADIUS,
                              self.MARKER_RADIUS);
    };


    self.create_marker = function (user, s_h, u_h, r_h, t) {
        var d = self.make_path_for_marker(s_h, u_h, r_h);

        // Map the color_index given by DataStore to the actual color
        // (FIXME This almost duplicates some code in Ranking.css...)
        switch (user["selected"]) {
            case 1:  // Blue
                var color_a = "#729fcf";
                var color_b = "#3465a4";
                break;
            case 2:  // Butter
                var color_a = "#fce94f";
                var color_b = "#edd400";
                break;
            case 3:  // Red
                var color_a = "#ef2929";
                var color_b = "#cc0000";
                break;
            case 4:  // Chameleon
                var color_a = "#8ae234";
                var color_b = "#73d216";
                break;
            case 5:  // Orange
                var color_a = "#fcaf3e";
                var color_b = "#f57900";
                break;
            case 6:  // Plum
                var color_a = "#ad7fa8";
                var color_b = "#75507b";
                break;
            case 7:  // Aluminium
                var color_a = "#babdb6";
                var color_b = "#888a85";
                break;
            case 8:  // Chocolate
                var color_a = "#e9b96e";
                var color_b = "#c17d11";
                break;
        }

        self.paper.setStart();
        self.paper.path(d).attr({
            "fill": color_b,
            "stroke": color_a,
            "stroke-width": self.MARKER_STROKE_WIDTH,
            "stroke-linejoin": "round"});
        // Place the text inside the label, with a padding-right equal to its
        // padding-top and padding-bottom.
        var t_x = self.width - self.PAD_R - self.MARKER_ARROW_WIDTH - (self.MARKER_LABEL_HEIGHT - 12) / 2;
        self.paper.text(t_x, u_h, self.transform_key(user)).attr({
            "fill": "#ffffff",
            "stroke": "none",
            "font-family": "sans-serif",
            "font-size": "12px",
            "text-anchor": "end"});
        var set = self.paper.setFinish();
        set.attr({"cursor": "pointer",
                  "opacity": 0});

        set.click(function () {
            Scoreboard.scroll_into_view(user["key"]);
        });

        user["markers"] = set;

        user["marker_c_anim"] = Raphael.animation({"opacity": 1}, t, function () {
            delete user["marker_c_anim"];
        });
        set.animate(user["marker_c_anim"]);
    };

    self.transform_key = function(user) {
      var s = user['f_name'] + ' ' + user['l_name'];
      var sl = s.split(' ');
      var out = '';
      for (var i = 0; i < sl.length; i++) {
          if (sl[i].length > 0) {
              out += sl[i][0];
          }
      }
      if (user["team"] != null && user["team"] != undefined) {
          return user['team'] + '-' + out;
      } else {
          return out;
      }
    };


    self.update_marker = function (user, s_h, u_h, r_h, t) {
        var d = self.make_path_for_marker(s_h, u_h, r_h);

        // If the duration of the animation is 0 or if the element has just
        // been created (i.e. its creation animation hasn't finished yet) then
        // just set the new path and position. Else, animate them.
        if (t && !user["marker_c_anim"]) {
            user["markers"].stop();
            user["marker_u_anim"] = Raphael.animation({"path": d, "y": u_h}, t, function () {
                delete user["marker_u_anim"];
            });
            user["markers"].animate(user["marker_u_anim"]);
        } else {
            user["markers"].attr({"path": d, "y": u_h});
        }
    };


    self.delete_marker = function (user, t) {
        var markers = user["markers"];
        delete user["markers"];

        // If an update animation is running, we stop and delete it
        if (user["marker_u_anim"]) {
            markers.stop();
            delete user["marker_u_anim"];
        }

        var anim = Raphael.animation({"opacity": 0}, t, function () {
            markers.remove();
        });
        markers.animate(anim);

        self.user_list.splice($.inArray(user, self.user_list), 1);
        self.update_markers(t);
    };


    self.get_score_height = function (score, max_score) {
        return self.height - self.PAD_B - score / max_score * (self.height - self.PAD_T - self.PAD_B);
    };


    self.get_rank_height = function (rank, max_rank) {
        return self.PAD_T + (rank - 1) / (max_rank - 1) * (self.height - self.PAD_T - self.PAD_B);
    };


    self.merge_clusters = function (a, b) {
        // See the next function to understand the purpose of this function
        var middle = (a.n * (a.b + a.e) / 2 + b.n * (b.b + b.e) / 2) / (a.n + b.n);
        a.list = a.list.concat(b.list);
        a.n += b.n;
        a.b = middle - (a.n * self.MARKER_LABEL_HEIGHT + (a.n - 1) * self.MARKER_PADDING) / 2;
        a.e = a.b + a.n * self.MARKER_LABEL_HEIGHT + (a.n - 1) * self.MARKER_PADDING;
    };


    self.update_markers = function (t) {
        // Use them as shortcut
        var h = self.MARKER_LABEL_HEIGHT;
        var p = self.MARKER_PADDING;

        // We iterate over all selected users (in top-to-bottom order). For
        // each of them we create a cluster which, initally, contains just that
        // user. Then, if the cluster overlaps with another, we merge them and
        // increase its size so that its element don't overlap anymore. We
        // repeat this process unit no two clusters overlap, and then proceed
        // to the next user. We also take care that no cluster is outside the
        // visible area, either above or below.

        // The list of clusters and its size (n == cs.length)
        var cs = new Array();
        var n = 0;

        for (var i in self.user_list) {
            var user = self.user_list[i];
            var r_height = self.get_rank_height(user["rank"], DataStore.user_count);

            // 'b' (for begin) is the y coordinate of the top of the cluster
            // 'e' (for end) is the y coordinate of the bottom of the cluster
            // 'n' is the number of items it contains (c.n == c.list.length)
            cs.push({'b': r_height - h/2, 'e': r_height + h/2, 'list': [user], 'n': 1});
            n += 1;

            // Check if it overlaps with the one above it
            while (n > 1 && cs[n-2].e + p > cs[n-1].b) {
                self.merge_clusters(cs[n-2], cs[n-1]);
                cs.pop();
                n -= 1;
            }

            // Check if it overflows at the top of the visible area
            if (cs[n-1].b < self.PAD_T - h/2) {
                cs[n-1].e += (self.PAD_T - h/2) - cs[n-1].b;
                cs[n-1].b = self.PAD_T - h/2;
            }
        }

        // Check if it overflows at the bottom of the visible area
        while (n > 0 && cs[n-1].e > self.height - self.PAD_B + h/2) {
            cs[n-1].b += (self.height - self.PAD_B + h/2) - cs[n-1].e;
            cs[n-1].e = self.height - self.PAD_B + h/2;

            // Check if it overlaps with the one above it
            if (n > 1 && cs[n-2].e + p > cs[n-1].b) {
                self.merge_clusters(cs[n-2], cs[n-1]);
                cs.pop();
                n -= 1;
            }
        }

        // If it overflows again at the top then there's simply not enough
        // space to hold them all. Compress them.
        if (n > 0 && cs[0].b < self.PAD_T - h/2) {
            cs[0].b = self.PAD_T - h/2;
        }

        // Proceed with the actual drawing
        for (var i in cs) {
            var c = cs[i];
            var begin = c.b;
            var step = (c.e - begin - h) / (c.n - 1);  // NaN if c.n == 1

            for (var j in c.list) {
                var user = c.list[j];

                var s_height = self.get_score_height(user["global"], DataStore.global_max_score);
                var r_height = self.get_rank_height(user["rank"], DataStore.user_count);

                if (user["markers"]) {
                    // Update the existing marker
                    self.update_marker(user, s_height, begin + h/2, r_height, t);
                } else {
                    // Create a new marker
                    self.create_marker(user, s_height, begin + h/2, r_height, t);
                }

                begin += step;  // begin is NaN if step is NaN: no problem
                                // because if c.n == 1 begin won't be used again
            }
        }
    };


    self.score_handler = function (u_id, user, t_id, task, delta) {
        var new_score = user["global"];
        var old_score = new_score - delta;
        var max_score = DataStore.global_max_score;

        self.scores[self.get_score_class(old_score, max_score)] -= 1;
        self.scores[self.get_score_class(new_score, max_score)] += 1;

        self.update_score_chart(1000);

        if (user["selected"] > 0) {
            self.user_list.sort(self.compare_users);
            self.update_markers(1000);
        }
    };


    self.rank_handler = function (u_id, user, delta) {
        if (user["selected"] > 0) {
            self.update_markers(1000);
        }
    };


    self.select_handler = function (u_id, color) {
        var user = DataStore.users[u_id];
        if (color > 0) {
            self.user_list.push(user);
            self.user_list.sort(self.compare_users);
            self.update_markers(1000);
        } else {
            self.delete_marker(DataStore.users[u_id], 1000);
        }
    };

    /* TODO: When users get added/removed the total user count changes and all
       rank "markers" need to be adjusted!
     */
};
