#!/usr/bin/env python3
"""
--documentation--
"""

__author__ = 'Peter Kleiweg'
__version__ = '0.3'
__date__ = '2020/09/16'


#| imports

import getopt, glob, math, os, re, sys, tempfile
import cairo


#| colors and sizes

colorvals = ((255, 255, 217),
             (237, 248, 177),
             (199, 233, 180),
             (127, 205, 187),
             ( 65, 182, 196),
             ( 29, 145, 192),
             ( 34,  94, 168),
             ( 37,  52, 148),
             (  8,  29,  88))

large = {'fontsize': 14,
         'dy': 4.0,
         'dylbl': 18,
         'numsub': 5,
         'lblsub': 12,
         'linewidth': 1.0,
         'c1': -16,
         'ch': 18}

medium = {'fontsize': 11,
          'dy': 1.5,
          'dylbl': 11,
          'numsub': 4,
          'lblsub': 8,
          'linewidth': .5,
          'c1': -10,
          'ch': 11}

small = {'fontsize': 8,
         'dy': .5,
         'dylbl': 7,
         'numsub': 3,
         'lblsub': 6,
         'linewidth': .5,
         'c1': -7,
         'ch': 7}


#| globals

outfile = ''
size = medium
methods = []
noise = .5
runs = 100
margin = 4
exp = 1.0
colorfile = ''
coloreq = False
colorraw = False
percentage = 50.001
showpercent = True
colorlinks = False

encoding = 'iso-8859-1'
writefile = ''
outtype = 'pdf'
outtypedefined = False
ystack = []
y = 800
dpi = 72
scale = 1
vextra = 0
ruler = True
font = 'sans-serif'


#| functions

def doLabel(s):
    global y
    ext = ctx.text_extents(s)
    ctx.move_to(98 - ext.width - ext.x_bearing, y + lblsub + vextra / 2)
    ctx.show_text(s)
    ctx.set_source_rgb(0, 0, 0)
    y += (dylbl + vextra)

def doCL(r, g, b):
    ctx.set_source_rgb(r, g, b)
    ctx.rectangle(cx, y - c1 - dylbl, 99 - cx, ch + vextra)
    ctx.fill()
    if r*.2126 + g*.7152 + b*.0722 > .5:
        ctx.set_source_rgb(0, 0, 0)
    else:
        ctx.set_source_rgb(1, 1, 1)

def doNend(n, f):
    global y
    if colorlinks:
        i = int((n - percentage) / (100 - percentage) * 9.0)
        if i > 8:
            i = 8
        r, g, b = colorvals[i]
        ctx.set_source_rgb(r / 255, g / 255, b / 255)
    f = math.pow(f, exp)
    y1 = ystack.pop()
    x2 = 100 + f * dx
    ctx.move_to(100, y)
    ctx.line_to(x2, y)
    ctx.line_to(x2, y1)
    ctx.line_to(100, y1)
    ctx.stroke()
    ctx.set_source_rgb(0, 0, 0)
    if showpercent:
        ctx.move_to(x2 + 2, (y+y1)/2 + numsub)
        ctx.show_text('{}'.format(int(n+.5)))
    y += dy

def doRuler():
    global y
    y += dylbl * 1.5
    i = 0
    while i <= Max:
        x = math.pow(i, Exp) * dx + 100
        ctx.move_to(x, y)
        ctx.line_to(x, y-2)
        i += RulerStep / 5
    i = 0
    ctx.stroke()
    while i <= Max:
        x = math.pow(i, Exp) * dx + 100
        ctx.move_to(x, y)
        ctx.line_to(x, y-4)
        ctx.stroke()
        s = frm.format(i)
        ctx.move_to(x - strlen(s) / 2, y + size['fontsize'])
        ctx.show_text(s)
        i += RulerStep
    ctx.move_to(100, y)
    ctx.line_to(math.pow(Max, Exp) * dx + 100, y)
    ctx.stroke()

def strlen(s):
    e = ctx.text_extents(s)
    return e.width + e.x_bearing

def getline(fp):
    while True:
        line = fp.readline()
        if not line:
            return False
        line = line.strip()
        if line and line[0] != '#':
            return line

reUnquote = re.compile(r"\\(\\|\(|\)|[0-7][0-7][0-7])".encode())

def unq(m):
    m1 = m.group(1)
    if m1 == b'\\':
        return b'\\'
    elif m1 == b'(':
        return b'('
    elif m1 == b')':
        return b')'
    else:
        c = int(m1.decode(), 8)
        return bytes([c])

def unquote(s):
    s1 = s.encode()
    s1 = reUnquote.sub(unq, s1)
    return s1.decode(encoding)

