// derived from https://cs.uwaterloo.ca/~csk/spectre/spectre.js


const {PI, cos, sin} = Math;

const ident = [1,0,0,0,1,0];

let pan = {x: -100, y: 100};

function radians(degrees) {
    return degrees * PI / 180;
}

let to_screen = [20, 0, 0, 0, -20, 0];
let lw_scale = 1;

let sys;

let scale_centre;
let scale_start;
let scale_ts;

let reset_but;
let tile_sel;
let shape_sel;
let colscheme_sel;

let subst_button;
let translate_button;
let scale_button;
let dragging = false;
let uibox = true;

const spectre = [
    pt(0, 0),
    pt(1.0, 0.0),
    pt(1.5, -0.8660254037844386),
    pt(2.366025403784439, -0.36602540378443865),
    pt(2.366025403784439, 0.6339745962155614),
    pt(3.366025403784439, 0.6339745962155614),
    pt(3.866025403784439, 1.5),
    pt(3.0, 2.0),
    pt(2.133974596215561, 1.5),
    pt(1.6339745962155614, 2.3660254037844393),
    pt(0.6339745962155614, 2.3660254037844393),
    pt(-0.3660254037844386, 2.3660254037844393),
    pt(-0.866025403784439, 1.5),
    pt(0.0, 1.0) 
];

const base_quad = [spectre[3], spectre[5], spectre[7], spectre[11]];


const tile_names = [ 
	'Gamma', 'Delta', 'Theta', 'Lambda', 'Xi',
	'Pi', 'Sigma', 'Phi', 'Psi' ];

const svg_point = ({x,y}) => `${x.toFixed(3)},${y.toFixed(3)}`;

function lerp(a,b,t) {
    return t*b + (1-t)*a;
}

function pt( x, y )
{
	return { x : x, y : y };
}

// Affine matrix inverse
function inv( T ) {
	const det = T[0]*T[4] - T[1]*T[3];
	return [T[4]/det, -T[1]/det, (T[1]*T[5]-T[2]*T[4])/det,
		-T[3]/det, T[0]/det, (T[2]*T[3]-T[0]*T[5])/det];
};

// Affine matrix multiply
function mul( A, B )
{
	return [A[0]*B[0] + A[1]*B[3], 
		A[0]*B[1] + A[1]*B[4],
		A[0]*B[2] + A[1]*B[5] + A[2],

		A[3]*B[0] + A[4]*B[3], 
		A[3]*B[1] + A[4]*B[4],
		A[3]*B[2] + A[4]*B[5] + A[5]];
}

function padd( p, q )
{
	return { x : p.x + q.x, y : p.y + q.y };
}

function psub( p, q )
{
	return { x : p.x - q.x, y : p.y - q.y };
}

function pframe( o, p, q, a, b )
{
	return { x : o.x + a*p.x + b*q.x, y : o.y + a*p.y + b*q.y };
}

// Rotation matrix
function trot( ang )
{
	const c = cos( ang );
	const s = sin( ang );
	return [c, -s, 0, s, c, 0];
}

//Scale matrix
function tscale(x,y) {
    return [x,0,0,0,y,0];
}

// Translation matrix
function ttrans( tx, ty )
{
	return [1, 0, tx, 0, 1, ty];
}

// Translation matrix moving p to q
function transTo( p, q )
{
	return ttrans( q.x - p.x, q.y - p.y );
}


// Matrix * point
function transPt( M, P )
{
	return pt(M[0]*P.x + M[1]*P.y + M[2], M[3]*P.x + M[4]*P.y + M[5]);
}

class Shape {
	constructor( pts, quad) {
		this.pts = pts;
		this.quad = quad;

		let blah = true;

		this.pts = [pts[pts.length-1]];
		for( const p of pts ) {
			const prev = this.pts[this.pts.length-1];
			const v = psub( p, prev );
			const w = pt( -v.y, v.x );
			if( blah ) {
				this.pts.push( pframe( prev, v, w, 0.33, 0.6 ) );
				this.pts.push( pframe( prev, v, w, 0.67, 0.6 ) );
			} else {
				this.pts.push( pframe( prev, v, w, 0.33, -0.6 ) );
				this.pts.push( pframe( prev, v, w, 0.67, -0.6 ) );
			}
			blah = !blah;
			this.pts.push( p );
		}
	}

