from collections import defaultdict
from shapely import geometry

import axi
import math
import random
import time

def circle(cx, cy, r, revs, points_per_rev):
    points = []
    a0 = random.random() * 2 * math.pi
    n = int(revs * points_per_rev)
    for i in range(n + 1):
        a = a0 + revs * 2 * math.pi * i / n
        x = cx + math.cos(a) * r
        y = cy + math.sin(a) * r
        points.append((x, y))
    return points

def fill_circle(cx, cy, r1, r2, revs, points_per_rev):
    points = []
    a0 = random.random() * 2 * math.pi
    n = int(revs * points_per_rev)
    for i in range(n + 1):
        a = a0 + revs * 2 * math.pi * i / n
        r = r1 + (r2 - r1) * min(1, float(i) / (n - points_per_rev))
        x = cx + math.cos(a) * r
        y = cy + math.sin(a) * r
        points.append((x, y))
    return points

def random_row(n):
    return bin(random.getrandbits(n) | (1 << n))[3:]

def compute_row(rule, previous):
    row = []
    previous = '00' + previous + '00'
    for i in range(len(previous) - 2):
        x = int(previous[i:i+3], 2)
        y = '1' if rule & (1 << x) else '0'
        row.append(y)
    return ''.join(row)

def compute_rows(rule, w, h):
    rows = [random_row(w * 16)]
    for _ in range(h - 1):
        rows.append(compute_row(rule, rows[-1]))
    return rows

def pad(rows):
    result = []
    n = len(max(rows, key=len))
    for row in rows:
        p = (n - len(row)) / 2 + 1
        row = '.' * p + row + '.' * p
        result.append(row)
    return result

def trim(rows):
    return [row.strip('.') for row in rows]

def crop(rows, n):
    w = len(rows[0])
    h = len(rows)
    i = w / 2 - n / 2
    j = i + n
    return [row[i:j] for row in rows]

def crop_diagonal(rows):
    rows = trim(rows)
    result = []
    for i, row in enumerate(rows):
        if i < len(rows) / 2:
            result.append(row)
        else:
            j = 2 * (i - len(rows) / 2 + 1)
            result.append(row[j:-j])
    return result

def trim_pair(pair, d):
    line = geometry.LineString(pair)
    p1 = line.interpolate(d)
    p2 = line.interpolate(line.length - d)
    return ((p1.x, p1.y), (p2.x, p2.y))

def form_pairs(rows):
    pairs = []
    rows = pad(rows)
    for y, row in enumerate(rows):
        if y == 0:
            continue
        for x, value in enumerate(row):
            if value != '1':
                continue
            i = x - len(rows[-1]) / 2
            j = y
            if rows[y - 1][x - 1] == '1':
                pairs.append(((i - 1, j - 1), (i, j)))
            if rows[y - 1][x] == '1':
                pairs.append(((i, j - 1), (i, j)))
            if rows[y - 1][x + 1] == '1':
                pairs.append(((i + 1, j - 1), (i, j)))
    points = set()
    for (x1, y1), (x2, y2) in pairs:
        points.add((x1, y1))
        points.add((x2, y2))
    return pairs, points

def create_drawing(rule, w, h):
    # print rule
    rows = compute_rows(rule, w, h)
    rows = pad(rows)
    rows = crop(rows, w)
    # rows = pad(rows)
    print len(rows[0]), len(rows)
    pairs, points = form_pairs(rows)
    counts = defaultdict(int)
    for a, b in pairs:
        counts[a] += 1
        counts[b] += 1
    # paths = [trim_pair(x, 0.25) for x in pairs]
    paths = pairs
    circle = axi.Drawing([fill_circle(0, 0, 0, 0.25, 2.5, 100)])
    # paths = []
    # paths = random.sample(pairs, len(pairs) / 2)
    for x, y in points:
        if counts[(x, y)] != 1:
            continue
        paths.extend(circle.translate(x, y).paths)
    d = axi.Drawing(paths)
    return d

def vertical_stack(ds, spacing=0):
    result = axi.Drawing()
    y = 0
    for d in ds:
        d = d.origin().translate(-d.width / 2, y)
        result.add(d)
        y += d.height + spacing
    return result

def horizontal_stack(ds, spacing=0):
    result = axi.Drawing()
    x = 0
    for d in ds:
        d = d.origin().translate(x, -d.height / 2)
        result.add(d)
        x += d.width + spacing
    return result

