--- === hs.geometry ===
---
--- Utility object to represent points, sizes and rects in a bidimensional plane
---
--- An hs.geometry object can be:
---  * a *point*, or vector2, with `x` and `y` fields for its coordinates
---  * a *size* with `w` and `h` fields for width and height respectively
---  * a *rect*, which has both a point component for one of its corners, and a size component - so it has all 4 fields
---  * a *unit rect*, which is a rect with all fields between 0 and 1; it represents a "relative" rect within another (absolute) rect
---    (e.g. a unit rect `x=0,y=0 , w=0.5,h=0.5` is the quarter portion closest to the origin); please note that hs.geometry
---    makes no distinction internally between regular rects and unit rects; you can convert to and from as needed via the appropriate methods
---
--- You can create these objects in many different ways, via `my_obj=hs.geometry.new(...)` or simply `my_obj=hs.geometry(...)`
--- by passing any of the following:
---  * 4 parameters `X,Y,W,H` for the respective fields - W and H, or X and Y, can be `nil`:
---    * `hs.geometry(X,Y)` creates a point
---    * `hs.geometry(nil,nil,W,H)` creates a size
---    * `hs.geometry(X,Y,W,H)` creates a rect given its width and height from a corner
---  * a table `{X,Y}` creates a point
---  * a table `{X,Y,W,H}` creates a rect
---  * a table `{x=X,y=Y,w=W,h=H}` creates a rect, or if you omit X and Y, or W and H, creates a size or a point respectively
---  * a table `{x1=X1,y1=Y1,x2=X2,y2=Y2}` creates a rect, where X1,Y1 and X2,Y2 are the coordinates of opposite corners
---  * a string:
---    * `"X Y"` or `"X,Y"` creates a point
---    * `"WxH"` or `"W*H"` creates a size
---    * `"X Y/WxH"` or `"X,Y W*H"` (or variations thereof) creates a rect given its width and height from a corner
---    * `"X1,Y1>X2,Y2"` or `"X1 Y1 X2 Y2"` (or variations thereof) creates a rect given two opposite corners
---    * `"[X,Y WxH]"` or `"[X1,Y1 X2,Y2]"` or variations (note the square brackets) creates a unit rect where x=X/100, y=Y/100, w=W/100, h=H/100
---  * a point and a size `"X Y","WxH"` or `{x=X,y=Y},{w=W,h=H}` create a rect
---
--- You can use any of these anywhere an hs.geometry object is expected in Hammerspoon; the constructor will be called for you.

-- remove copy-on-new
--  * another `hs.geometry` object - creates a copy of that object

--TODO guard against infinites etc everywhere (constructor, length,...)
--TODO allow segments? (like rects, but no norm())

local rawget,rawset,type,pairs,ipairs,tonumber,tostring,sqrt=rawget,rawset,type,pairs,ipairs,tonumber,tostring,math.sqrt
local min,max,floor,atan,smatch,sformat=math.min,math.max,math.floor,math.atan,string.match,string.format
local getmetatable,setmetatable=getmetatable,setmetatable

local geometry = {}

--- hs.geometry:type() -> string
--- Method
--- Returns the type of an hs.geometry object
---
--- Parameters:
---  * None
---
--- Returns:
---  * a string describing the type of this hs.geometry object, i.e. 'point', 'size', 'rect' or 'unitrect'; `nil` if not a valid object
local function gettype(t)
  if t._x and t._y then
    if t._w and t._h then
      for _,k in ipairs{'_x','_y','_w','_h'} do
        if t[k]>1.0000000000001 or t[k]<-0.0000000000001 then return 'rect' end
      end
      return 'unitrect'
    else return 'point' end
  elseif t._w and t._h then return 'size' end
end
geometry.type=gettype
local function isRect(t) local typ=gettype(t) return typ=='rect' or typ=='unitrect' end