	streamSVG( S, stream ) {
        const tpts = this.pts.map(p => transPt( S, p ));

        const [a,c,e,b,d,f] = S;
        const matS = [a,b,c,d,e,f].map(p=>p.toFixed(3));
		const s = `<use href="#spectre" transform="matrix(${matS.join(',')})"/>`;

		stream.push( s );
	}

    bounds(S) {
        const points = this.pts.map(p => transPt(S,p));
        return {
            minx: Math.min(...points.map(p => p.x)),
            miny: Math.min(...points.map(p => p.y)),
            maxx: Math.max(...points.map(p => p.x)),
            maxy: Math.max(...points.map(p => p.y)),
        };
    }

    * flatten(S) {
        const points = this.pts.map(p => transPt(S,p));
        const ymax = Math.max(...points.map(p => p.y));
        yield {points, ymax, shape: this};
    }
}

class Meta
{
	constructor()
	{
		this.geoms = [];
		this.quad = [];
	}

	addChild( g, T )
	{
		this.geoms.push( { geom : g, xform: T } );
	}

	draw( S ) 
	{
		for( let g of this.geoms ) {
			g.geom.draw( mul( S, g.xform ) );
		}
	}

	streamSVG( S, stream )
	{
        const {minx,miny,maxx,maxy} = this.bounds(S);

        const [a,c,e,b,d,f] = S;
        const matS = [a,b,c,d,e,f].map(p=>p.toFixed(3));
        stream.push(`<g transform="matrix(${matS.join(',')})">`);
        const quad_points = this.quad.map(({x,y}) => `${x},${y}`).join(' ');
 //       stream.push(`<polygon fill="blue" fill-opacity="0.2" points="${quad_points}"></polygon>`);
		for( let g of this.geoms ) {
			g.geom.streamSVG( g.xform, stream );
		}
        stream.push('</g>');

	}

    bounds(S) {
        const sub_bounds = this.geoms.map(g => g.geom.bounds(mul(S,g.xform)));
        return {
            minx: Math.min(...sub_bounds.map(b=>b.minx)),
            miny: Math.min(...sub_bounds.map(b=>b.miny)),
            maxx: Math.max(...sub_bounds.map(b=>b.maxx)),
            maxy: Math.max(...sub_bounds.map(b=>b.maxy)),
        };
    }

    * flatten(S) {
        for(let g of this.geoms) {
            yield* g.geom.flatten(mul(S, g.xform));
        }
    }
}

function tiles(level, label) {
    let quad;
    let out = [];
    let transform;
    if(level == 0) {
        transform = ident;
        quad = base_quad;
        switch(label) {
            case 'Delta':
            case 'Theta':
            case 'Lambda':
            case 'Xi':
            case 'Pi':
            case 'Sigma':
            case 'Phi':
            case 'Psi':
                out.push(ident);
            case 'Gamma':
                const mystic = new Meta();
                out.push(ident);
                out.push(mul( ttrans( spectre[8].x, spectre[8].y ), trot( PI / 6 ) ));
        }
    } else {
        /*
         * Each of the subtiles is identical, but rotated and translated.
         *
         * Produce transformation matrices Ts for each of the subtiles: they're formed by rotating the quad and then matching up a pair of points.
         *
         * The whole thing is then reflected.
         *
         * The layout of subtiles depends on the larger tile.
         *
         */
        const labels = ['Delta', 'Theta', 'Lambda', 'Xi', 
				 'Pi', 'Sigma', 'Phi', 'Psi'];

        const sublevels = Object.fromEntries(labels.map(label => tiles(level-1, label)));
        const subquad = sublevels['Delta'].quad;

        const reflection = tscale(-1,1);


        // How to get from each subtile to the next.
        const t_rules = [
            [60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1],
            [0, 2, 0], [60, 3, 1], [-120, 3, 3] ];  

        
        let Ts = [ident];
        let total_ang = 0;
        let rot = ident;
        let tquad = [...subquad];
        for( const [ang,from,to] of t_rules ) {
            total_ang += ang;
            if( ang != 0 ) {
                rot = trot( radians( total_ang ) );
                tquad = subquad.map(q => transPt(rot,q));
            }

            const ttt = transTo( tquad[to], transPt( Ts[Ts.length-1], subquad[from] ) );
            Ts.push( mul( ttt, rot ) );
        }

        Ts = Ts.map(t => mul(reflection, t));

        
        // Now build the actual supertiles, labelling appropriately.
        const super_rules = {
            'Gamma' :  ['Pi','Delta','null','Theta','Sigma','Xi','Phi','Gamma'],
            'Delta' :  ['Xi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'],
            'Theta' :  ['Psi','Delta','Pi','Phi','Sigma','Pi','Phi','Gamma'],
            'Lambda' : ['Psi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'],
            'Xi' :     ['Psi','Delta','Pi','Phi','Sigma','Psi','Phi','Gamma'],
            'Pi' :     ['Psi','Delta','Xi','Phi','Sigma','Psi','Phi','Gamma'],
            'Sigma' :  ['Xi','Delta','Xi','Phi','Sigma','Pi','Lambda','Gamma'],
            'Phi' :    ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','Gamma'],
            'Psi' :    ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] };
        const super_quad = [
            transPt( Ts[6], subquad[2] ),
            transPt( Ts[5], subquad[1] ),
            transPt( Ts[3], subquad[2] ),
            transPt( Ts[0], subquad[1] ) ]; 
    }
    return {quad, tiles: out};
}


