// Ported from Michael Fogleman's generative snowflakes web app:
// https://www.michaelfogleman.com/static/snowflakes/
// https://twitter.com/FogleBird/status/1356082505699500034?s=20

const rows = 5; // min=1, max=49, step=1
const cols = rows;
const show_seeds = 0; // min=0, max=1, step=1
const predefined_seeds = 0; // min=0, max=1000, step=1

const default_minIterations = 6; // min=3, max=15, step=1
const default_maxIterations = 15; // min=10, max=100, step=1
const default_minCells = 32; // min=10, max=100, step=1
const default_off_bias = 1.0; // min=0.1, max=2, step=0.1
const default_on_bias = 1.0; // min=0.1, max=2, step=0.1

const global_scale = 5 / Math.max(rows, cols);

const saved_min_max_seeds = [
    // min, max, cells, off, on, seed
    [6, 10, 32, 1, 1, 385],
    [3, 20, 32, 1, 1, 228],
    [6, 10, 32, 1, 1, 276],
    [6, 10, 32, 1, 1, 336],
    [6, 10, 32, 1, 1, 1059],
    [6, 10, 32, 1, 1, 1078],
    [6, 15, 32, 1, 1, 3749],
    [6, 10, 32, 1, 1, 8763192680],
    [6, 10, 32, 1, 1, 191708363],
    [6, 10, 32, 1, 1, 9941172564],
    [6, 10, 32, 1, 1, 567728039],
    [6, 10, 32, 1, 1, 561975500],
    [6, 10, 32, 1, 1, 305913754],
    //[3, 30, 13, 1, 1, 479046], // Slow
    [10, 15, 100, 1, 1, 570],
    [12, 28, 73, 1, 1, 32022]
];

var off_bias = default_off_bias;
var on_bias = default_on_bias;

var line_count = 0;

const lookup = [
    0, 1, 1, 2, 1, 3, 2, 4,
    1, 5, 3, 6, 2, 6, 4, 7,
    1, 3, 5, 6, 3, 8, 6, 9,
    2, 6, 6, 10, 4, 9, 7, 11,
    1, 2, 3, 4, 5, 6, 6, 7,
    3, 6, 8, 9, 6, 10, 9, 11,
    2, 4, 6, 7, 6, 9, 10, 11,
    4, 7, 9, 11, 7, 11, 11, 12,
];

function pack(p) {
    return p[0] + "," + p[1];
}

function unpack(p) {
    const s = p.split(",");
    const x = parseInt(s[0]);
    const y = parseInt(s[1]);
    return [x, y];
}

function add(cells, p) {
    cells.add(pack(p));
}

function has(cells, p) {
    return cells.has(pack(p));
}

function mask(cells, p) {
    const x = p[0];
    const y = p[1];
    let result = 0;
    if (has(cells, [x + 1, y])) {
        result |= 1;
    }
    if (has(cells, [x + 1, y - 1])) {
        result |= 2;
    }
    if (has(cells, [x, y - 1])) {
        result |= 4;
    }
    if (has(cells, [x - 1, y])) {
        result |= 8;
    }
    if (has(cells, [x - 1, y + 1])) {
        result |= 16;
    }
    if (has(cells, [x, y + 1])) {
        result |= 32;
    }
    return result;
}

function neighbors(p) {
    const x = p[0];
    const y = p[1];
    let result = [];
    result.push([x + 1, y]);
    result.push([x + 1, y - 1]);
    result.push([x, y - 1]);
    result.push([x - 1, y]);
    result.push([x - 1, y + 1]);
    result.push([x, y + 1]);
    return result;
}

function neighborhood(cells) {
    let result = new Set();
    for (let p of cells) {
        p = unpack(p);
        const x = p[0];
        const y = p[1];
        add(result, [x, y]);
        add(result, [x + 1, y]);
        add(result, [x + 1, y - 1]);
        add(result, [x, y - 1]);
        add(result, [x - 1, y]);
        add(result, [x - 1, y + 1]);
        add(result, [x, y + 1]);
    }
    return result;
}