local function norm(t)
  if t._x and t._w and t._w<0 then t._x=t._x+t._w t._w=-t._w end
  if t._y and t._h and t._h<0 then t._y=t._y+t._h t._h=-t._h end
end

-- constructor stuff
local function parse(s)
  local ws=' *'
  local outeropen=ws..'(%[?)'..ws
  local outerclose=ws..'(%]?)'
  local sepout=ws..'([ ,>/|])'..ws
  local sepin=ws..'([ x*,])'..ws
  local number='(%-?%d*%.?%d*)'
  local x,y,w,h
  local sept,n3,sep2,n4,close
  local open,n1,sep1,n2,i = smatch(s,outeropen..number..sepin..number..'()')
  if i then sept,n3,sep2,n4,close = smatch(s,sepout..number..sepin..number..outerclose,i) end
  if open and #open==0 then open=nil end
  if close and #close==0 then close=nil end
  if (not open)~=(not close) then error('Mismatched brackets or missing element',3) end
  if sep1=='x' or sep1=='*' then --it's a size
    w=tonumber(n1) h=tonumber(n2)
    if not w then error('Cannot parse width',3) end
    if not h then error('Cannot parse height',3) end
  else
    x=tonumber(n1) y=tonumber(n2)
    if not x then error('Cannot parse x coord',3) end
    if not y then error('Cannot parse y coord',3) end
    if sept=='>' or sep2==' ' or sep2==',' then --it's a rectx2y2
      w=tonumber(n3) h=tonumber(n4)
      if not w then error('Cannot parse x2 coord',3) end
      if not h then error('Cannot parse y2 coord',3) end
      w=w-x h=h-y
    elseif sep2 then --it's a rectsize
      w=tonumber(n3) h=tonumber(n4)
      if not w then error('Cannot parse width',3) end
      if not h then error('Cannot parse height',3) end
    end
  end
  if open then
    if not x or not y or not w or not h then error('Missing element',3)
    else x,y,w,h=x/100,y/100,w/100,h/100 end
  end
  return x,y,w,h
end

local function parsearg(a)
  if type(a)=='string' then return parse(a)
  elseif type(a)=='table' then
    local x,y,w,h
    if a[1] then x,y,w,h=a[1],a[2],a[3],a[4]
    else
      x,y=a.x or a.x1,a.y or a.y1
      w,h=a.w or (a.x2 and a.x2-(x or 0)),a.h or (a.y2 and a.y2-(y or 0))
    end
    return x,y,w,h
  end
end
local function maketable(t,x,y,w,h)
  local nt={_x=t._x or x,_y=t._y or y,_w=t._w or w,_h=t._h or h}
  for k,v in pairs(nt) do if type(v)~='number' then nt[k]=nil end end
  return nt
end

local function new(x,y,w,h)
  if y==nil and getmetatable(x)==geometry then return x end -- disable copy-on-new
  local t=maketable({},x,y,w,h)
  for _,a in ipairs{x,y--[[,w,h--]]} do
    if a~=nil and (not t._x or not t._y or not t._w or not t._h) then t=maketable(t,parsearg(a)) end
  end
  norm(t)
  if not gettype(t) then error('Cannot create geometry object, wrong arguments',2) end
  return setmetatable(t,geometry)
end
--- hs.geometry.new(...) -> hs.geometry object
--- Constructor
--- Creates a new hs.geometry object
---
--- Parameters:
---  * ... - see the module description at the top
---
--- Returns:
---  * a newly created hs.geometry object
geometry.new=new
--- hs.geometry.copy(geom) -> hs.geometry object
--- Constructor
--- Creates a copy of an hs.geometry object
---
--- Parameters:
---  * geom - an hs.geometry object to copy
---
--- Returns:
---  * a newly created copy of the hs.geometry object
local function copy(o) return new(o.x,o.y,o.w,o.h) end
geometry.copy=copy

