# -*- coding: UTF-8 -*-

"""
    This file is Free Software under the GNU GPL, Version >=2;
    and comes with NO WARRANTY!

    Version 1.3

    usage example:

    {{{
    #!burndown
    points:180
    start:2015-03-01
    end:2015-03-07
    2015-03-01:180
    2015-03-02:100
    2015-03-04:0
    }}}

    explanation:
    'points' represents the size of the value of the y-Axis
    'start' represents the start-point of a project.
    'end' represents the end point.
    the other values representing actual measured dates. The format is:
    <year>-<date>-<month>:<remaining points>

    the 'ideal line' will be plotted from the startpoint to the endpoint
    the 'actual line' will be plotted over all remaining points

    Initial Version 2015-03-05
    @copyright: 2015 by Intevation GmbH Osnabrueck
    @author: Sean Engelhardt <sean.engelhardt@intevation.de>
    @license: GNU GPLv>=2.
"""

import math

# a class named parser is needed my the moinmo.in plugin interface
class Parser:
    def __init__(self, raw, request, **kw):
        self.pagename = request.page.page_name
        self.raw = raw
        self.request = request
        self.formatter = request.formatter
        self.kw = kw
        self.d3js_source = "http://d3js.org/d3.v3.min.js"
        self.chart = self.html_code()


    #get month-numbers between 1-12 innstead 0-11
    def normalize_month_number(self, month):
        return str(int(month) - 1)


    #cut of "start" or end, normalize the month, number and return as well-formed date array for d3.js
    def refract_parameters(self, parameters):
        #cut of the "start"
        date_array = parameters.split(':')

        #save the date as array
        date_array = date_array[1].split('-')

        date_array[1] = self.normalize_month_number(date_array[1])
        return date_array


    def is_valid_number(self, entry):
        if math.isnan(int(entry)):
            # raise ValueError("The entry: '" + entry + "' does not seem to be a proper number!")
            raise ValueError(entry)


    def validate_date_numbers(self, entry):
        if math.isnan(int(entry[0])) or math.isnan(int(entry[1])) or math.isnan(int(entry[2])):
            # raise ValueError('The entry: "' + entry + '" does not seem to be a proper date!')
            raise ValueError(entry)


    # the format methode is used by the plugin interface automaticly.
    # format is also called for each !# command.
    # the formatter (object) is not documented well. See moinmoin/formatter/base.py
    # half of the methods raise a 'not implemented' error.
    # however, the formater object is mostly used to print out lines on the webpage
    # e.g. formatter.text(plain text) / formatter.rawHTML(htmlcode) / formatter.paragraph(int)
    def format(self, formatter):

        try:

            parameters = self.raw.split()

            ideal = []
            actual = []
            points = ""

            for line in parameters:

                if line.startswith("points"):
                    points = line.split(":")[1]
                    self.is_valid_number(points)

                elif line.startswith("start"):

                    splitted_line = self.refract_parameters(line)
                    self.validate_date_numbers(splitted_line)

                    ideal.append("{date : new Date(%s), points: %s}" % (", ".join(splitted_line), points))

                elif line.startswith("end"):
                    splitted_line = self.refract_parameters(line)
                    self.validate_date_numbers(splitted_line)

                    ideal.append("{date : new Date(%s), points: 0}," % (", ".join(splitted_line)))

                elif line.startswith("20"):

                    # get the points
                    points_of_date = line.split(':')[1]
                    self.is_valid_number(points_of_date)

                    # Cut the points of the line
                    splitted_line = line.split(':')[0]

                    # get the date as array
                    splitted_line = splitted_line.split('-')
                    self.validate_date_numbers(splitted_line)

                    splitted_line[1] = self.normalize_month_number(splitted_line[1])

                    actual.append("{date : new Date(%s), points : %s}" % (", ".join(splitted_line), points_of_date))

            self.chart = (self.chart.replace("var ideal=[];", "var ideal=[%s];" % (", ".join(ideal),))
                                    .replace("var actual=[];", "var actual=[%s];" % (", ".join(actual))))

            # print("REQUESR\n-----\n" + self.request)

            self.request.write(formatter.rawHTML(self.chart))

        except ValueError as err:
            error_output = """
                <div style="border:thin solid red;">
                    An Error Occured! Did you used the burn down chart parser in the wrong way? <br />
                    {0}
                </div>
                """.format(err)
            self.request.write(formatter.rawHTML(error_output))

    def html_code(self):
        compressed_css_code = """
            .svg div{ font: 10px sans-serif; text-align: right; float: left; display: block; padding: 10px; margin: 10px; color: white; } .axis path, .axis line { fill: none; stroke: black; stroke-width: 1px; } .line { fill: none; stroke-width: 3px; } .line.ideal { stroke: blue; } .line.actual { stroke: red; } .point.ideal { fill: blue; stroke: blue; } .point.actual { fill: red; stroke: red; } .grid .tick { stroke: lightgrey; opacity: 0.7; } .grid path { stroke-width: 0; }
            """
        compressed_js_code = """
            var ideal=[];var actual=[];
            var yAxisDomain=[];var daysInSprint=function(){return Math.max(dayDifference(getFirstDateInStructure(actual),getLastDateInStructure(actual)),dayDifference(getFirstDateInStructure(ideal),getLastDateInStructure(ideal)))};var pointsInSprint=function(){var a=Math.max(getMaxPointInStructure(actual),getMaxPointInStructure(ideal));var b=Math.min(getMinPointInStructure(actual),getMinPointInStructure(ideal));return(a-b)};function dayDifference(b,a){var c=(a-b)/(1000*60*60*24);if(c>60){c=60}return c}function getMaxPointInStructure(b){var a=0;for(var c=0;c<b.length;c++){if(b[c].points>a){a=b[c].points}}return a}function getMinPointInStructure(a){var c=0;for(var b=0;b<a.length;b++){if(a[b].points<c){c=a[b].points}}return c}function getLastDateInStructure(a){var c=a[0].date;for(var b=0;b<a.length;b++){if(a[b].date>c){c=a[b].date}}return c}function getFirstDateInStructure(a){var c=a[0].date;for(var b=0;b<a.length;b++){if(a[b].date<c){c=a[b].date}}return c}function contentOrPreviewDiv(){if(document.getElementById("preview")){return"#preview"}else{if(document.getElementById("content")){return"#content"}else{return"body"}}}function setPointTickLimit(a){if(pointsInSprint()<a){return pointsInSprint()}else{return a}}function setDateTickLimit(a){if(daysInSprint()<a){return daysInSprint()}else{return a}}function makeChart(){var e={top:10,right:30,bottom:100,left:65},c=800-e.left-e.right,k=600-e.top-e.bottom;var i=d3.time.scale().range([0,c]);var h=d3.scale.linear().range([k,0]);var j=d3.svg.line().x(function(m){return i(m.date)}).y(function(m){return h(m.points)});var l=d3.svg.line().x(function(m){return i(m.date)}).y(function(m){return h(m.points)});yAxisDomain.push(getMinPointInStructure(actual),Math.max(getMaxPointInStructure(actual),getMaxPointInStructure(ideal)));yAxisDomain[1]=function(){if(yAxisDomain[1]<=10){return Math.ceil(yAxisDomain[1]*1.1)}else{if(yAxisDomain[1]<100){return Math.ceil((yAxisDomain[1]+1)/5)*5}else{return Math.ceil((yAxisDomain[1]+1)/10)*10}}}();h.domain(d3.extent(yAxisDomain,function(m){return m}));i.domain(d3.extent(ideal,function(m){return m.date}));var d=d3.svg.axis().scale(i).orient("bottom").ticks(setDateTickLimit(10)).tickFormat(d3.time.format("%m-%d"));var b=d3.svg.axis().scale(h).orient("left").ticks(setPointTickLimit(10));var a=d3.svg.axis().scale(i).orient("bottom").ticks(setDateTickLimit(25));var g=d3.svg.axis().scale(h).orient("left").ticks(setPointTickLimit(25));var f=d3.select(contentOrPreviewDiv()).append("svg").attr("class","svg").attr("width",c+e.left+e.right).attr("height",k+e.top+e.bottom).append("g").attr("transform","translate("+e.left+","+e.top+")");f.append("g").attr("class","grid").attr("transform","translate(0,"+k+")").call(a.tickSize(-k,0,0).tickFormat(""));f.append("g").attr("class","grid").call(g.tickSize(-c,0,0).tickFormat(""));f.append("g").attr("class","x axis").attr("transform","translate(0,"+k+")").call(d).selectAll("text").style("text-anchor","end").attr("dx","-.7em").attr("dy",".2em").attr("transform",function(){return"rotate(-65)"});f.append("g").attr("class","y axis").call(b).append("text").attr("transform","rotate(-90)").attr("y",6).attr("dy",".5em").style("text-anchor","end");f.append("text").attr("x",c/2).attr("y",k+60).attr("dy",".5em").style("text-anchor","middle").text("Date");f.append("text").attr("transform","rotate(-90)").attr("y",10-e.left).attr("x",0-(k/2)).attr("dy","1em").style("text-anchor","middle").text("Points");f.append("path").datum(ideal).attr("class","line ideal").attr("d",j);f.append("path").datum(actual).attr("class","line actual").attr("d",l);f.selectAll("circle.point.ideal").data(ideal).enter().append("circle").attr("cx",function(m){return i(m.date)}).attr("cy",function(m){return h(m.points)}).attr("r",4).attr("class","point ideal");f.selectAll("circle.point.actual").data(actual).enter().append("circle").attr("cx",function(m){return i(m.date)}).attr("cy",function(m){return h(m.points)}).attr("r",4).attr("class","point actual")}makeChart();
            """

        return """
            <!DOCTYPE html><html><head><style>{css}</style></head><body><script src="{d3js}"></script><script type="text/javascript">{js}</script></body></html>
        """.format(css = compressed_css_code, d3js = self.d3js_source, js = compressed_js_code)