function step(cells, off, on) {
    let offMask = 0;
    let onMask = 0;
    for (let i = 0; i < 13; i++) {
        if (random() < off[i]) {
            offMask |= 1 << i;
        }
        if (random() < on[i]) {
            onMask |= 1 << i;
        }
    }

    let result = new Set();
    for (let p of neighborhood(cells)) {
        p = unpack(p);
        const bit = 1 << lookup[mask(cells, p)];
        if (has(cells, p)) {
            if ((offMask & bit) == 0) {
                add(result, p);
            }
        } else {
            if ((onMask & bit) != 0) {
                add(result, p);
            }
        }
    }
    return result;
}

function generate(iterations, minCells) {
    while (true) {
        let off = [];
        let on = [];
        for (let i = 0; i < 13; i++) {
            off.push(random() * off_bias);
            on.push(random() * on_bias);
        }

        let cells = new Set();
        add(cells, [0, 0]);

        let history = [];
        history.push(edges(cells));

        for (let i = 0; i < iterations; i++) {
            cells = step(cells, off, on);
            history.push(edges(cells));
            if (cells.size < 2) {
                break;
            }
        }

        if (hasIsolatedCells(cells)) {
            continue;
        }

        if (cells.size >= minCells) {
            return history;
        }
    }
}

function cartesian(p) {
    const size = 1;
    const q = parseInt(p[0]);
    const r = parseInt(p[1]);
    const s = Math.sqrt(3);
    const x = s * q + s / 2 * r;
    const y = 3.0 / 2 * r;
    return [x * size, y * size];
}

function less(p, q) {
    if (p[0] === q[0]) {
        return p[1] < q[1];
    }
    return p[0] < q[0];
}

function hasIsolatedCells(cells) {
    for (let p of cells) {
        p = unpack(p);
        let ok = false;
        for (let q of neighbors(p)) {
            if (has(cells, q)) {
                ok = true;
                break;
            }
        }
        if (!ok) {
            return true;
        }
    }
    return false;
}

function edges(cells) {
    let result = [];
    for (let p of cells) {
        p = unpack(p);
        let ok = false;
        for (let q of neighbors(p)) {
            if (!has(cells, q)) {
                continue;
            }
            ok = true;
            if (!less(p, q)) {
                continue;
            }
            result.push([cartesian(p), cartesian(q)]);
        }
        if (!ok) {
            result.push([cartesian(p), cartesian(p)]);
        }
    }
    return mergeEdges(result);
}

function packEdge(edge) {
    const a = edge[0];
    const b = edge[1];
    return a[0] + "," + a[1] + "," + b[0] + "," + b[1];
}

function nextEdge(lookup, edge) {
    const a = edge[0];
    const b = edge[1];
    const nx = b[0] + (b[0] - a[0]);
    const ny = b[1] + (b[1] - a[1]);
    for (let p of lookup.get(pack(b))) {
        const ex = Math.abs(nx - p[0]);
        const ey = Math.abs(ny - p[1]);
        if ((ex < 1e-6) && (ey < 1e-6)) {
            return [b, p];
        }
    }
    return null;
}

function mergeEdges(edges) {
    let lookup = new Map();
    for (let edge of edges) {
        lookup.set(pack(edge[0]), []);
        lookup.set(pack(edge[1]), []);
    }
    for (let edge of edges) {
        if (pack(edge[0]) === pack(edge[1])) {
            continue;
        }
        lookup.get(pack(edge[0])).push(edge[1]);
        lookup.get(pack(edge[1])).push(edge[0]);
    }

    let result = [];
    let seen = new Set();

    for (let edge of edges) {
        const a = edge[0];
        const b = edge[1];

        if (seen.has(packEdge([a, b])) || seen.has(packEdge([b, a]))) {
            continue;
        }

        let p0 = a;
        let p1 = b;

        // forward
        let e = [a, b];
        while (true) {
            seen.add(packEdge(e));
            p1 = e[1];
            e = nextEdge(lookup, e);
            if (!e) {
                break;
            }
        }

        // reverse
        e = [b, a];
        while (true) {
            seen.add(packEdge(e));
            p0 = e[1];
            e = nextEdge(lookup, e);
            if (!e) {
                break;
            }
        }

        result.push([p0, p1]);
    }

    return result;
}