function geometry.__tostring(t)
  local typ=gettype(t)
  local fmt
  if typ=='rect' or typ=='unitrect' then fmt=t.x..','..t.y..','..t.w..','..t.h
  elseif typ=='point' then fmt=t.x..','..t.y
  elseif typ=='size' then fmt=t.w..','..t.h end
  return sformat('hs.geometry.%s(%s)',typ,fmt)
end

-- getters and setters
--- hs.geometry.x
--- Field
--- The x coordinate for this point or rect's corner; changing it will move the rect but keep the same width and height

--- hs.geometry.y
--- Field
--- The y coordinate for this point or rect's corner; changing it will move the rect but keep the same width and height

--- hs.geometry.x1
--- Field
--- Alias for `x`

--- hs.geometry.y1
--- Field
--- Alias for `y`
function geometry.getx(t) return t._x end
function geometry.gety(t) return t._y end
geometry.getx1=geometry.getx
geometry.gety1=geometry.gety
function geometry.getw(t) return t._w end
function geometry.geth(t) return t._h end
function geometry.getx2(t) return (t._w and t._x) and (t._x+t._w) or nil end
function geometry.gety2(t) return (t._h and t._y) and (t._y+t._h) or nil end

function geometry.setx(t,v) t._x=tonumber(v) or error('number expected',3) return t end
function geometry.sety(t,v) t._y=tonumber(v) or error('number expected',3) return t end
geometry.setx1=geometry.setx
geometry.sety1=geometry.sety

--- hs.geometry.xy
--- Field
--- The point component for this hs.geometry object; setting this to a new point will move the rect but keep the same width and height

--- hs.geometry.topleft
--- Field
--- Alias for `xy`
function geometry.getxy(t) return (t._x and t._y) and new(t._x,t._y) or nil end
function geometry.setxy(t,p) p=new(p) t._x=p.x t._y=p.y return t end
geometry.gettopleft=geometry.getxy
geometry.settopleft=geometry.setxy

--- hs.geometry.w
--- Field
--- The width of this rect or size; changing it will keep the rect's x,y corner constant

--- hs.geometry.h
--- Field
--- The height of this rect or size; changing it will keep the rect's x,y corner constant
function geometry.setw(t,v)
  t._w=max(tonumber(v) or error('number expected',3),0) -- disallow negative w,h instead of flipping the rect
  --  norm(t)
  return t
end
function geometry.seth(t,v)
  t._h=max(tonumber(v) or error('number expected',3),0)
  --  norm(t)
  return t
end
--- hs.geometry.x2
--- Field
--- The x coordinate for the second corner of this rect; changing it will affect the rect's width

--- hs.geometry.y2
--- Field
--- The y coordinate for the second corner of this rect; changing it will affect the rect's height
function geometry.setx2(t,v)
  if not t.x then error('not a rect',3) end
  t._w=(tonumber(v) or error('number expected',3))-t._x
  norm(t)
  return t
end
function geometry.sety2(t,v)
  if not t.y then error('not a rect',3) end
  t._h=(tonumber(v) or error('number expected',3))-t._y
  norm(t)
  return t
end

--- hs.geometry.wh
--- Field
--- The size component for this hs.geometry object; setting this to a new size will keep the rect's x,y corner constant

--- hs.geometry.size
--- Field
--- Alias for `wh`
function geometry.getwh(t) return (t._w and t._h) and new(nil,nil,t._w,t._h) or nil end
function geometry.setwh(t,s) s=new(s) t._w=s.w t._h=s.h return t end
geometry.getsize=geometry.getwh
geometry.setsize=geometry.setwh

--- hs.geometry.x2y2
--- Field
--- The point denoting the other corner of this hs.geometry object; setting this to a new point will change the rect's width and height

--- hs.geometry.bottomright
--- Field
--- Alias for `x2y2`
function geometry.getx2y2(t) return (t.x2 and t.y2) and new(t.x2,t.y2) or nil end
function geometry.setx2y2(t,s) s=new(s) t.x2=s.x t.y2=s.y return t end
geometry.getbottomright=geometry.getx2y2
geometry.setbottomright=geometry.setx2y2