function buildSpectreBase() {
	const ret = {};

	for( lab of ['Delta', 'Theta', 'Lambda', 'Xi', 
				 'Pi', 'Sigma', 'Phi', 'Psi'] ) {
        ret[lab] = new Shape( spectre, base_quad, lab );
	}

	const mystic = new Meta();
    mystic.addChild( new Shape( spectre, base_quad, 'Gamma1' ), ident );
    mystic.addChild( new Shape( spectre, base_quad, 'Gamma2' ),
        mul( ttrans( spectre[8].x, spectre[8].y ), trot( PI / 6 ) ) );
	mystic.quad = base_quad;
	ret['Gamma'] = mystic;

	return ret;
}

function buildHatTurtleBase( hat_dominant )
{
	const r3 = 1.7320508075688772;
	const hr3 = 0.8660254037844386;

	function hexPt( x, y )
	{
		return pt( x + 0.5*y, -hr3*y );
	}

	function hexPt2( x, y )
	{
		return pt( x + hr3*y, -0.5*y );
	}

	const hat = [
		hexPt(-1, 2), hexPt(0, 2), hexPt(0, 3), hexPt(2, 2), hexPt(3, 0),
		hexPt(4, 0), hexPt(5,-1), hexPt(4,-2), hexPt(2,-1), hexPt(2,-2),
		hexPt( 1, -2), hexPt(0,-2), hexPt(-1,-1), hexPt(0, 0) ];

	const turtle = [
		hexPt(0,0), hexPt(2,-1), hexPt(3,0), hexPt(4,-1), hexPt(4,-2),
		hexPt(6,-3), hexPt(7,-5), hexPt(6,-5), hexPt(5,-4), hexPt(4,-5),
		hexPt(2,-4), hexPt(0,-3), hexPt(-1,-1), hexPt(0,-1)
		];

	const hat_keys = [
		hat[3], hat[5], hat[7], hat[11]
	];
	const turtle_keys = [
		turtle[3], turtle[5], turtle[7], turtle[11]
	];

	const ret = {};

	if( hat_dominant ) {
		for( lab of ['Delta', 'Theta', 'Lambda', 'Xi', 
					 'Pi', 'Sigma', 'Phi', 'Psi'] ) {
			ret[lab] = new Shape( hat, hat_keys, lab );
		}

		const mystic = new Meta();
		mystic.addChild( new Shape( hat, hat_keys, 'Gamma1' ), ident );
		mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma2' ),
			ttrans( hat[8].x, hat[8].y ) );
		mystic.quad = hat_keys;
		ret['Gamma'] = mystic;
	} else {
		for( lab of ['Delta', 'Theta', 'Lambda', 'Xi', 
					 'Pi', 'Sigma', 'Phi', 'Psi'] ) {
			ret[lab] = new Shape( turtle, turtle_keys, lab );
		}

		const mystic = new Meta();
		mystic.addChild( new Shape( turtle, turtle_keys, 'Gamma1' ), ident );
		mystic.addChild( new Shape( hat, hat_keys, 'Gamma2' ),
			mul( ttrans( turtle[9].x, turtle[9].y ), trot( PI/3 ) ) );
		mystic.quad = turtle_keys;
		ret['Gamma'] = mystic;
	}

	return ret;
}