function distance2(x1, y1, x2, y2)
{
    const dx = x1 - x2;
    const dy = y1 - y2;
    return dx*dx + dy*dy;
}

const cosA = Math.cos(Math.PI/6);
const sinA = Math.sin(Math.PI/6);

function drawEdges(edges, scale, offset_x, offset_y) {
    
    edges_to_sort = [];
    edges_sorted = [];

    for (let e of edges) { edges_to_sort.push(e); }
    
    var current_x = 0;
    var current_y = 0;
    while (edges_to_sort.length > 0)
    {
        var min_distance = -1;
        var best_index = -1;
        var invert = false;
        var index = 0;
        var epsilon = 0.1;
        for (let e of edges_to_sort)
        {
            var penalty = distance2(0, 0, e[0][0], e[0][1]) * 0.1;
            var dist2 = distance2(current_x, current_y, e[0][0], e[0][1]);
            if (dist2 > epsilon) dist2 += penalty;
            if (best_index < 0 || min_distance > dist2)
            {
                best_index = index;
                min_distance = dist2;
                invert = false;
            }
            penalty = distance2(0, 0, e[1][0], e[1][1]) * 0.1;
            dist2 = distance2(current_x, current_y, e[1][0], e[1][1]);
            if (dist2 > epsilon) dist2 += penalty;
            if (best_index < 0 || min_distance > dist2)
            {
                best_index = index;
                min_distance = dist2;
                invert = true;
            }
            index++;
        }
        if (best_index >= 0)
        {
            var edge = edges_to_sort[best_index];
            edges_to_sort.splice(best_index, 1);
            if (invert)
            {
                for (let i = 0; i < 2; i++)
                {
                    var tmp = edge[0][i];
                    edge[0][i] = edge[1][i];
                    edge[1][i] = tmp;
                }
            }
            current_x = edge[1][0];
            current_y = edge[1][1];
            edges_sorted.push(edge);
        }
    }
    
    for (let e of edges_sorted) {
        line_count++;
        if (e[0][0] === e[1][0] && e[0][1] === e[1][1]) {
            const p = 0.02;
            var x0 = e[0][0]-p;
            var y0 = e[0][1]-p;
            var x1 = e[1][0]+p;
            var y1 = e[1][1]+p;
            var x2 = e[0][0]-p;
            var y2 = e[0][1]+p;
            var x3 = e[1][0]+p;
            var y3 = e[1][1]-p;
            var rx0 = cosA * x0 - sinA * y0;
            var ry0 = sinA * x0 + cosA * y0;
            var rx1 = cosA * x1 - sinA * y1;
            var ry1 = sinA * x1 + cosA * y1;
            var rx2 = cosA * x2 - sinA * y2;
            var ry2 = sinA * x2 + cosA * y2;
            var rx3 = cosA * x3 - sinA * y3;
            var ry3 = sinA * x3 + cosA * y3;
            x0 = rx0 * scale * global_scale + offset_x * global_scale * 40;
            y0 = ry0 * scale * global_scale + offset_y * global_scale * 40;
            x1 = rx1 * scale * global_scale + offset_x * global_scale * 40;
            y1 = ry1 * scale * global_scale + offset_y * global_scale * 40;
            x2 = rx2 * scale * global_scale + offset_x * global_scale * 40;
            y2 = ry2 * scale * global_scale + offset_y * global_scale * 40;
            x3 = rx3 * scale * global_scale + offset_x * global_scale * 40;
            y3 = ry3 * scale * global_scale + offset_y * global_scale * 40;
            
            turtle.goto(x0, y0);
            turtle.pendown();
            turtle.goto(x1, y1);
            turtle.goto(x2, y2);
            turtle.goto(x3, y3);
            turtle.penup();
            
        } else {
            var x0 = e[0][0];
            var y0 = e[0][1];
            var x1 = e[1][0];
            var y1 = e[1][1];
            var rx0 = cosA * x0 - sinA * y0;
            var ry0 = sinA * x0 + cosA * y0;
            var rx1 = cosA * x1 - sinA * y1;
            var ry1 = sinA * x1 + cosA * y1;
            x0 = rx0 * scale * global_scale + offset_x * global_scale * 40;
            y0 = ry0 * scale * global_scale + offset_y * global_scale * 40;
            x1 = rx1 * scale * global_scale + offset_x * global_scale * 40;
            y1 = ry1 * scale * global_scale + offset_y * global_scale * 40;
            
            turtle.goto(x0, y0);
            turtle.pendown();
            turtle.goto(x1, y1);
            turtle.penup();
        }
    }
}