--- hs.geometry.table
--- Field
--- The `{x=X,y=Y,w=W,h=H}` table for this hs.geometry object; useful e.g. for serialization/deserialization
function geometry.gettable(t) return {x=t._x,y=t._y,w=t._w,h=t._h} end
function geometry.settable(t,nt) t._x=nt.x t._y=nt.y t._w=nt.w t._h=nt.h return t end

--- hs.geometry.string
--- Field
--- The `"X,Y/WxH"` string for this hs.geometry object (*reduced precision*); useful e.g. for logging

local function ntos(n)
  local f=floor(n)
  return f==n and tostring(f) or sformat('%.2f',n)
end

function geometry.getstring(t)
  local typ=geometry.type(t)
  if typ=='point' then return ntos(t._x)..','..ntos(t._y)--sformat('%.2f,%.2f',t._x,t._y)
  elseif typ=='size' then return ntos(t._w)..'x'..ntos(t._h) --sformat('%.2fx%.2f',t._w,t._h)
  elseif typ=='rect' then return ntos(t._x)..','..ntos(t._y)..'/'..ntos(t._w)..'x'..ntos(t._h) --sformat('%.2f,%.2f/%.2fx%.2f',t._x,t._y,t._w,t._h)
  elseif typ=='unitrect' then return '['..ntos(t._x*100)..','..ntos(t._y*100)..'>'..ntos(t.x2*100)..','..ntos(t.y2*100)..']' --sformat('[%.0f,%.0f>%.0f,%.0f]',t._x*100,t._y*100,t.x2*100,t.y2*100)
  else return ''
  end
end
function geometry.setstring(t,s)
  local nt=new(s)
  t._x=nt._x t._y=nt._y t._w=nt._w t._h=nt._h
  return t
end

--- hs.geometry.center
--- Field
--- A point representing the geometric center of this rect or the midpoint of this vector2; changing it will move the rect/vector accordingly
function geometry.getcenter(t)
  return new((t.x or 0)+(t.w or 0)/2,(t.y or 0)+(t.h or 0)/2)
end
function geometry.setcenter(t,...)
  local c,nc=geometry.getcenter(t),new(...)
  if not nc.x or not nc.y then error('not a point',3) end
  if t.x then t.x=t.x+nc.x-c.x end
  if t.y then t.y=t.y+nc.y-c.y end
  return t
end
--- hs.geometry.length
--- Field
--- A number representing the length of the diagonal of this rect, or the length of this vector2; changing it will scale the rect/vector - see `hs.geometry:scale()`
function geometry.getlength(t)
  if t.w and t.h then t=new(t.w,t.h) end
  return sqrt(t.x^2+t.y^2)
end
function geometry.setlength(t,l)
  l=tonumber(l) or error('number expected',3)
  local ol=geometry.getlength(t)
  if ol>0 then
    t=geometry.scale(t,l/ol)
  end
  return t
end
--- hs.geometry.area
--- Field
--- A number representing the area of this rect or size; changing it will scale the rect/size - see `hs.geometry:scale()`
function geometry.getarea(t)
  return (t.w or 0)*(t.h or 0)
end
function geometry.setarea(t,a,...)
  local oa=geometry.getarea(t)
  if oa>0 then
    if type(a)~='number' then
      a=geometry.getarea(new(a,...))
    end
    if a<=0 or a/2==a then error('invalid area, must be > 0 and < inf',2) end
    local f=sqrt(a/oa)
    geometry.scale(t,f)
  end
  return t
end

--- hs.geometry.aspect
--- Field
--- A number representing the aspect ratio of this rect or size; changing it will reshape the rect/size, keeping its area and center constant
function geometry.getaspect(t)
  return (t.w or t.x)/(t.h or t.y) -- downstream deals with NaN