function buildHexBase()
{
	const hr3 = 0.8660254037844386;

	const hex = [
		pt(0, 0),
		pt(1.0, 0.0),
		pt(1.5, hr3),
		pt(1, 2*hr3),
		pt(0, 2*hr3),
		pt(-0.5, hr3) 
	];

	const hex_keys = [ hex[1], hex[2], hex[3], hex[5] ];

	const ret = {};

	for( lab of ['Gamma', 'Delta', 'Theta', 'Lambda', 'Xi', 
				 'Pi', 'Sigma', 'Phi', 'Psi'] ) {
		ret[lab] = new Shape( hex, hex_keys, lab );
	}

	return ret;
}

function buildSupertiles( sys )
{
	// First, use any of the nine-unit tiles in sys to obtain
	// a list of transformation matrices for placing tiles within
	// supertiles.

	const quad = sys['Delta'].quad;
	const R = [-1,0,0,0,1,0];
	
	const t_rules = [
		[60, 3, 1], [0, 2, 0], [60, 3, 1], [60, 3, 1],
		[0, 2, 0], [60, 3, 1], [-120, 3, 3] ];  

	const Ts = [ident];
	let total_ang = 0;
	let rot = ident;
	const tquad = [...quad];
	for( const [ang,from,to] of t_rules ) {
		total_ang += ang;
		if( ang != 0 ) {
			rot = trot( radians( total_ang ) );
			for( i = 0; i < 4; ++i ) {
				tquad[i] = transPt( rot, quad[i] );
			}
		}

		const ttt = transTo( tquad[to], 
			transPt( Ts[Ts.length-1], quad[from] ) );
		Ts.push( mul( ttt, rot ) );
	}

	for( let idx = 0; idx < Ts.length; ++idx ) {
		Ts[idx] = mul( R, Ts[idx] );
	}

	// Now build the actual supertiles, labelling appropriately.
	const super_rules = {
		'Gamma' :  ['Pi','Delta','null','Theta','Sigma','Xi','Phi','Gamma'],
		'Delta' :  ['Xi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'],
		'Theta' :  ['Psi','Delta','Pi','Phi','Sigma','Pi','Phi','Gamma'],
		'Lambda' : ['Psi','Delta','Xi','Phi','Sigma','Pi','Phi','Gamma'],
		'Xi' :     ['Psi','Delta','Pi','Phi','Sigma','Psi','Phi','Gamma'],
		'Pi' :     ['Psi','Delta','Xi','Phi','Sigma','Psi','Phi','Gamma'],
		'Sigma' :  ['Xi','Delta','Xi','Phi','Sigma','Pi','Lambda','Gamma'],
		'Phi' :    ['Psi','Delta','Psi','Phi','Sigma','Pi','Phi','Gamma'],
		'Psi' :    ['Psi','Delta','Psi','Phi','Sigma','Psi','Phi','Gamma'] };
	const super_quad = [
		transPt( Ts[6], quad[2] ),
		transPt( Ts[5], quad[1] ),
		transPt( Ts[3], quad[2] ),
		transPt( Ts[0], quad[1] ) ]; 

	const ret = {};

	for( const [lab, subs] of Object.entries( super_rules ) ) {
		const sup = new Meta();
		for( let idx = 0; idx < 8; ++idx ) {
			if( subs[idx] == 'null' ) {
				continue;
			}
            sup.addChild( sys[subs[idx]], Ts[idx] );
		}
		sup.quad = super_quad;

		ret[lab] = sup;
	}

	return ret;
}