function Snowflake(id) {
    this.id = id;
    
    if (predefined_seeds > 0) this.seed = this.id + predefined_seeds * 1000;
    else this.seed = Math.floor(9999999999 * Math.random());
    this.minIterations = default_minIterations;
    this.maxIterations = default_maxIterations;
    this.minCells = default_minCells;
    if (typeof saved_min_max_seeds !== 'undefined' && saved_min_max_seeds.length > this.id)
    {
        this.minIterations = saved_min_max_seeds[this.id][0];
        this.maxIterations = saved_min_max_seeds[this.id][1];
        this.minCells = saved_min_max_seeds[this.id][2];
        off_bias = saved_min_max_seeds[this.id][3];
        on_bias = saved_min_max_seeds[this.id][4];
        this.seed = saved_min_max_seeds[this.id][5];
    }
    
    this.reset();
}

Snowflake.prototype.reset = function() {
    randomSeed(this.seed);
    const iterations = Math.floor(random() * (this.maxIterations - this.minIterations + 1)) + this.minIterations;
    //this.minIterations = iterations; this.maxIterations = iterations;
    this.history = generate(iterations, this.minCells);
    this.computeBounds();
}

Snowflake.prototype.draw = function(offset_x, offset_y) {
    let index = this.history.length - 1;
    const edges = this.history[index];
    var scale = 40 * this.computeScale();
    if (show_seeds) { scale *= 0.7; offset_y *= 0.9; }

    drawEdges(edges, scale, offset_x, offset_y);
    
    if (show_seeds)
    {
        turtle.penup();
        var string = `${this.minIterations} ${this.maxIterations} ${this.minCells} ${off_bias} ${on_bias} ${this.seed}`;
        var len = string.length;
        const text_size = 0.07;
        var t_offset_x = -len/(0.1 / text_size);
        var t_offset_y = 0.45;
        turtle.goto(offset_x * global_scale * 40 + t_offset_x, (offset_y + t_offset_y) * global_scale * 40);
        text.print(turtle, string, text_size);
        turtle.penup();
    }
}

Snowflake.prototype.computeBounds = function() {
    let x0 = 0;
    let y0 = 0;
    let x1 = 0;
    let y1 = 0;
    for (let edges of this.history) {
        for (let e of edges) {
            for (let p of e) {
                x0 = Math.min(x0, p[0]);
                x1 = Math.max(x1, p[0]);
                y0 = Math.min(y0, p[1]);
                y1 = Math.max(y1, p[1]);
            }
        }
    }
    this.bounds = [x0, y0, x1, y1];
}

Snowflake.prototype.computeScale = function() {
    const p = 0.9;
    let x0, y0, x1, y1;
    [x0, y0, x1, y1] = this.bounds;
    const dx = x1 - x0 + 2;
    const dy = y1 - y0 + 2;
    const sx = p * 1 / dx;
    const sy = p * 1 / dy;
    const s = Math.min(sx, sy);
    return s;
}

let flakes = [];
let elapsed = 0;

function setup()
{
    line_count = 0;
}

function draw(t_i)
{
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            if (col == 0 && row&1) continue;
            const i = row * cols + col;
            if (i != t_i) continue;
            let flake = flakes[i];
            const offset_x = col - cols * 0.5 + 0.5 + ((row&1) ? -0.5 : 0);
            var offset_y = row - rows * 0.5 + 0.5;
            if (show_seeds) offset_y *= 1.3;
            flake.draw(offset_x * 0.9, offset_y * 0.8);
        }
    }
}


Canvas.setpenopacity(0.9);

