#!/usr/bin/env python
# Note:
# Originally written by guanqun (https://github.com/guanqun/) Sep 29, 2011
# Edited by Intrepid (https://github.com/intrepid/) Apr 12, 2012

import math
import cairo
import sys
import subprocess
import os
from email.utils import mktime_tz, parsedate_tz, formatdate

# defaults
author = ''
width = 1100
output_file = 'output.png'

if len(sys.argv) > 1:
    if sys.argv[1] == "--help" or sys.argv[1] == "-h":
        print("SYNTAX:    git punchchard")
        print
        print("EXAMPLE:   git punchchard file=outputfile.png width=4000")
        print("This creates 'outputfile.png', a 4000px wide png image")
        print
        print("EXAMPLE:   git punchcard path=/home/siddharth/code/punchcard")
        print("This creates a punch card of the git repository at the given path")
        print
        print("EXAMPLE:   git punchcard opaque=0")
        print("This sets the opacity of all the circles to constant value 0")
        print("0 -> Black, 1 -> White (invisible)")
        print
        print("EXAMPLE:   git punchcard timezone=+7.5")
        print("This shows the graph with all times converted to timezone UTC+7.5")
        print("If your contributors live in multiple timezones, this helps you")
        print("in getting a relative estimate of when they work.")
        print
        sys.exit(0)

original_path = os.getcwd()
path = os.getcwd()
opaque = -1
utc = 0
timezone = None
gitopts = ""

options = dict()

if len(sys.argv) > 1:
    options = dict(arg.split('=', 1) for arg in sys.argv[1:])
if 'width' in options:
    width = int(options['width'])
if 'author' in options:
    author = options['author']
if 'file' in options:
    output_file = options["file"]
if 'path' in options:
    path = options["path"]
if 'opaque' in options:
    try:
        opaque = float(options["opaque"])
    except ValueError as error:
        print("WARNING: Could not parse the opaque argument you gave. Please \
        enter a value between 0 and 1")
        opaque = -1
if 'utc' in options:
    utc = options['utc'] == '1'
    timezone = 0
if 'timezone' in options:
    try:
        timezone = float(options["timezone"])
    except ValueError as error:
        print("WARNING: COULD not parse timezone argument.")
        print("Please enter a decimal value. Eg: 3.0, -11.5")
        timezone = None

if 'gitopts' in options:
    gitopts = options['gitopts']

height = int(round(width/2.75, 0))

# Calculate the relative distance
distance = int(math.sqrt((width*height)/270.5))

# Round the distance to a number divisible by 2
if distance % 2 == 1:
    distance -= 1

max_range = (distance/2) ** 2

# Good values for the relative position
left = width/18 + 10  # The '+ 10' is to prevent text from overlapping 
top = height/20 + 10
indicator_length = height/20

days = ['Sat', 'Fri', 'Thu', 'Wed', 'Tue', 'Mon', 'Sun']
hours = ['12am'] + [str(x) for x in range(1, 12)] + ['12pm'] + [str(x) for x in range(1, 12)]
def get_x_y_from_date(day, hour):
    y = top + (days.index(day) + 1) * distance
    x = left + (hour + 1) * distance
    return x, y

def get_log_data():
    try:
        os.chdir(path)
        print('current path: ', os.getcwd())
        gitcommand = ['git', 'log', '--no-merges', '--author='+author, \
                '--pretty=format: %aD']

        if gitopts != "":
            gitcommand.append(gitopts)

        print('run cmd: ', ' '.join(gitcommand))

        p = subprocess.Popen(gitcommand,
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             universal_newlines=True)
        outdata, errdata = p.communicate()
    except OSError as e:
        print('Git not installed?')
        sys.exit(-1)
    if outdata == '':
        print('Not a git repository?')
        sys.exit(-1)
    return outdata

# get day and hour
temp_log = get_log_data()

# CONVERT EVERYTHING TO UTC or ADD TIMEZONE OFFSET

utc_offset = timezone != None and timezone * 3600 or 0

fixed_stamps = temp_log.split('\n')

if timezone != None:
    fixed_stamps = [formatdate(mktime_tz(parsedate_tz(x)) + utc_offset) \
            for x in fixed_stamps]

data_log = [[x.strip().split(',')[0], x.strip().split(' ')[4].split(':')[0]] \
        for x in fixed_stamps]

stats = {}
for d in days:
    stats[d] = {}
    for h in range(0, 24):
        stats[d][h] = 0

total = 0
for line in data_log:
    stats[ line[0] ][ int(line[1]) ] += 1
    total += 1

def get_length(nr):
    if nr == 0:
        return 0
    for i in range(1, distance//2):
        if i*i <= nr and nr < (i+1)*(i+1):
            return i
    if nr == max_range:
        return distance/2-1

# normalize
all_values = []
for d, hour_pair in stats.items():
    for h, value in hour_pair.items():
        all_values.append(value)
max_value = max(all_values)
final_data = []
for d, hour_pair in stats.items():
    for h, value in hour_pair.items():
        final_data.append( [ get_length(int( float(stats[d][h]) / max_value * max_range )), get_x_y_from_date(d, h) ] )

surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
cr = cairo.Context(surface)

cr.set_line_width (1)

# draw background to white
cr.set_source_rgb(1, 1, 1)
cr.rectangle(0, 0, width, height)
cr.fill()

# set black
cr.set_source_rgb(0, 0, 0)

# draw x-axis and y-axis
cr.move_to(left, top)
cr.rel_line_to(0, 8 * distance)
cr.rel_line_to(25 * distance, 0)
cr.stroke()

# draw indicators on x-axis and y-axis
x, y = left, top
for i in range(8):
    cr.move_to(x, y)
    cr.rel_line_to(-indicator_length, 0)
    cr.stroke()
    y += distance

x += distance
for i in range(25):
    cr.move_to(x, y)
    cr.rel_line_to(0, indicator_length)
    cr.stroke()
    x += distance

# select font
cr.select_font_face ('sans-serif', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)

# and set a appropriate font size
cr.set_font_size(math.sqrt((width*height)/3055.6))

# draw Mon, Sat, ... Sun on y-axis
x, y = (left - 5), (top + distance)
for i in range(7):
    x_bearing, y_bearing, width, height, x_advance, y_advance = cr.text_extents(days[i])
    cr.move_to(x - indicator_length - width, y + height/2)
    cr.show_text(days[i])
    y += distance

# draw 12am, 1, ... 11 on x-axis
x, y = (left + distance), (top + (7 + 1) * distance + 5)
for i in range(24):
    x_bearing, y_bearing, width, height, x_advance, y_advance = cr.text_extents(hours[i])
    cr.move_to(x - width/2 - x_bearing, y + indicator_length + height)
    cr.show_text(hours[i])
    x += distance

# draw circles according to their frequency
def draw_circle(pos, length):
    # find the position
    # max of length is half of the distance
    x, y = pos
    clr = (1 - float(length * length) / max_range )
    if opaque >= 0 and opaque < 1:
        clr = opaque
    cr.set_source_rgba (clr, clr, clr)
    cr.move_to(x, y)
    cr.arc(x, y, length, 0, 2 * math.pi)
    cr.fill()

for each in final_data:
    draw_circle(each[1], each[0])

# write to output
os.chdir(original_path)
surface.write_to_png (output_file)

print("punchcard written to output file at: %s" % os.path.join(original_path, output_file))