/* modified from https://css-tricks.com/converting-color-spaces-in-javascript/
 */
function hexToHSL(H) {
    const [r,g,b] = [0,1,2].map(i=>H.slice(2*i+1,2*i+3)).map(n=>parseInt(n,16)/255);
    let cmin = Math.min(r,g,b),
        cmax = Math.max(r,g,b),
        delta = cmax - cmin,
        h = 0,
        s = 0,
        l = 0;

    if (delta == 0)
        h = 0;
    else if (cmax == r)
        h = ((g - b) / delta) % 6;
    else if (cmax == g)
        h = (b - r) / delta + 2;
    else
        h = (r - g) / delta + 4;

    h = Math.round(h * 60);

    if (h < 0)
        h += 360;

    l = (cmax + cmin) / 2;
    s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
    s = +(s * 100).toFixed(1);
    l = +(l * 100).toFixed(1);

    return {h,s,l};
}

let last_num_iterations;

function get_settings() {
    return Object.fromEntries(
        Array.from(document.querySelectorAll('input,textarea')).map(i => [i.id, i.type=='number' ? i.valueAsNumber : i.value])
    );
}

function rebuild() {
    const svg = document.querySelector('svg');
    const settings = get_settings();
    let sys = buildSpectreBase(false);
    
    for(let i=0;i<settings.num_iterations;i++) {
        sys = buildSupertiles( sys );	
    }

    sys = sys['Delta'];
    window.sys = sys;

    const drawing = [];
    const board = svg.querySelector('#board');
    board.innerHTML = '';
    sys.streamSVG(ident, drawing);
    board.innerHTML = drawing.join(' ');

    const viewbox = svg.getBoundingClientRect();
    const rule = new Function('t','x','y', settings.colouring_rule);
    function visit(g, t) {
        for(let c of g.children) {
            visit(c, t+Math.random()*10-5);
        }
        if(g.tagName.toLowerCase()=='use') {
            const e = document.querySelector('defs .spectre').cloneNode(true);
            const b = g.getBoundingClientRect();
            const point = svg.createSVGPoint();
            point.x = b.x;
            point.y = b.y;
            const position = point.matrixTransform(svg.querySelector('#display').getScreenCTM().inverse());
            e.style.setProperty('--t',t);
            e.style.setProperty('--r',Math.random());
            e.style.setProperty('--x',position.x);
            e.style.setProperty('--y',position.y);
            e.removeAttribute('id');

            g.replaceWith(e);
            e.setAttribute('transform', g.getAttribute('transform'));
        }
    }
    visit(board,30);
    make_download();
}

function finish() {
    const svg = document.querySelector('svg');
    const viewbox = svg.getBoundingClientRect();
    make_download();
    document.getElementById('link').click();
}

function setup() {
    const settings = get_settings();
    const svg = document.querySelector('svg');
    const raw_zoom = document.getElementById('zoom').valueAsNumber;
    const zoom = raw_zoom;
    //const bounds = sys.bounds(ident);
    svg.querySelector('#display').setAttribute('transform',`scale(${zoom}) translate(${pan.x} ${pan.y})`);
    svg.style.setProperty('--zoom', zoom);
    svg.style.setProperty('--pan-x', pan.x);
    svg.style.setProperty('--pan-y', pan.y);
    
    if(settings.num_iterations != last_num_iterations) {
        rebuild();
        last_num_iterations = settings.num_iterations;
    }
}

for(let i of document.querySelectorAll('input')) {
    i.addEventListener('input', setup);
    i.addEventListener('change', setup)
}

setup();

document.getElementById('finish').addEventListener('click', () => finish());

function make_download() {
    const svg = document.querySelector('svg').cloneNode(true);
    document.body.append(svg);
    Array.from(svg.querySelectorAll('.spectre')).forEach(s => {
        s.setAttribute('fill', getComputedStyle(s).fill);
    });
    for(let e of svg.querySelectorAll('#guides, #draggables circle')) {
        e.parentElement.removeChild(e);
    }
    for(let img of svg.querySelectorAll('image.template')) {
        img.setAttribute('width', getComputedStyle(img).width);
    }
    document.body.removeChild(svg);
    const f = new File([svg.outerHTML],'aperiodic-monotile-clothes.svg',{type:'image/svg+xml'});
    const url = URL.createObjectURL(f);
    document.getElementById('link').setAttribute('href',url);
}

