#!/usr/bin/env python3
"""Pretty, table-aligned display of recent commits.

First argument can be an integer, and that number of commits will be displayed.
"""

from dataclasses import dataclass
from datetime import datetime
from typing import List, Union, Any
import re
import subprocess
import sys

if len(sys.argv) > 3:
    raise Exception('Maximum 2 args')


def isNum(arg):
    try:
        int(arg)
        return True
    except ValueError:
        return False

numbers = list(filter(isNum, sys.argv[1:]))
non_numbers = list(filter(lambda arg: not isNum(arg), sys.argv[1:]))

if len(numbers) > 1:
    raise Exception('Not sure which args are numbers: %s' % str(sys.argv))

number = '10'
if len(numbers) == 1:
    number = str(numbers[0])

if len(non_numbers) > 1:
    raise Exception('Not sure which args are refs: %s' % str(sys.argv))

branch = non_numbers[0] if len(non_numbers) else 'HEAD'

@dataclass
class Commit:
    refnames: List[str]
    sha: str
    reldate: str
    author: str
    date: datetime
    message: str

delim = '|||'
cmd = ['git', 'log', '--format=' + delim.join(['%d', '%h', '%cr', '%an', '%ci', '%s']), '-n', number, branch, '--']
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()
if err:
    raise Exception(err.decode())
lines = out.decode('utf8').splitlines()

reldate_subs = [
    (' weeks?', 'wk'),
    (' years?', 'yr'),
    (' months?', 'mo'),
    (' days?', 'd'),
    (' hours?', 'h'),
    (' minutes?', 'm'),
    (' seconds?', 's'),
    (',', ''),
    (' ago', '')
]


def shorten_reldate(reldate):
    for sub in reldate_subs:
        reldate = re.sub(sub[0], sub[1], reldate)
    return reldate


# This is an approximation of valid branch names. The real rules are here:
# https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
normal_refname_regex = r'[^ ~^:?*\[@\\]+'

refname_or_tag_regex = "(?:(?:HEAD -> )?%s|tag: %s)" % (normal_refname_regex, normal_refname_regex)


def parse_refnames(refnames):
    match = re.match('^\((?P<names>(?:%s, )*%s)\)$' %
                        (refname_or_tag_regex, refname_or_tag_regex), refnames)
    if (match):
        return match.group('names').split(', ')
    else:
        return []

def parse_commit(line) -> Commit:
    refnames, sha, reldate, author, date, message = line.split(delim)
    return Commit(
        refnames=parse_refnames(refnames.strip()),
        sha=sha,
        reldate=shorten_reldate(reldate),
        author=author,
        date=datetime.strptime(date, '%Y-%m-%d %H:%M:%S %z'),
        message=message,
    )

cOff = "\x1b[0m"
cYellowIntense = "\x1b[0;93m"
cYellow = "\x1b[0;33m"
cRed = "\x1b[0;91m"
cCyan = "\x1b[0;36m"
cBlue = "\x1b[0;94m"
cGreen = "\x1b[0;92m"


def color(c: str, text: str) -> str:
    return [c, text, cOff]


class RefNameCol:
    @staticmethod
    def text(commit: Commit):
        return ' '.join(x.replace('tag: ', '') for x in commit.refnames)

    @staticmethod
    def fmt(commit: Commit):
        parts = [cYellow]
        for i, refname in enumerate(commit.refnames):
            if i:
                parts.append(' ')
            if refname.startswith('tag: '):
                parts += [cYellowIntense, refname[5:], cYellow]
            else:
                parts.append(refname)
        parts.append(cOff)
        return parts

class ColorColumn:
    def __init__(self, field, color):
        self.color = color
        self.field = field

    def text(self, commit: Commit):
        return getattr(commit, self.field)

    def fmt(self, commit: Commit):
        return color(self.color, self.text(commit))

CommitSha = ColorColumn('sha', cRed)
RelDate = ColorColumn('reldate', cGreen)
Author = ColorColumn('author', cCyan)
Message = ColorColumn('message', cOff)

class CommitDate:
    @staticmethod
    def text(commit: Commit):
        return commit.date.strftime('%Y-%m-%d %H:%M:%S')

    @staticmethod
    def fmt(commit: Commit):
        return color(cBlue, CommitDate.text(commit))


@dataclass
class Column:
    max_len: Union[int, None]
    formatter: Any
    fixed_width: bool


columns = [
    Column(max_len=28, formatter=RefNameCol, fixed_width=True),
    Column(max_len=None, formatter=CommitSha, fixed_width=True),
    Column(max_len=None, formatter=RelDate, fixed_width=True),
    Column(max_len=None, formatter=Author, fixed_width=True),
    Column(max_len=None, formatter=CommitDate, fixed_width=True),
    Column(max_len=None, formatter=Message, fixed_width=False)
]


commits = [parse_commit(line) for line in lines]
widths = []
for c in columns:
    max_len = max(len(c.formatter.text(commit)) for commit in commits)
    if c.max_len and max_len > c.max_len:
        max_len = c.max_len
    widths.append(max_len)

def pad_or_clamp(txt: List[str], w: int) -> str:
    total_len = 0
    out = []
    for piece in txt:
        if piece.startswith('\x1b['):
            out.append(piece)
        else:
            if total_len + len(piece) > w:
                out.append(piece[:w - total_len])
                total_len = w
                break
            else:
                out.append(piece)
                total_len += len(piece)

    if total_len < w:
        out = [' ' * (w - total_len)] + out
    return ''.join(out) + cOff

for commit in commits:
    for c, width in zip(columns, widths):
        fmt = c.formatter.fmt(commit)
        if c.fixed_width:
            print(pad_or_clamp(fmt, width), end=' ')
        else:
            print(''.join(fmt), end=' ')
    print(cOff)