end

function geometry.setaspect(t,asp,...)
  if t.w and t.h and t.w>0 and t.h>0 then
    if type(asp)~='number' then
      asp=geometry.getaspect(new(asp,...))
    end
    if asp<=0 or asp/2==asp then error('invalid aspect, must be > 0 and < inf',2) end
    local oasp,oc,oa=geometry.getaspect(t),geometry.getcenter(t),geometry.getarea(t)
    t.w=t.w*asp/oasp
    geometry.setarea(t,oa) geometry.setcenter(t,oc)
  end
  return t
end

function geometry.__index(t,k)
  if k == "__luaSkinType" then  -- support table<->NSValue auto-conversion in LuaSkin
    local typ=gettype(t)
    if typ=='rect' or typ=='unitrect' then return "NSRect"
    elseif typ=='point' then return "NSPoint"
    elseif typ=='size' then return "NSSize" end
  else
    local r=rawget(geometry,'get'..k)
    if r then return r(t) else return rawget(geometry,k) end --avoid getting .size metatable fn when it's nil
  end
end

function geometry.__newindex(t,k,v)
  local r=rawget(geometry,'set'..k)
  if r then r(t,v) else rawset(t,k,v) end
end

-- methods
--- hs.geometry:equals(other) -> boolean
--- Method
--- Checks if two geometry objects are equal
---
--- Parameters:
---  * other - another hs.geometry object, or a table or string or parameter list to construct one
---
--- Returns:
---  * `true` if this hs.geometry object perfectly overlaps other, `false` otherwise
function geometry.equals(t1,t2)
  return t1.x==t2.x and t1.y==t2.y and t1.w==t2.w and t1.h==t2.h
end

--- hs.geometry:move(point) -> hs.geometry object
--- Method
--- Moves this point/rect
---
--- Parameters:
---  * point - an hs.geometry object, or a table or string or parameter list to construct one, indicating the x and y displacement to apply
---
--- Returns:
---  * this hs.geometry object for method chaining
function geometry.move(t,...)
  t=new(t) local p=new(...)
  if t.x then t.x=t.x+(p.x or p.w) end
  if t.y then t.y=t.y+(p.y or p.h) end
  return t
end

--- hs.geometry:scale(size) -> hs.geometry object
--- Method
--- Scales this vector2/size, or this rect *keeping its center constant*
---
--- Parameters:
---  * size - an hs.geometry object, or a table or string or parameter list to construct one, indicating the factors for scaling this rect's width and height;
---    if a number, the rect will be scaled by the same factor in both axes
---
--- Returns:
---  * this hs.geometry object for method chaining
function geometry.scale(t,s1,s2,...)
  t=new(t)
  if type(s1)=='number' and not s2 then s2=s1 end
  local s=new(s1,s2,...)
  local c=geometry.getcenter(t)
  local sw,sh=s.w or s.x,s.h or s.y
  if (sw~=0 and sw/2==sw) or (sh~=0 and sh/2==sh) then error('invalid scale factor, must be < inf',2) end
  if t.w and t.h then
    if sw<=0 or sh<=0 then error('invalid scale factor, must be > 0',2) end
    t.w=t.w*sw
    t.h=t.h*sh
    geometry.setcenter(t,c)
  else
    t.x=t.x*sw
    t.y=t.y*sh
  end
  return t
end

--- hs.geometry:fit(bounds) -> hs.geometry object
--- Method
--- Ensure this rect is fully inside `bounds`, by scaling it down if it's larger (preserving its aspect ratio) and moving it if necessary
---
--- Parameters:
---  * bounds - an hs.geometry rect object, or a table or string or parameter list to construct one, indicating the rect that
---    must fully contain this rect
---
--- Returns:
---  * this hs.geometry object for method chaining