function set_colours() {
    const svg = document.querySelector('svg');
    const fd = new FormData(document.querySelector('form'));
    console.log(fd);

    svg.style.setProperty('--y0-lum', document.getElementById('y0-lum').cy.baseVal.value);
    svg.style.setProperty('--y1-lum', document.getElementById('y1-lum').cy.baseVal.value);
    svg.style.setProperty('--y0-hue', document.getElementById('y0-hue').cy.baseVal.value);
    svg.style.setProperty('--y1-hue', document.getElementById('y1-hue').cy.baseVal.value);

    'back front front-pocket hood label-panel left-sleeve right-sleeve'.split(' ').forEach(w => {
        const img = document.getElementById(`hoodie-${w}`);
        svg.style.setProperty(`--hoodie-${w}-x`, img.x.baseVal.value);
        svg.style.setProperty(`--hoodie-${w}-y`, img.y.baseVal.value);
    });

    svg.style.setProperty('--hue-interpolation', fd.get('hue-interpolation'));

    svg.dataset.template = fd.get('template-name');

    'min-hue max-hue sat-scale randomisation template-scale'.split(' ').forEach(w => {
        const n = document.getElementById(w).valueAsNumber;
        svg.style.setProperty(`--${w}`, n);
        const o = document.querySelector(`output[for="${w}"]`);
        if(o) {
            o.textContent = n;
        }
    });
}

Array.from(document.querySelectorAll('form :is(input,select)')).map(i => {
    i.addEventListener('input', set_colours);
});
set_colours();

function getsvg(event) {
    let t = event.target;
    while(t && t.tagName.toLowerCase() != 'svg') {
      t = t.parentElement;
    }
    return t;
}

function getcoords(svg, event) {
    const point = svg.createSVGPoint();
    point.x = event.clientX;
    point.y = event.clientY;
    const position = point.matrixTransform(svg.querySelector('#display').getScreenCTM().inverse());
    return position;
}

function init_draggables() {
    let dragging = false;
    let off = null;
    const svg = document.querySelector('svg');

    function coord_attributes(element) {
        switch(element.tagName.toLowerCase()) {
            case 'circle':
                return ['cx', 'cy'];
            default:
                return ['x', 'y'];
        }
    }

    function get_element_centre(element) {
        const [xattr, yattr] = coord_attributes(element);
        return {x: element[xattr].baseVal.value, y: element[yattr].baseVal.value};
    }

    function set_element_centre(element, p) {
        const [xattr, yattr] = coord_attributes(element);
        element.setAttribute(xattr, p.x - off.x);
        element.setAttribute(yattr, p.y - off.y);
    }

    svg.addEventListener('pointerdown', e => {
        if(e.buttons == 1 && e.target.classList.contains('draggable')) {
            dragging = e.target;
            const p = getcoords(svg, e);
            const {x,y} = get_element_centre(dragging);
            off = {x: p.x - x, y: p.y - y};
        } else {
            dragging = svg;
            const z = svg.querySelector('#display').getScreenCTM().a;
            off = {x: e.clientX - pan.x*z, y: e.clientY - pan.y*z};
        }
    });

    document.body.addEventListener('pointermove', e => {
        if(!dragging) {
            return;
        }

        if(dragging.classList.contains('draggable')) {
            const p = getcoords(svg, e);
            set_element_centre(dragging, p);
            set_colours();
        } else {
            const z = svg.querySelector('#display').getScreenCTM().a;
            pan.x = (e.clientX - off.x) / z;
            pan.y = (e.clientY - off.y) / z;
            setup();
        }
    });
    document.body.addEventListener('pointerup', () => {
        dragging = false;
    });

    svg.addEventListener('wheel', e => {
        const dy = e.deltaY / 1000;
        const zoom_input = document.getElementById('zoom');
        zoom_input.value = zoom_input.valueAsNumber - dy;
        setup();
        e.preventDefault();
    });

    set_colours();
}

init_draggables();