set role gis;

drop schema if exists hex cascade;
create schema hex;

set search_path to hex,public,gis;

-- return a postgis text polygon
-- the 0,0 hex center is at the origin
-- we scale, rotate, then translate
create or replace function hexgeom(
	xc integer, yc integer, scale float = 1.0, angle float = 0.0, xorigin float = 0.0, yorigin float = 0.0
) returns text language 'plpgsql' as $$
declare
	x float[];
	y float[];
	stride float;
	xoff float;
	yoff float;
	i integer;
	cosa float;
	sina float;
begin
	stride := scale * 2.0 / sqrt(3.0);
	yoff = scale / 2.0;
	x[1] := (2 + 3 * xc ) * stride;
	x[4] := (-2 + 3 * xc) * stride;
	x[2] := (1 + 3 * xc ) * stride;
	x[6] := (1 + 3 * xc ) * stride;
	x[3] := (-1 + 3 * xc ) * stride;
	x[5] := (-1 + 3 * xc ) * stride;

	-- hex coordinates are positive down
	y[1] := (0 - 2 * yc + xc % 2) * yoff;
	y[4] := (0 - 2 * yc + xc % 2) * yoff;
	y[2] := (1 - 2 * yc + xc % 2) * yoff;
	y[3] := (1 - 2 * yc + xc % 2) * yoff;
	y[5] := (-1 - 2 * yc + xc % 2) * yoff;
	y[6] := (-1 - 2 * yc + xc % 2) * yoff;

	if angle != 0.0 then
		cosa := cos(angle);
		sina := sin(angle);

		for i in 1..6 loop
			xoff := cosa * x[i] + sina * y[i];
			y[i] := -sina * x[i] + cosa * y[i];
			x[i] := xoff;
		end loop;
	end if;
	if xorigin != 0.0 or yorigin != 0.0 then
		for i in 1..6 loop
			y[i] := y[i] + yorigin;
			x[i] := x[i] + xorigin;
		end loop;
	end if;

	return format('POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s, %s %s))',
		x[1], y[1], x[2], y[2], x[3], y[3], x[4], y[4], x[5], y[5], x[6], y[6]);
end;
$$ immutable strict;

create or replace function hexcovergrid(
	coverage geometry,
	hexsize numeric
	angle numeric = 0.0,
	full boolean = true,
	srid	integer, -- will default to the srid of the coverage
	origin	geometry(POINT) -- this would be the 0,0 hex center, or perhaps 1,1
) returns table (x integer, y integer, cantor numeric, hex geometry) as $$
declare
	xdim integer,
	ydim integer,
	rcover geometry;
	hex geometry;
	stride double precision;
	xorigin float;
	yorigin float;
	x integer;
	y integer;
	xoff double precision;
begin
	-- the origin will be the center of the 1,1 hex.
	-- if null, then the origin will be set to fit in the
	-- covering box.

	-- counter rotate geometry, fill, rotate grid
	rcover = ST_Rotate(st_transform(coverage,srid), -angle*3.1415926/180.0);
	if srid is null and rcover is not null then
		srid := st_srid(rcover);
	end if;
	
	stride := hexsize * 2.0 / sqrt(3.0);
	-- the center of the 1,1 hex should be one stride right and one half tilesize
	-- down from the upper right of the coverage.  That is, the upper right of
	-- (the bounding box of) the coverage should coincide with the "northwest"
	-- vertex of hex 1,1
	xorigin = st_xmin(rcover) + stride;
	yorigin = st_ymax(rcover) + hexsize / 2.0;

	xdim := ((ST_XMax(rcover) -  ST_XMin(rcover))/hexsize*2/sqrt(3.0))::integer;
	ydim := ((ST_YMax(rcover) -  ST_YMin(rcover))/hexsize)::integer;

	--raise notice 'coverage = % % % %', xmin, ymin, xdim, ydim;
	
        for x in 0 .. xdim loop
                for y in 0 .. ydim loop
			hex := st_geomfromtext(hexgeom(x, ydim-y, hexsize,angle,xorigin,yorigin),srid);
			if not full and not st_intersects(hex, rcover) then
				next;
			end if;
			return query select
		      	 	x, ydim-y,
		       		hex.cantor(ARRAY[x,ydim-y]), 
				hex
			;
                end loop;
        end loop;
END;
$$
language 'plpgsql'
;

create or replace function hexgrid(
	xdim integer,
	ydim integer,
	angle numeric,
	coverage geometry,
	srid	integer,
	origin	geometry(POINT),
	tilesize numeric
) returns table (x integer, y integer, cantor numeric, hex geometry) as $$
declare
	rcover geometry;
	stride double precision;
	x integer;
	y integer;
	xmin double precision;
	ymin double precision;
	xoff double precision;