const turtle = new Turtle();
const text = new Text();

turtle.penup();

// The walk function will be called until it returns false.
function walk(i)
{
    if (i >= rows * cols)
    {
        turtle.penup();
        turtle.goto(-30,97);
        //text.print(turtle, `Line count: ${line_count}`, .1);
        turtle.penup();
        return false;
    }

    if (i==0) setup();
    
    flakes.push(new Snowflake(i));
    draw(i);
    
    //sleep(10);
    
    return true;
}



////////////////////////////////

function sleep(milliseconds) {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < milliseconds);
}

////////////////////////////////////////////////////////////////
// Text utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/1713ddbe99
////////////////////////////////////////////////////////////////

function Text() {
    class Text {
        print (t, str, scale = 1, italic = 0, kerning = 1) {
            t.radians();
            let pos = [t.x(), t.y()], h = t.h(), o = pos;
            str.split('').map(c => {
                const i = c.charCodeAt(0) - 32;
                if (i < 0 ) {
                    pos = o = this.rotAdd([0, 48*scale], o, h);
                } else if (i > 96 ) {
                    pos = this.rotAdd([16*scale, 0], o, h);
                } else {
                    const d = dat[i], lt = d[0]*scale, rt = d[1]*scale, paths = d[2];
                    paths.map( p => {
                        t.up();
                    	p.map( s=> {
                        	t.goto(this.rotAdd([(s[0]-s[1]*italic)*scale - lt, s[1]*scale], pos, h));
                        	t.down();
                        });
                    });
                    pos = this.rotAdd([(rt - lt)*kerning, 0], pos, h);
                }
            });
        }
        rotAdd (a, b, h) {
            return [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0], 
                    Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];
        }
    }
    