def usage():
    sys.stderr.write("""
Usage: %(progname)s [more options] [-m string] [-n float] [-r int] difference_file
Usage: %(progname)s [more options] [-m string] difference_file ...
Usage: %(progname)s [more options] cluster_file ...

 -L           : large output
 -M           : medium output (default)
 -S           : small output

 -c  filename : coloured labels (a 3D vector file)
 -Ce          : (with -c) equivalent scaling of colour axes
 -Cr          : (with -c) raw input, no scaling of colour axes
 -D  int      : for png only: dots per inch (default: 72)
 -e  float    : exponent (default: 1.0)
 -f           : no ruler
 -F           : serif font (default: sans-serif)
 -g           : coloured links
 -G           : output png (default: pdf)
 -m  string   : cluster method: sl cl ga wa uc wc wm (default: wa)
 -n  float    : noise level (default: 0.5)
 -o  filename : output file, if name ends in .png, the png format is selected
 -p  float    : minimum percentage (default: 50.001)
 -P           : don't print percentage
 -r  int      : number of runs (default: 100)
 -s  int      : extra spacing between lines (default: 0)
 -u           : utf-8 input encoding (default: iso-8859-1)
 -v  int      : margin (default: 4)

Option -m can be used multiple times

""" % globals())
    sys.exit()


#| set-up

dirname, progname = os.path.split(os.path.realpath(sys.argv[0]))

os.environ['PATH'] = dirname + os.path.pathsep + os.environ['PATH']

optd, argv = getopt.getopt(sys.argv[1:], 'LMSe:fgm:n:r:v:c:C:p:PD:FGo:s:u')

for op, val in optd:
    if op == '-C':
        if val == 'e':
            coloreq = True
            colorraw = False
        elif val == 'r':
            coloreq = False
            colorraw = True
    elif op == '-L':
        size = large
    elif op == '-M':
        size = medium
    elif op == '-S':
        size = small
    elif op == '-c':
        colorfile = val
    elif op == '-D':
        dpi = int(val)
    elif op == '-e':
        exp = float(val)
    elif op == '-f':
        ruler = False
    elif op == '-F':
        font = 'serif'
    elif op == '-g':
        colorlinks = True
    elif op == '-G':
        outtype = 'png'
        outtypedefined = True
    elif op == '-m':
        assert re.match('(sl|cl|ga|wa|uc|wc|wm)$', val)
        methods.append(val)
    elif op == '-n':
        noise = float(val)
    elif op == '-o':
        outfile = val
    elif op == '-p':
        percentage = float(val)
    elif op == '-P':
        showpercent = False
    elif op == '-r':
        runs = int(val)
    elif op == '-s':
        vextra = int(val)
    elif op == '-u':
        encoding = 'utf-8'
    elif op == '-v':
        margin = int(val)
    else:
        assert False
if not methods:
    methods = ['wa']

argv1 = argv
argv = []
for arg in argv1:
    for f in glob.glob(arg):
        argv.append(f)
if not argv:
    usage()

fp = open(argv[0], 'rt', encoding=encoding)
items = getline(fp).split()
fp.close()
if len(items) == 1:
    if len(argv) == 1:
        what = 'onedif'
    else:
        what = 'manydifs'
else:
    what = 'clus'


#| colorfile

if colorfile:
    colors = {}
    fp = open(colorfile, 'rt', encoding=encoding)
    i = int(getline(fp))
    assert i == 3
    while True:
        lbl = getline(fp)
        if not lbl:
            break
        r = float(getline(fp))
        g = float(getline(fp))
        b = float(getline(fp))
        colors[lbl] = [r, g, b]
    fp.close()
    if not colorraw:
        r1 = r2 = r
        g1 = g2 = g
        b1 = b2 = b
        for lbl in colors:
            if colors[lbl][0] < r1: r1 = colors[lbl][0]
            if colors[lbl][0] > r2: r2 = colors[lbl][0]
            if colors[lbl][1] < g1: g1 = colors[lbl][1]
            if colors[lbl][1] > g2: g2 = colors[lbl][1]
            if colors[lbl][2] < b1: b1 = colors[lbl][2]
            if colors[lbl][2] > b2: b2 = colors[lbl][2]
        if coloreq:
            m = max(r2 - r1, g2 - g1, b2 - b1)
            rs = (m - (r2 - r1)) / (2.0 * m)
            gs = (m - (g2 - g1)) / (2.0 * m)
            bs = (m - (b2 - b1)) / (2.0 * m)
            for lbl in colors:
                colors[lbl][0] = (colors[lbl][0] - r1) / m + rs
                colors[lbl][1] = (colors[lbl][1] - g1) / m + gs
                colors[lbl][2] = (colors[lbl][2] - b1) / m + bs
        else:
            for lbl in colors:
                colors[lbl][0] = (colors[lbl][0] - r1) / (r2 - r1)
                colors[lbl][1] = (colors[lbl][1] - g1) / (g2 - g1)
                colors[lbl][2] = (colors[lbl][2] - b1) / (b2 - b1)


#| main

tempdir = tempfile.mkdtemp()

if outfile == '':
    writefile = os.path.join(tempdir, 'outfile')
else:
    writefile = outfile
    if not outtypedefined:
        if outfile.endswith('.png'):
            outtype = 'png'

#| try: main