begin
	-- the origin will be the center of the 1,1 hex.
	-- if null, then the origin will be set to fit in the
	-- covering box.

	-- xdim, and ydim can be null, in which case they are
	-- calculated from the tilesize and the covering box
	
	-- calculate ydim

	-- precedence: ydim, xdim, tilesize

	-- counter rotate geometry, fill, rotate grid
	rcover = ST_Rotate(st_transform(coverage,srid), -angle*3.1415926/180.0);
	if srid is null and rcover is not null then
		srid := st_srid(rcover);
	end if;
	
	xmin := ST_XMin(rcover);
	ymin := ST_YMin(rcover);
	
	stride := tilesize * 2.0 / sqrt(3.0);

	xdim := ((ST_XMax(rcover) -  ST_XMin(rcover))/tilesize*2/sqrt(3.0))::integer;
	ydim := ((ST_YMax(rcover) -  ST_YMin(rcover))/tilesize)::integer;

	--raise notice 'coverage = % % % %', xmin, ymin, xdim, ydim;
	
        for x in 0 .. xdim loop
                for y in 0 .. ydim loop
			return query select
		      	 	x, ydim-y,
		       		hex.cantor(ARRAY[x,ydim-y]), 
				st_geomfromtext(hexgeom(x, ydim-y, tilesize,angle),srid)
			;
                end loop;
        end loop;
END;
$$
language 'plpgsql'
;

-- create some hexagon ops

create function ijk(xy integer[]) returns integer[] language 'plpgsql' as $$
declare
	ijk integer[];
begin
	if array_length(xy,1) = 3 then
		return xy;
	end if;

	ijk[1] := xy[1];
	ijk[2] := -xy[2];

	if xy[1] < 0 then
		ijk[2] := ijk[2] + (-xy[1]+1) / 2;
	else
		ijk[2] := ijk[2] - xy[1]/2;
	end if;

	ijk[3] := -ijk[1] - ijk[2];

	return ijk;
end
$$ immutable strict;

create function ijk(x integer, y integer) returns integer[] language 'sql' as $$
	select hex.ijk(ARRAY[x,y]);
$$ immutable strict;

-- invert the cantor paring
create function xy(hex numeric) returns integer[] language 'plpgsql' as $$
declare
	xy integer[];
	w integer;
	t integer;
	py integer;
begin
	if hex >= 0 then
		hex := hex-1;
		w := floor((sqrt(8.0 * hex + 1.0) - 1.0)/2.0)::integer;
		t := (w * w + w)/2;
		py := hex - t;

		xy = ARRAY[w-py,py];
	-- handle the negative cases
	else 
		hex := -hex;
		hex := hex-1;
		w := floor((sqrt(8.0 * hex + 1.0) - 1.0)/2.0)::integer;
		t := (w * w + w)/2;
		py := hex - t;

		xy = ARRAY[w-py,py];

		if xy[1] % 2 = 1 then
			xy[1] = -(xy[1]+1)/2;
		else
			xy[1] = xy[1]/2;
		end if;
		if xy[2] % 2 = 1 then
			xy[2] = -(xy[2]+1)/2;
		else
			xy[2] = xy[2]/2;
		end if;
	end if;

	return xy;
end;
$$ immutable strict;

create function ijk(hex numeric) returns integer[] language 'sql' as $$
	select hex.ijk(hex.xy(hex));
$$ immutable strict;

create function xy(ijk integer[]) returns integer[] language 'plpgsql' as $$
declare
	xy integer[];
begin
	-- make sure this is really an ijk
	-- just no-op if it's already an
	-- xy, so this is sort of a coercion function
	if array_length(ijk,1) = 2 then
		return ijk;
	end if;
	xy[1] := ijk[1];
	xy[2] := -ijk[2];

	if ijk[1] < 0 then
		xy[2] := xy[2] + (-ijk[1] + 1) / 2;
	else
		xy[2] := xy[2] - ijk[1]/2;
	end if;
	return xy;
end;
$$;

CREATE OR REPLACE FUNCTION vec_add(arr1 integer[], arr2 integer[])
RETURNS integer[] AS
$$
SELECT array_agg(result)
FROM (SELECT tuple.val1 + tuple.val2 AS result
      FROM (SELECT UNNEST($1) AS val1
                   ,UNNEST($2) AS val2
                   ,generate_subscripts($1, 1) AS ix) tuple
      ORDER BY ix) inn;
$$ LANGUAGE SQL IMMUTABLE STRICT;

create function adjhex(dir integer, hex integer[]) returns integer[] language 'plpgsql' as $$
declare
	adj integer[];