def title(rule):
    d1 = axi.Drawing(axi.text('Rule %d' % rule, axi.FUTURAM))
    d1 = d1.scale_to_fit_height(0.25)
    d2 = axi.Drawing(axi.text('Elementary Cellular Automaton', axi.FUTURAL))
    d2 = d2.scale_to_fit_height(0.1875)
    ds = [d1, d2]
    d = vertical_stack(ds, 0.125)
    d = d.join_paths(0.01)
    d = d.simplify_paths(0.001)
    return d

def decoder(rule):
    paths = []
    for i in range(8):
        for j in range(3):
            x = i * 4 + j
            on = i & (1 << j)
            if on:
                paths.append(fill_circle(x, 0, 0, 0.4, 8.5, 100))
            else:
                paths.append(circle(x, 0, 0.4, 2.5, 100))
        x = i * 4 + 1
        on = rule & (1 << i)
        if on:
            paths.append(fill_circle(x, 1, 0, 0.4, 8.5, 100))
        else:
            paths.append(circle(x, 1, 0.4, 2.5, 100))
    d = axi.Drawing(paths)
    d = d.scale_to_fit_width(8.5 * 2 / 3)
    d = d.scale(-1, 1)
    d = d.join_paths(0.01)
    d = d.simplify_paths(0.001)
    return d

def label(text):
    d = axi.Drawing(axi.text(text, axi.FUTURAL))
    d = d.scale_to_fit_height(0.125)
    d = d.rotate(-90)
    d = d.move(12, 8.5, 1, 1)
    d = d.join_paths(0.01)
    d = d.simplify_paths(0.001)
    return d

def multiple_label(text):
    d = axi.Drawing(axi.text(text, axi.FUTURAL))
    d = d.scale_to_fit_height(0.125)
    d = d.move(0, 8.5, 0, 1)
    d = d.join_paths(0.01)
    d = d.simplify_paths(0.001)
    return d

def single(number, rule, seed):
    if seed is None:
        seed = int(time.time() * 1000) % 100000
    path = '%d-%d-%d' % (number, rule, seed)
    random.seed(seed)
    # rule = 90
    w = 96
    h = 120
    d = create_drawing(rule, w, h)
    d = d.scale_to_fit_width(8.5)

    d = d.sort_paths()
    d = d.join_paths(0.01)
    d = d.simplify_paths(0.001)

    d = vertical_stack([title(rule), d, decoder(rule)], 0.25)
    d = d.rotate(-90)
    d = d.scale_to_fit(12, 8.5)
    d.add(label('#%d' % number))

    rotated = d.rotate(90).center(8.5, 12)

    d.dump(path + '.axi')
    im = rotated.render(
        scale=109 * 1, line_width=0.3/25.4,
        show_axi_bounds=False, use_axi_bounds=False)
    im.write_to_png(path + '.png')
    # axi.draw(d)

def multiple():
    w = 32
    h = 137

    # rules = [x for x in range(256) if bin(x).count('1') == 4]
    # rules = [18, 22, 26, 30, 41, 45, 54, 60, 73, 90, 105, 106, 110, 122, 126, 146, 150, 154]
    # rules = sorted(random.sample(rules, 6))
    # print rules

    # rules = sorted([22, 30, 60, 90, 106, 150, 105, 122, 154])
    rules = sorted([22, 30, 60, 90, 106, 150])
    ds = []
    for rule in rules:
        d1 = create_drawing(rule, w, h)
        d1 = d1.scale_to_fit_height(8)
        d2 = axi.Drawing(axi.text('Rule %d' % rule, axi.FUTURAL))
        d2 = d2.scale_to_fit_height(0.125)
        d = vertical_stack([d1, d2], 0.125)
        ds.append(d)
    title = axi.Drawing(axi.text('Elementary Cellular Automata', axi.FUTURAM))
    title = title.scale_to_fit_height(0.25)
    d = horizontal_stack(ds, 0.25)
    d = vertical_stack([title, d], 0.2)
    d = d.scale_to_fit(12, 8.5)
    # d.add(multiple_label('#31'))
    print len(d.paths)
    print 'joining paths'
    d = d.join_paths(0.01)
    print len(d.paths)
    print 'sorting paths'
    d = d.sort_paths()
    print len(d.paths)
    print 'joining paths'
    d = d.join_paths(0.01)
    print len(d.paths)
    print 'simplifying paths'
    d = d.simplify_paths(0.001)
    print d.bounds
    d.dump('out.axi')
    im = d.render(scale=109 * 1, line_width=0.3/25.4, show_axi_bounds=False)
    im.write_to_png('out.png')
    # axi.draw(d)

def main():
    # number = 29
    # rule = 90
    # seed = None
    # single(number, rule, seed)
    # for seed in range(10, 20):
    #     single(number, rule, seed)
    multiple()

if __name__ == '__main__':
    main()