try:

    #| clustering

    filelist = os.path.join(tempdir, 'filelist')
    fpl = open(filelist, 'wt')
    if what == 'onedif':
        diffile = argv[0]
        seed = 0
        for run in range(runs):
            for method in methods:
                f = os.path.join(tempdir, 'f%(run)03i-%(method)s.clu' % vars())
                fpl.write(f + '\n')
                seed += 1
                os.system('cluster -%(method)s -N %(noise)g -s %(seed)i -o %(f)s %(diffile)s' % vars())
    elif what == 'manydifs':
        i = 0
        for diffile in argv:
            for method in methods:
                f = os.path.join(tempdir, 'f%(i)03i-%(method)s.clu' % vars())
                fpl.write(f + '\n')
                os.system('cluster -%(method)s -o %(f)s %(diffile)s' % vars())
            i += 1
    else:
        for f in argv:
            fpl.write(f + '\n')
    fpl.close()

    gr = os.path.join(tempdir, 'groups')
    os.system('agclus -o %s -l %s' % (gr, filelist))

    fp = os.popen('agden -p %f %s' % (percentage, gr), 'r')
    result = fp.readlines()
    fp.close()


    #| longest string and maximum value

    if outtype == 'pdf':
        surface = cairo.PDFSurface(None, 1, 1)
    else:
        surface = cairo.ImageSurface(cairo.Format.ARGB32, 1, 1)
    ctx = cairo.Context(surface)
    ctx.select_font_face(font)
    ctx.set_font_size(size['fontsize'])

    if outtype == 'png':
        if dpi != 72:
            scale = int(dpi / 72.0 + .5)
            ctx.scale(scale, scale)

    fp = open(gr, 'rt', encoding=encoding)
    n = int(getline(fp))
    n = int(getline(fp))
    maxlbl = 0
    for i in range(n):
        l = strlen(getline(fp))
        if l > maxlbl:
            maxlbl = l
    maxdif = 0
    while True:
        line = getline(fp)
        if not line:
            break
        f = float (line.split()[2])
        if f > maxdif:
            maxdif = f
    fp.close()

    #| interval on ruler

    minvalue = 0.0
    step = pow(10.0, math.ceil(math.log10(maxdif - minvalue)) - 1.0)
    if ((maxdif - minvalue) / step > 6.0):
        step *= 2.0
    elif ((maxdif - minvalue) / step < 3.0):
        step *= 0.5

    frm = '{:.0}'
    if step < 1:
        a = '{:.1g}'.format(step)
        n = 0
        for c in a:
            if c == '0':
                n += 1
        frm = '{{:.{}f}}'.format(n)

    #| bounding box

    x1 = 96 - int(maxlbl + .5)
    x2 = 504
    if showpercent:
        x2 += int(strlen('100') + .5)
    y2 = 804
    y1 = 798.0
    for line in result:
        if line.startswith('nstart'):
            y1 -= size['dy'] * 2.0
        elif line[0] == '(':
            y1 -= (size['dylbl'] + vextra)
    if ruler:
        y1 -= 1.5 * size['dylbl']
        y1 -= size['fontsize']
    y1 = int(y1 - .5)

    y = y1 + 4

    surface.finish()

    #| output

    if outtype == 'pdf':
        surface = cairo.PDFSurface(writefile, x2 - x1 + 2 * margin, y2 - y1 + 2 * margin)
    else:
        surface = cairo.ImageSurface(cairo.Format.ARGB32, (x2 - x1 + 2 * margin) * scale, (y2 - y1 + 2 * margin) * scale)
    ctx = cairo.Context(surface)
    ctx.select_font_face(font)
    ctx.set_font_size(size['fontsize'])

    if outtype == 'png':
        if dpi != 72:
            ctx.scale(scale, scale)

    ctx.translate(margin-x1, margin-y1)
    ctx.set_source_rgb(1, 1, 1)
    ctx.rectangle(x1-margin, y1-margin, x2-x1+2*margin, y2-y1+2*margin)
    ctx.fill()
    ctx.set_source_rgb(0, 0, 0)

    dy = size['dy']
    dylbl = size['dylbl']
    numsub = size['numsub']
    lblsub = size['lblsub']
    linewidth = size['linewidth']
    c1 = size['c1']
    ch = size['ch']

    ctx.set_line_width(linewidth)

    cx = x1 + 1
    dx = 400.0 / (maxdif ** exp)
    Exp = exp
    RulerStep = step
    Max = maxdif
    for line in result:
        line = line.strip()
        if line[0] == '(':
            m = re.match(r'\((.*)\)', line)
            lbl = unquote(m.group(1))
            if colorfile:
                doCL(colors[lbl][0], colors[lbl][1], colors[lbl][2])
            doLabel(lbl)
        elif line == 'nstart':
            ystack.append(y)
            y += dy
        elif line.endswith(' nend'):
            a = line.split()
            doNend(float(a[0]), float(a[1]))
    if ruler:
        doRuler()

    surface.flush()

    if outtype == 'png':
        surface.write_to_png(writefile)

    surface.finish()

    if not outfile:
        fp = open(writefile, "rb")
        sys.stdout.buffer.write(fp.read())
        fp.close()

#| finally: clean up

finally:

    for f in os.listdir(tempdir):
        os.remove(os.path.join(tempdir, f))
    os.rmdir(tempdir)