begin
	if array_length(hex,1) = 2 then
		hex = hex.ijk(hex);
	end if;

	dir := dir % 6;
	if dir = 2 then
		hex := hex.vec_add(hex, ARRAY[-1, 1, 0]);
	elsif dir = 1 then
		hex := hex.vec_add(hex, ARRAY[ 0, 1,-1]);
	elsif dir = 0 then
		hex := hex.vec_add(hex, ARRAY[ 1, 0,-1]);
	elsif dir = 5 then
		hex := hex.vec_add(hex, ARRAY[ 1,-1, 0]);
	elsif dir = 4 then
		hex := hex.vec_add(hex, ARRAY[ 0,-1, 1]);
	elsif dir = 3 then
		hex := hex.vec_add(hex, ARRAY[-1, 0, 1]);
	else
		-- error
	end if;
	return hex;
end;
$$ immutable strict;

create function adjhex(dir integer, hex numeric) returns integer[] language 'sql' as $$
	select hex.adjhex(dir, hex.ijk(hex));
$$ immutable strict;

-- TODO make this work for any, or inline it
create function natcantor(hex integer[]) returns numeric language 'sql' as $$
	select cast((hex[1] + hex[2]) * (hex[1] + hex[2] + 1) / 2 + hex[2] + 1 as numeric);
$$ immutable strict;

create function cantor(hex integer[]) returns numeric language 'plpgsql' as $$
begin
	-- convert to xy
	if array_length(hex,1) = 3 then
		hex = hex.xy(hex);
	end if;

	if hex[1] < 0 or hex[2] < 0 then
		hex[1] = hex[1] * 2;
		hex[2] = hex[2] * 2;
		if hex[1] < 0 then
			hex[1] = -hex[1] - 1;
		end if;
		if hex[2] < 0 then
			hex[2] = -hex[2] - 1;
		end if;
		return -cast((hex[1] + hex[2]) * (hex[1] + hex[2] + 1) / 2 + hex[2] + 1 as numeric);
	end if;

	return cast((hex[1] + hex[2]) * (hex[1] + hex[2] + 1) / 2 + hex[2] + 1 as numeric);
end;
$$ immutable strict;

create function adjacent(hex numeric) returns setof numeric language 'sql' as $$
select hex.cantor(hex.adjhex(dir, hex)) from generate_series(0,5) as dir;
$$ immutable strict;

create function adjacent(dir integer, hex numeric) returns numeric language 'sql' as $$
	select hex.cantor(hex.adjhex(dir, hex.ijk(hex)));
$$ immutable strict;

create function within(hex numeric, range integer = 1, include boolean = false) returns setof numeric language 'plpgsql' as $$
declare
	c integer[]; -- cantor coordinates
	base integer[];
begin
	if range <= 0 then
		return;
	end if;
	if include then
		return next hex;
	end if;

	-- we could in theory eliminate the inner loop and
	-- do a return query
	base = hex.ijk(hex);
	for r in 1 .. range loop
		c = base;
		c[1] := c[1] + r;
		c[3] := -c[1] - c[2]; -- keep the invariant
		hex := hex.cantor(c);
		
		for h in 0 .. r * 6 - 1 loop
			return next hex;
			hex = hex.adjacent(h/r+2,hex);
		end loop;
	end loop;
	return;
end;
$$ immutable strict;

create function within_array(hex numeric, range integer = 1, include boolean = false) returns numeric[] language 'sql' as $$
select array_agg(c) from hex.within(hex, range, include) c;
$$ immutable strict;

create function atrange(hex numeric, range integer = 1) returns setof numeric language 'plpgsql' as $$
declare
	c integer[]; -- cantor coordinates
begin
	if range = 0 then
		return next hex;
	end if;
	if range <= 0 then
		return;
	end if;

	c = ijk(hex);
	c[1] := c[1] + range;
	c[3] := -c[1] - c[2]; -- keep the invariant
	hex := hex.cantor(c);
	
	for q in 0 .. range * 6 - 1 loop
		return next hex;
		hex = hex.adjacent(q/range+2,hex);
	end loop;
	return;
end;
$$ immutable strict;

select
xy(ARRAY[1,2]),
ijk(xy(ARRAY[1,2])),
xy(ijk(xy(ARRAY[1,2])))
;

select xy(ARRAY[2,2]), dir, xy(adjhex(dir, ijk(ARRAY[2,2]))) from generate_series(0,5) as dir;
select
xy(adjhex(dir, ijk(ARRAY[1,2]))),
cantor(xy(adjhex(dir, ijk(ARRAY[1,2])))),
xy(cantor(xy(adjhex(dir, ijk(ARRAY[1,2])))))
from generate_series(0,5) as dir;

select cantor(ARRAY[4,3]);
--select * from within(cantor(ARRAY[4,4]), 2);
select h, xy(h) from within(cantor(ARRAY[4,3]), 2) as h;