function geometry.fit(t,...)
  t=new(t) local bounds=new(...)
  if not isRect(t) then error('not a rect',2) elseif not isRect(bounds) then error('bounds must be a rect',2) end
  if t:inside(bounds) then return t end
  if t.w>bounds.w then t:scale(bounds.w/t.w) end
  if t.h>bounds.h then t:scale(bounds.h/t.h) end
  return t:move(bounds:intersect(t).center-t.center):move((t:intersect(bounds).center-t.center)*2):intersect(bounds)
end

--- hs.geometry:normalize() -> point
--- Method
--- Normalizes this vector2
---
--- Parameters:
---  * None
---
--- Returns:
---  * this hs.geometry point for method chaining
function geometry.normalize(t)
  t=new(t)
  geometry.setlength(t,1)
  return t
end

--- hs.geometry:floor() -> hs.geometry object
--- Method
--- Truncates all coordinates in this object to integers
---
--- Parameters:
---  * None
---
--- Returns:
---  * this hs.geometry point for method chaining
function geometry.floor(t)
  t=new(t)
  for _,k in ipairs{'_x','_y','_w','_h'} do
    if t[k] then t[k]=floor(t[k]) end
  end
  return t
end

--- hs.geometry:vector(point) -> point
--- Method
--- Returns the vector2 from this point or rect's center to another point or rect's center
---
--- Parameters:
---  * point - an hs.geometry object, or a table or string or parameter list to construct one; if a rect, uses the rect's center
---
--- Returns:
---  * an hs.geometry point
function geometry.vector(t,...)
  t=new(t) local t2=new(...)
  if isRect(t) then t=geometry.getcenter(t) end
  if isRect(t2) then t2=geometry.getcenter(t2) end
  return new((t2.x or 0)-(t.x or 0),(t2.y or 0)-(t.y or 0))
end

--- hs.geometry:angle() -> number
--- Method
--- Returns the angle between the positive x axis and this vector2
---
--- Parameters:
---  * None
---
--- Returns:
---  * a number representing the angle in radians
function geometry.angle(t)
  return atan(t.y or t.h,t.x or t.w)
end

--- hs.geometry:angleTo(point) -> number
--- Method
--- Returns the angle between the positive x axis and the vector connecting this point or rect's center to another point or rect's center
---
--- Parameters:
---  * point - an hs.geometry object, or a table or string or parameter list to construct one; if a rect, uses the rect's center
---
--- Returns:
---  * a number representing the angle in radians
function geometry.angleTo(t,...)
  return geometry.angle(geometry.vector(t,...))
end

--- hs.geometry:distance(point) -> number
--- Method
--- Finds the distance between this point or rect's center and another point or rect's center
---
--- Parameters:
---  * point - an hs.geometry object, or a table or string or parameter list to construct one; if a rect, uses the rect's center
---
--- Returns:
---  * a number indicating the distance
function geometry.distance(t,...)
  return geometry.getlength(geometry.vector(t,...))
end

--- hs.geometry:union(rect) -> hs.geometry rect
--- Method
--- Returns the smallest rect that encloses both this rect and another rect
---
--- Parameters:
---  * rect - an hs.geometry rect, or a table or string or parameter list to construct one
---
--- Returns:
---  * a new hs.geometry rect
function geometry.union(t1,...)
  t1=new(t1) local t2=new(...)
  if not isRect(t1) or not isRect(t2) then error('cannot find union for non-rects',2) end
  return new{x=min(t1.x,t2.x),y=min(t1.y,t2.y),x2=max(t1.x2,t2.x2),y2=max(t1.y2,t2.y2)}
end

--- hs.geometry:inside(rect) -> boolean
--- Method
--- Checks if this hs.geometry object lies fully inside a given rect
---
--- Parameters:
---  * rect - an hs.geometry rect, or a table or string or parameter list to construct one
---
--- Returns:
---  * `true` if this point/rect lies fully inside the given rect, `false` otherwise
function geometry.inside(t1,...)
  t1=new(t1) local t2=new(...)
  if gettype(t1)=='size' or gettype(t2)=='size' then error('sizes have no inside',2) end
  return t1.x>=t2.x and t1.y>=t2.y and (t1.x2 or t1.x)<=(t2.x2 or t2.x) and (t1.y2 or t1.y)<=(t2.y2 or t2.y)
