import csv
import cairo
import argparse
import json
 
def split(input, size):
    return [input[start:start+size] for start in range(0, len(input), size)]
 
class Slopegraph:
 
    starts = {} # starting "points"    ends = {} # ending "points"    pairs = [] # base pair array for the final plotting
 
    def readCSV(self, filename):
 
        slopeReader = csv.reader(open(filename, 'rb'), delimiter=',', quotechar='"')
 
        for row in slopeReader:
 
            # add chosen values (need start/end for each CSV row) to the final plotting array.
 
            lab = row[0] # label
            beg = float(row[1]) # left vals
            end = float(row[2]) # right vals
 
            self.pairs.append( (float(beg), float(end)) )
 
            # combine labels of common values into one string
 
            if beg in self.starts:
                self.starts[beg] = self.starts[beg] + "; " + lab
            else:
                self.starts[beg] = lab
 
 
            if end in self.ends:
                self.ends[end] = self.ends[end] + "; " + lab
            else:
                self.ends[end] = lab
 
 
    def sortKeys(self):
 
        # sort all the values (in the event the CSV wasn't) so
        # we can determine the smallest increment we need to use
        # when stacking the labels and plotting points
 
        self.startSorted = [(k, self.starts[k]) for k in sorted(self.starts)]
        self.endSorted = [(k, self.ends[k]) for k in sorted(self.ends)]
 
        self.startKeys = sorted(self.starts.keys())
        self.delta = max(self.startSorted)
        for i in range(len(self.startKeys)):
            if (i+1 <= len(self.startKeys)-1):
                currDelta = float(self.startKeys[i+1]) - float(self.startKeys[i])
                if (currDelta < self.delta):
                    self.delta = currDelta
 
        self.endKeys = sorted(self.ends.keys())
        for i in range(len(self.endKeys)):
            if (i+1 <= len(self.endKeys)-1):
                currDelta = float(self.endKeys[i+1]) - float(self.endKeys[i])
                if (currDelta < self.delta):
                    self.delta = currDelta
 
 
    def findExtremes(self):
 
        # we also need to find the absolute min & max values
        # so we know how to scale the plots
 
        self.lowest = min(self.startKeys)
        if (min(self.endKeys) < self.lowest) : self.lowest = min(self.endKeys)
 
        self.highest = max(self.startKeys)
        if (max(self.endKeys) > self.highest) : self.highest = max(self.endKeys)
 
        self.delta = float(self.delta)
        self.lowest = float(self.lowest)
        self.highest = float(self.highest)
 
 
    def calculateExtents(self, filename, format, valueFormatString):
 
        if (format == "pdf"):
            surface = cairo.PDFSurface (filename, 8.5*72, 11*72)
        elif (format == "ps"):
            surface = cairo.PSSurface(filename, 8.5*72, 11*72)
            surface.set_eps(True)
        elif (format == "svg"):
            surface = cairo.SVGSurface (filename, 8.5*72, 11*72)
        elif (format == "png"):
            surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, int(8.5*72), int(11*72))
        else:
            surface = cairo.PDFSurface (filename, 8.5*72, 11*72)
 
        cr = cairo.Context(surface)
        cr.save()
        cr.select_font_face(self.LABEL_FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        cr.set_font_size(self.LABEL_FONT_SIZE)
        cr.set_line_width(self.LINE_WIDTH)
 
        # find the *real* maximum label width (not just based on number of chars)
 
        maxLabelWidth = 0
        maxNumWidth = 0
 
        for k in sorted(self.startKeys):
            s1 = self.starts[k]
            xbearing, ybearing, self.sWidth, self.sHeight, xadvance, yadvance = (cr.text_extents(s1))
            if (self.sWidth > maxLabelWidth) : maxLabelWidth = self.sWidth
            xbearing, ybearing, self.startMaxLabelWidth, startMaxLabelHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (k)))
            if (self.startMaxLabelWidth > maxNumWidth) : maxNumWidth = self.startMaxLabelWidth
 
        self.sWidth = maxLabelWidth
        self.startMaxLabelWidth = maxNumWidth
 
        maxLabelWidth = 0
        maxNumWidth = 0
 
        for k in sorted(self.endKeys):
            e1 = self.ends[k]
            xbearing, ybearing, self.eWidth, eHeight, xadvance, yadvance = (cr.text_extents(e1))
            if (self.eWidth > maxLabelWidth) : maxLabelWidth = self.eWidth
            xbearing, ybearing, self.endMaxLabelWidth, endMaxLabelHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (k)))
            if (self.endMaxLabelWidth > maxNumWidth) : maxNumWidth = self.endMaxLabelWidth
 
        self.eWidth = maxLabelWidth
        self.endMaxLabelWidth = maxNumWidth 
 
        cr.restore()
        cr.show_page()
        surface.finish()
 
        self.width = self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + self.startMaxLabelWidth + self.SPACE_WIDTH + self.SLOPE_LENGTH + self.SPACE_WIDTH + self.endMaxLabelWidth + self.SPACE_WIDTH + self.eWidth + self.X_MARGIN ;
        self.height = (self.Y_MARGIN * 2) + (((self.highest - self.lowest) / self.delta) * self.LINE_HEIGHT)
 
        self.HEADER_SPACE = 0.0
        if (self.HEADER_FONT_FAMILY != None):
            self.HEADER_SPACE = self.HEADER_FONT_SIZE + 2*self.LINE_HEIGHT
            self.height += self.HEADER_SPACE
 
 
    def makeSlopegraph(self, filename, config):
 
        (lab_r,lab_g,lab_b) = split(config["label_color"],2)        
        LAB_R = (int(lab_r, 16)/255.0)
        LAB_G = (int(lab_g, 16)/255.0)
        LAB_B = (int(lab_b, 16)/255.0)
 
        (val_r,val_g,val_b) = split(config["value_color"],2)
        VAL_R = (int(val_r, 16)/255.0)
        VAL_G = (int(val_g, 16)/255.0)
        VAL_B = (int(val_b, 16)/255.0)
 
        (line_r,line_g,line_b) = split(config["slope_color"],2)
        LINE_R = (int(line_r, 16)/255.0)
        LINE_G = (int(line_g, 16)/255.0)
        LINE_B = (int(line_b, 16)/255.0)
 
        if (config["background_color"] != "transparent"):
            (bg_r,bg_g,bg_b) = split(config["background_color"],2)
            BG_R = (int(bg_r, 16)/255.0)
            BG_G = (int(bg_g, 16)/255.0)
            BG_B = (int(bg_b, 16)/255.0)
 
        if (config['format'] == "pdf"):
            surface = cairo.PDFSurface (filename, self.width, self.height)
        elif (config['format'] == "ps"):
            surface = cairo.PSSurface(filename, self.width, self.height)
            surface.set_eps(True)
        elif (config['format'] == "svg"):
            surface = cairo.SVGSurface (filename, self.width, self.height)
        elif (config['format'] == "png"):
            surface = cairo.ImageSurface (cairo.FORMAT_ARGB32, int(self.width), int(self.height))
        else:
            surface = cairo.PDFSurface (filename, self.width, self.height)
 
        cr = cairo.Context(surface)
 
        cr.save()
 
        cr.set_line_width(self.LINE_WIDTH)
 
        if (config["background_color"] != "transparent"):
            cr.set_source_rgb(BG_R,BG_G,BG_B)
            cr.rectangle(0,0,self.width,self.height)
            cr.fill()
 
        # draw headers (if present)
 
        if (self.HEADER_FONT_FAMILY != None):
 
            (header_r,header_g,header_b) = split(config["header_color"],2)      
            HEADER_R = (int(header_r, 16)/255.0)
            HEADER_G = (int(header_g, 16)/255.0)
            HEADER_B = (int(header_b, 16)/255.0)
 
            cr.save()
 
            cr.select_font_face(self.HEADER_FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
            cr.set_font_size(self.HEADER_FONT_SIZE)
            cr.set_source_rgb(HEADER_R,HEADER_G,HEADER_B)
 
            xbearing, ybearing, hWidth, hHeight, xadvance, yadvance = (cr.text_extents(config["labels"][0]))            
            cr.move_to(self.X_MARGIN + self.sWidth - hWidth, self.Y_MARGIN + self.HEADER_FONT_SIZE)
            cr.show_text(config["labels"][0])
 
            xbearing, ybearing, hWidth, hHeight, xadvance, yadvance = (cr.text_extents(config["labels"][1]))            
            cr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth, self.Y_MARGIN + self.HEADER_FONT_SIZE)
            cr.show_text(config["labels"][1])
 
            cr.stroke()
 
            cr.restore()
 
        # draw start labels at the correct positions
 
        cr.select_font_face(self.LABEL_FONT_FAMILY, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
        cr.set_font_size(self.LABEL_FONT_SIZE)
 
        valueFormatString = config["value_format_string"]
 
        for k in sorted(self.startKeys):
 
            val = float(k)
            label = self.starts[k]
            xbearing, ybearing, lWidth, lHeight, xadvance, yadvance = (cr.text_extents(label))
            xbearing, ybearing, kWidth, kHeight, xadvance, yadvance = (cr.text_extents(valueFormatString % (val)))
 
            cr.set_source_rgb(LAB_R,LAB_G,LAB_B)
            cr.move_to(self.X_MARGIN + (self.sWidth - lWidth), self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1/self.delta))
            cr.show_text(label)
 
            cr.set_source_rgb(VAL_R,VAL_G,VAL_B)
            cr.move_to(self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + (self.startMaxLabelWidth - kWidth), self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1/self.delta))
            cr.show_text(valueFormatString % (val))
 
            cr.stroke()
 
        # draw end labels at the correct positions
 
        for k in sorted(self.endKeys):
 
            val = float(k)
            label = self.ends[k]
            xbearing, ybearing, lWidth, lHeight, xadvance, yadvance = (cr.text_extents(label))
 
            cr.set_source_rgb(VAL_R,VAL_G,VAL_B)
            cr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth - self.SPACE_WIDTH - self.endMaxLabelWidth, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1/self.delta))
            cr.show_text(valueFormatString % (val))
 
            cr.set_source_rgb(LAB_R,LAB_G,LAB_B)
            cr.move_to(self.width - self.X_MARGIN - self.SPACE_WIDTH - self.eWidth, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - val) * self.LINE_HEIGHT * (1/self.delta))
            cr.show_text(label)
 
            cr.stroke()
 
        # do the actual plotting
 
        cr.set_line_width(self.LINE_WIDTH)
        cr.set_source_rgb(LINE_R, LINE_G, LINE_B)
 
        for s1,e1 in self.pairs:
            cr.move_to(self.X_MARGIN + self.sWidth + self.SPACE_WIDTH + self.startMaxLabelWidth + self.LINE_START_DELTA, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - s1) * self.LINE_HEIGHT * (1/self.delta) - self.LINE_HEIGHT/4)
            cr.line_to(self.width - self.X_MARGIN - self.eWidth - self.SPACE_WIDTH - self.endMaxLabelWidth - self.LINE_START_DELTA, self.Y_MARGIN + self.HEADER_SPACE + (self.highest - e1) * self.LINE_HEIGHT * (1/self.delta) - self.LINE_HEIGHT/4)
            cr.stroke()
 
        cr.restore()
        cr.show_page()
 
        if (config['format'] == "png"):
            surface.write_to_png(filename)
 
        surface.finish()    
 
    def __init__(self, config):
 
        # since some methods need these, make them local to the class
 
        self.LABEL_FONT_FAMILY = config["label_font_family"]
        self.LABEL_FONT_SIZE = float(config["label_font_size"])
 
        if "header_font_family" in config:
            self.HEADER_FONT_FAMILY = config["header_font_family"]
            self.HEADER_FONT_SIZE = float(config["header_font_size"])
        else:
            self.HEADER_FONT_FAMILY = None
            self.HEADER_FONT_SIZE = None
 
        self.X_MARGIN = float(config["x_margin"])
        self.Y_MARGIN = float(config["y_margin"])
        self.LINE_WIDTH = float(config["line_width"])
 
        if "slope_length" in config:
            self.SLOPE_LENGTH = float(config["slope_length"])
        else:
            self.SLOPE_LENGTH = 300
 
        self.SPACE_WIDTH = self.LABEL_FONT_SIZE / 2.0
        self.LINE_HEIGHT = self.LABEL_FONT_SIZE + (self.LABEL_FONT_SIZE / 2.0)
        self.LINE_START_DELTA = 1.5*self.SPACE_WIDTH
 
        OUTPUT_FILE = config["output"] + "." + config["format"]
 
        # process the values & make the slopegraph
 
        self.readCSV(config["input"])
        self.sortKeys()
        self.findExtremes()
        self.calculateExtents(OUTPUT_FILE, config["format"], config["value_format_string"])
        self.makeSlopegraph(OUTPUT_FILE, config)
 
 
def main():
 
    parser = argparse.ArgumentParser(description="Creates a slopegraph from a CSV source")
    parser.add_argument("--config",required=True,                    help="config file name to use for  slopegraph creation",)
    args = parser.parse_args()
 
    if args.config:
 
        json_data = open(args.config)
        config = json.load(json_data)
        json_data.close()
 
        Slopegraph(config)
 
    return(0)
 
if __name__ == "__main__":
    main()