const dat = ('br>eoj^jl<jqirjskrjq>brf^fe<n^ne>`ukZdz<qZjz<dgrg<cmqm>`thZhw<lZlw<qao_l^h^e_caccdeefg'+
'gmiojpkqmqporlshsercp>^vs^as<f^h`hbgdeeceacaab_d^f^h_k`n`q_s^<olmmlolqnspsrrspsnqlol>]wtgtfsereqfph'+
'nmlpjrhsdsbraq`o`makbjifjekckaj_h^f_eaecffhimporqssstrtq>eoj`i_j^k_kajcid>cqnZl\\j_hcghglhqjulxnz>c'+
'qfZh\\j_lcmhmllqjuhxfz>brjdjp<egom<ogem>]wjajs<ajsj>fnkojpiojnkokqis>]wajsj>fnjniojpkojn>_usZaz>`ti'+
'^f_dbcgcjdofrisksnrpoqjqgpbn_k^i^>`tfbhak^ks>`tdcdbe`f_h^l^n_o`pbpdofmicsqs>`te^p^jfmfogphqkqmppnrk'+
'shserdqco>`tm^clrl<m^ms>`to^e^dgefhekenfphqkqmppnrkshserdqco>`tpao_l^j^g_ebdgdlepgrjsksnrppqmqlping'+
'kfjfggeidl>`tq^gs<c^q^>`th^e_dadceegfkgnhpjqlqopqorlshserdqcocldjfhigmfoepcpao_l^h^>`tpeohmjjkikfjd'+
'hcecddaf_i^j^m_oapepjoomrjshserdp>fnjgihjikhjg<jniojpkojn>fnjgihjikhjg<kojpiojnkokqis>^vrabjrs>]wag'+
'sg<amsm>^vbarjbs>asdcdbe`f_h^l^n_o`pbpdofngjijl<jqirjskrjq>]xofndlcicgdfeehekfmhnknmmnk<icgefhfkgmh'+
'n<ocnknmpnrntluiugtdsbq`o_l^i^f_d`bbad`g`jambodqfrislsorqqrp<pcokompn>asj^bs<j^rs<elol>_tc^cs<c^l^o'+
'_p`qbqdpfoglh<chlhoipjqlqopqorlscs>`urcqao_m^i^g_eadccfckdnepgrismsorqprn>_tc^cs<c^j^m_oapcqfqkpnop'+
'mrjscs>`sd^ds<d^q^<dhlh<dsqs>`rd^ds<d^q^<dhlh>`urcqao_m^i^g_eadccfckdnepgrismsorqprnrk<mkrk>_uc^cs<'+
'q^qs<chqh>fnj^js>brn^nnmqlrjshsfreqdndl>_tc^cs<q^cl<hgqs>`qd^ds<dsps>^vb^bs<b^js<r^js<r^rs>_uc^cs<c'+
'^qs<q^qs>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^>_tc^cs<c^l^o_p`qbqepgohlici>_uh^f_daccbfbkcnd'+
'pfrhslsnrppqnrkrfqcpan_l^h^<koqu>_tc^cs<c^l^o_p`qbqdpfoglhch<jhqs>`tqao_l^h^e_caccdeefggmiojpkqmqpo'+
'rlshsercp>brj^js<c^q^>_uc^cmdpfrisksnrppqmq^>asb^js<r^js>^v`^es<j^es<j^os<t^os>`tc^qs<q^cs>asb^jhjs'+
'<r^jh>`tq^cs<c^q^<csqs>cqgZgz<hZhz<gZnZ<gznz>cqc^qv>cqlZlz<mZmz<fZmZ<fzmz>brj\\bj<j\\rj>asazsz>fnkc'+
'ieigjhkgjfig>atpeps<phnfleiegfehdkdmepgrislsnrpp>`sd^ds<dhffhekemfohpkpmopmrkshsfrdp>asphnfleiegfeh'+
'dkdmepgrislsnrpp>atp^ps<phnfleiegfehdkdmepgrislsnrpp>asdkpkpiognfleiegfehdkdmepgrislsnrpp>eqo^m^k_j'+
'bjs<gene>atpepuoxnylzizgy<phnfleiegfehdkdmepgrislsnrpp>ate^es<eihfjemeofpips>fni^j_k^j]i^<jejs>eoj^'+
'k_l^k]j^<kekvjyhzfz>are^es<oeeo<ikps>fnj^js>[y_e_s<_ibfdegeifjijs<jimfoeretfuius>ateees<eihfjemeofp'+
'ips>atiegfehdkdmepgrislsnrppqmqkphnfleie>`sdedz<dhffhekemfohpkpmopmrkshsfrdp>atpepz<phnfleiegfehdkd'+
'mepgrislsnrpp>cpgegs<gkhhjfleoe>bsphofleieffehfjhkmlompopporlsisfrep>eqj^jokrmsos<gene>ateeeofrhsks'+
'mrpo<peps>brdejs<pejs>_ubefs<jefs<jens<rens>bseeps<pees>brdejs<pejshwfydzcz>bspees<eepe<esps>cqlZj['+
'i\\h^h`ibjckekgii<j[i]i_jakbldlfkhgjkllnlpkrjsiuiwjy<ikkmkojqirhthvixjylz>fnjZjz>cqhZj[k\\l^l`kbjci'+
'eigki<j[k]k_jaibhdhfihmjilhnhpirjskukwjy<kkimiojqkrltlvkxjyhz>^vamakbhdgfghhlknlplrksi<akbidhfhhill'+
'nmpmrlsisg>brb^bscsc^d^dsese^f^fsgsg^h^hsisi^j^jsksk^l^lsmsm^n^nsoso^p^psqsq^r^rs').split('>').map(
    r=> { return [r.charCodeAt(0)-106,r.charCodeAt(1)-106, r.substr(2).split('<').map(a => {const ret 
    = []; for (let i=0; i<a.length; i+=2) {ret.push(a.substr(i, 2).split('').map(b => b.charCodeAt(0)
    -106));} return ret; })]; });

    return new Text();
}

///////////////

var seed = 0;
var r_m = 4294967296; // a - 1 should be divisible by m's prime factors
var r_a = 1664525; // c and m should be co-prime
var r_c = 1013904223;

function randomSeed(val)
{
    seed = val;//(val == null ? Math.random() * m : val) >>> 0;
}

function random()
{
    seed = (r_a * seed + r_c) % r_m;
    var rand = seed / r_m;

    return rand;
}