end

--- hs.geometry:intersect(rect) -> hs.geometry rect
--- Method
--- Returns the intersection rect between this rect and another rect
---
--- Parameters:
---  * rect - an hs.geometry rect, or a table or string or parameter list to construct one
---
--- Returns:
---  * a new hs.geometry rect
---
--- Notes:
---  * If the two rects don't intersect, the result rect will be a "projection" of the second rect onto this rect's
---    closest edge or corner along the x or y axis; the `w` and/or `h` fields in the result rect will be 0.
function geometry.intersect(t1,...)
  t1=new(t1) local t2=new(...)
  if not isRect(t1) or not isRect(t2) then error('cannot find intersection for non-rects',2) end
  local t1x,t1y,t1x2,t1y2=t1.x,t1.y,t1.x2,t1.y2
  local t2x,t2y,t2x2,t2y2=t2.x,t2.y,t2.x2,t2.y2
  t2x=min(t1x2,max(t1x,t2x)) t2x2=max(t1x,min(t1x2,t2x2))
  t2y=min(t1y2,max(t1y,t2y)) t2y2=max(t1y,min(t1y2,t2y2))
  return new{x=t2x,y=t2y,x2=t2x2,y2=t2y2}
end

-- fun with operator overloading
geometry.__eq=geometry.equals
--geometry.__len=geometry.getlength
geometry.__unm=function(t) return new(t.x and -t.x,t.y and -t.y,t.w,t.h) end
geometry.__add=function(t1,t2) -- :move or :union
  t2=new(t2)
  local tp1,tp2=gettype(t1),gettype(t2)
  if tp1=='size' or tp2=='size' then error('cannot add to a size',2)
  elseif tp1=='point' or tp2=='point' then return new(t1.x+t2.x,t1.y+t2.y,t1.w or t2.w,t1.h or t2.h)
    --  elseif tp2=='point' then return geometry.move(t1,t2)
  else return geometry.union(t1,t2) end
end
geometry.__concat=geometry.union --TODO if segments, allow here
geometry.__sub=function(t1,t2)
  t2=new(t2)
  local tp1,tp2=gettype(t1),gettype(t2)
  if tp1=='size' or tp2=='size' then error('cannot subtract from a size',2) end
  if isRect(t2) then t2=geometry.getcenter(t2) end
  return new(t1.x-t2.x,t1.y-t2.y,t1.w,t1.h)
    --  return geometry.vector(t2,t1)
end
geometry.__mul=function(t1,t2)t1=copy(t1) return geometry.scale(t1,t2) end
geometry.__pow=geometry.intersect
geometry.__lt=function(t1,t2)
  t2=new(t2)
  if t1.w and t1.h and t2.w and t2.h then -- rect<rect
    return geometry.getarea(t1)<geometry.getarea(t2)
  elseif t1.x and t1.y and t2.x and t2.y then
    if t2.w and t2.h then -- point<rect
      return geometry.inside(t1,t2)
    else -- point<point, treat them like vector2
      return geometry.getlength(t1)<geometry.getlength(t2)
    end
  else error('cannot compare geometry objects of different type',2) end
end

--- hs.geometry:toUnitRect(frame) -> hs.geometry unit rect
--- Method
--- Converts a rect into its unit rect within a given frame
---
--- Parameters:
---  * frame - an hs.geometry rect (with `w` and `h` >0)
---
--- Returns:
---  * An hs.geometry unit rect object
---
--- Notes:
---  * The resulting unit rect is always clipped within `frame`'s bounds (via `hs.geometry:intersect()`); if `frame`
---    does not encompass this rect *no error will be thrown*, but the resulting unit rect won't be a direct match with this rect
---    (i.e. calling `:fromUnitRect(frame)` on it will return a different rect)
function geometry.toUnitRect(t,...)
  t=new(t) local frame=new(...)
  if t._w and t._h and gettype(t)~='rect' then error('not a rect',2) end
  if gettype(frame)~='rect' or frame.area==0 then error('frame must be a valid rect',2) end
  -- if frame~=geometry.union(frame,t) then error('frame does not encompass this rect',2) end
  -- no error; but there might be reasons to change this
  t=geometry.intersect(frame,t)
  t._x=(t._x-frame._x)/frame._w
  t._y=(t._y-frame._y)/frame._h
  if t.w then t._w=t._w/frame._w end -- allow 'unit points' as well
  if t.h then t._h=t._h/frame._h end
  return t
end

--- hs.geometry:fromUnitRect(frame) -> hs.geometry rect
--- Method
--- Converts a unit rect within a given frame into a rect
---
--- Parameters:
---  * frame - an hs.geometry rect (with `w` and `h` >0)
---
--- Returns:
---  * An hs.geometry rect object
function geometry.fromUnitRect(t,...)
  t=new(t) local frame=new(...)
  if t._w and t._h and gettype(t)~='unitrect' then error('not a unit rect',2) end
  if gettype(frame)~='rect' or frame.area==0 then error('frame must be a valid rect',2) end
  return new(t._x*frame._w+frame._x,t._y*frame._h+frame._y,t._w and t._w*frame._w,t._h and t._h*frame._h)
end


-- legacy
geometry.hypot=geometry.getlength
geometry.rectMidPoint=geometry.getcenter
geometry.intersectionRect=geometry.intersect
geometry.isPointInRect=geometry.inside

--- hs.geometry:rotateCCW(aroundpoint, ntimes) -> hs.geometry point
--- Method
--- Rotates a point around another point N times
---
--- Parameters:
---  * aroundpoint - an hs.geometry point to rotate this point around
---  * ntimes - the number of times to rotate, defaults to 1
---
--- Returns:
---  * A new hs.geometry point containing the location of the rotated point
function geometry.rotateCCW(p, ap, ntimes)
  p,ap=new(p),new(ap)
  if gettype(p)=='size' or gettype(ap)=='size' then error('cannot rotate sizes',2) end
  local r=new(p.x,p.y)
  for _=1, ntimes or 1 do
    local rx = r.x
    r.x = (ap.x - (r.y - ap.y))
    r.y = (ap.y + (rx - ap.x))
  end
  return r
end

--- hs.geometry.rect(x, y, w, h) -> hs.geometry rect
--- Constructor
--- Convenience function for creating a rect-table
---
--- Parameters:
---  * x - A number containing the horizontal co-ordinate of the top-left point of the rect
---  * y - A number containing the vertical co-ordinate of the top-left point of the rect
---  * w - A number containing the width of the rect
---  * h - A number containing the height of the rect
---
--- Returns:
---  * An hs.geometry rect object
geometry.rect=new
geometry.unitrect=new

--- hs.geometry.point(x, y) -> hs.geometry point
--- Constructor
--- Convenience function for creating a point object
---
--- Parameters:
---  * x - A number containing the horizontal co-ordinate of the point
---  * y - A number containing the vertical co-ordinate of the point
---
--- Returns:
---  * An hs.geometry point object
geometry.point=new

--- hs.geometry.size(w, h) -> hs.geometry size
--- Constructor
--- Convenience function for creating a size object
---
--- Parameters:
---  * w - A number containing a width
---  * h - A number containing a height
---
--- Returns:
---  * An hs.geometry size object
function geometry.size(w, h)
  if type(w)=='table' then return new(w) else return new(nil,nil,w,h) end
end

return setmetatable(geometry,{__call=function(_,...)return new(...) end})
