;; cycle-placement -- toggle a window's placement between multiple candidate
;; positions.
;;
;; Copyright 2014 Cory Echols <cory@sixtimesnine.org>

(define-structure cycle-placement
	(export
		cycle-window-placement-on-current-head
		move-resize-frame-by-percent
		move-resize-frame-by-percent-on-current-head
	)

	(open rep
		rep.system
		sawfish.wm.state.maximize
		sawfish.wm.windows
		sawfish.wm.misc
		sawfish.wm.util.rects
	)

	;; Standard reduce thing
	(defun reduce (f seq start)
		(cond
			((equal (length seq) 0) start)
			(t (reduce f (cdr seq) (funcall f start (car seq))))
		)
	)

	;; Find dock windows on the current window's head.
	(defun docks-on-current-head (win)
		(filter-windows (lambda (w)
			(and
				(equal (current-head win) (current-head w))
				(window-get w 'dock-type))))
	)

	;; Return a new size for window w, so that it will not overlap any dock
	;; windows within the rectangle defined by (x y width height)
	(defun bump-window-position-for-docks (win x y width height)
		(let* ((win-rect (list x y (+ x width) (+ y height)))
			   (largest-rect (reduce
							   find-largest-subrect
							   (rectangles-from-windows (docks-on-current-head (input-focus)))
							   win-rect)
			   )
			   (left (nth 0 largest-rect))
			   (top (nth 1 largest-rect))
			   (width (- (nth 2 largest-rect) left))
			   (height (- (nth 3 largest-rect) top))
			  )
			(list left top width height)
		)
	)

	(defun max-by-weight-func-helper (f seq max-weight max-elem)
		(cond
			((equal (length seq) 0) max-elem)
			((> max-weight (funcall f (car seq))) (max-by-weight-func-helper f (cdr seq) max-weight max-elem))
			(t (max-by-weight-func-helper f (cdr seq) (funcall f (car seq)) (car seq)))
		)
	)

	;; Apply f to each elem in seq.  Return the elem in seq that results in the
	;; max value for f.
	(defun max-by-weight-func (f seq)
		(cond
			((equal (length seq) 0) seq)
			(t (max-by-weight-func-helper f (cdr seq) (funcall f (car seq)) (car seq)))
		)
	)

	;; Find the largest (by area) rectangle that would result from
	;; removing r1 from r2
	(defun find-largest-subrect (r1 r2)
		(max-by-weight-func rectangle-area (rect-minus r1 r2))
	)

	;; Move and resize the frame to the supplied position and size
	(defun move-resize-frame-to (window x y width height)
		(move-window-to window x y)
		(resize-frame-to window width height)
	)

	;; Resize the window's frame to the provided width and height.
	(defun resize-frame-to (window width height)
		(let* ((dim-difference (window-frame-difference window))
			   (w-diff (car dim-difference))
			   (h-diff (cdr dim-difference))
			   (new-width (- width w-diff))
			   (new-height (- height h-diff)))
			(unmaximize-window window)
			(resize-window-to window new-width new-height))
	)

	;; Calculate how much frame decorations add to a window's size.
	(defun window-frame-difference (window)
		(let* ((win-dim (window-dimensions window))
			  (win-w (car win-dim))
			  (win-h (cdr win-dim))
			  (frame-dim (window-frame-dimensions window))
			  (frame-w (car frame-dim))
			  (frame-h (cdr frame-dim)))
			(cons (- frame-w win-w) (- frame-h win-h)))
	)

	;; Move and resize a frame, where supplied coordinates and size are
	;; a percentage of the current screen dimensions.
	;; Also avoid any docks or panels within (x y width height)
	(defun move-resize-frame-by-percent (window x y width height)
		(let* ((screen-x (car (screen-dimensions)))
			   (screen-y (cdr (screen-dimensions)))
			   (new-x (exact-percent x screen-x))
			   (new-y (exact-percent y screen-y))
			   (new-width (exact-percent width screen-x))
			   (new-height (exact-percent height screen-y))
			   (bumped-position (bump-window-position-for-docks window new-x new-y new-width new-height))
			  )
			(move-resize-frame-to
			  window
			  (nth 0 bumped-position)
			  (nth 1 bumped-position)
			  (nth 2 bumped-position)
			  (nth 3 bumped-position)
			))
	)

	;; Move and resize a frame, where suppplied coordinates and size are a
	;; percentage of the current monitor dimensions
	(defun move-resize-frame-by-percent-on-current-head (window x y width height)
		(let* ((head-x (car (current-head-dimensions window)))
			   (head-y (cdr (current-head-dimensions window)))
			   (head-offset-x (car (current-head-offset window)))
			   (head-offset-y (cdr (current-head-offset window)))
			   (new-x (+ head-offset-x (exact-percent x head-x)))
			   (new-y (+ head-offset-y (exact-percent y head-y)))
			   (new-width (exact-percent width head-x))
			   (new-height (exact-percent height head-y))
			   (bumped-position (bump-window-position-for-docks window new-x new-y new-width new-height))
			  )
			(move-resize-frame-to
			  window
			  (nth 0 bumped-position)
			  (nth 1 bumped-position)
			  (nth 2 bumped-position)
			  (nth 3 bumped-position)
			)
		)
	)

	;; Return 'percent' of 'quantity' as an exact integer.
	;; librep has muliple numeric types, but it looks like sawfish wants
	;; integers for everything.  There's probably a better way to do this.
	(defun exact-percent (percent quantity)
		(inexact->exact (exact->inexact (* ( / percent 100) quantity) ))
	)

	;; Cycle window through the list of candidate positions
	;; _window_ is a window, as would be returned by sawfish functions such as
	;; `(input-focus)`.
    ;;
	;; _list-of-candidates_ is a list-of-lists.  Each of the contained lists is
	;; a candidate position.  The candidate position describes where the window
	;; should be placed as `(x-percent y-percent width-percent height-percent)`.
	;; The first invocation of the function will place the window according to
	;; the first entry in `list-of-candidates`.  Successive invocations will
	;; select from the following entries in the list.
	(defun cycle-window-placement-on-current-head (window candidate-positions)
		(let* ((last-cycled-position (window-get window 'cte-last-cycled-pos 'none))
			   (last-cycled-member (member last-cycled-position candidate-positions))
			   (next-cycled-position (cond
										((< 1 (length last-cycled-member))
											(nth 1 last-cycled-member))
										(t
											(nth 0 candidate-positions)))))
			(window-put window 'cte-last-cycled-pos next-cycled-position)
			(move-resize-frame-by-percent-on-current-head
				window
				(nth 0 next-cycled-position)
				(nth 1 next-cycled-position)
				(nth 2 next-cycled-position)
				(nth 3 next-cycled-position)
			)
		)
	)
)


# stuff done once per exucution--not depending on r
function InitWRE!(o::StrBootTest{T}) where T
	o.Repl.kZ>1 && (o.numer_b = Vector{T}(undef,nrows(o.Repl.RRpar)))

	iszero(o.granular) && (o.Repl.Zperp = o.DGP.Zperp = Matrix{T}(undef,0,0))  # drop this potentially large array

	o.liml && o.Repl.kZ==1 && o.Nw==1 && (o.As = o.β̈s = zeros(1, o.B+1))

	o.S✻ZperpU              = [Matrix{T}(undef, o.Repl.kZperp, o.N✻) for _ ∈ 0:o.Repl.kZ]
	o.invZperpZperpS✻ZperpU = [Matrix{T}(undef, o.Repl.kZperp, o.N✻) for _ ∈ 0:o.Repl.kZ]
	o.S✻YU                  = [Vector{T}(undef, o.N✻) for _ ∈ 0:o.Repl.kZ, _ ∈ 0:o.Repl.kZ]
	o.S✻XU                  = [Matrix{T}(undef, o.DGP.kX, o.N✻) for _ ∈ 0:o.Repl.kZ]
	o.invXXS✻XU             = [Matrix{T}(undef, o.DGP.kX, o.N✻) for _ ∈ 0:o.Repl.kZ]
	o.S✻UU                  = [Vector{T}(undef, o.N✻) for _ ∈ 0:o.Repl.kZ, _ ∈ 0:o.Repl.kZ]
	o.S✻⋂XU₂      = Array{T,3}(undef, o.Repl.kX, o.N✻⋂, o.kY₂)
	o.S✻⋂XU₂RparY = Array{T,3}(undef, o.Repl.kX, o.N✻⋂, o.Repl.kZ)
	o.invXXS✻XU₂  = Array{T,3}(undef, o.Repl.kX, o.N✻ , o.kY₂)
	o.invXXS✻XU₂RparY = Array{T,3}(undef, o.Repl.kX, o.N✻, o.Repl.kZ)
	o.S✻XU₂ = Array{T,3}(undef, o.Repl.kX, o.N✻, o.kY₂)
	o.S✻XU₂RparY = Array{T,3}(undef, o.Repl.kX, o.N✻, o.Repl.kZ)
	o.S✻ZperpU₂ = Array{T,3}(undef, o.Repl.kZperp, o.N✻, o.kY₂)
	o.S✻ZperpU₂RparY = Array{T,3}(undef, o.Repl.kZperp, o.N✻, o.Repl.kZ)
	o.invZperpZperpS✻ZperpU₂ = Array{T,3}(undef, o.Repl.kZperp, o.N✻, o.kY₂)
	o.invZperpZperpS✻ZperpU₂RparY = Array{T,3}(undef, o.Repl.kZperp, o.N✻, o.Repl.kZ)

	o.T1L = isone(o.Nw) ? [Matrix{T}(undef, o.Repl.kX, ncols(o.v))] :
												[Matrix{T}(undef, o.Repl.kX, length(o.WeightGrp[1])), Matrix{T}(undef, o.Repl.kX, length(o.WeightGrp[end]))]
	o.T1R = deepcopy(o.T1L)
	o.T2 = Matrix{T}(undef, o.N✻, o.N✻)
	o.robust && o.bootstrapt && iszero(o.granular) &&
		(o.negS✻UMZperpX = [Array{T,3}(undef, o.Repl.kX, o.N⋂, o.N✻) for _ in 0:o.Repl.kZ])

	if o.bootstrapt
		if o.robust && o.granular
			o.S✻UMZperp = [Matrix{T}(undef, o.Nobs, o.N✻) for _ ∈ 0:o.Repl.kZ]
			o.S✻UPX     = [Matrix{T}(undef, o.Nobs, o.N✻) for _ ∈ 0:o.Repl.kZ]
		end
		if o.liml || !o.robust
			o.YY✻_b   = zeros(o.Repl.kZ+1, o.Repl.kZ+1)
			o.YPXY✻_b = zeros(o.Repl.kZ+1, o.Repl.kZ+1)
		end
		o.NFE>0 && (o.bootstrapt || !isone(o.κ) || o.liml) && (o.CTFEU = Vector{Matrix{T}}(undef, o.Repl.kZ+1))
	end
	o.S✻⋂XY₂      = o.Repl.S✻⋂XY₂     - o.Repl.S✻⋂XZperp     * o.Repl.invZperpZperpZperpY₂  - o.Repl.invZperpZperpZperpX' * (o.Repl.S✻⋂ZperpY₂  - o.Repl.S✻⋂ZperpZperp * o.Repl.invZperpZperpZperpY₂ )
	o.S✻⋂XX       = o.Repl.S✻⋂XX      - o.Repl.S✻⋂XZperp     * o.Repl.invZperpZperpZperpX   - o.Repl.invZperpZperpZperpX' * (o.Repl.S✻⋂XZperp'  - o.Repl.S✻⋂ZperpZperp * o.Repl.invZperpZperpZperpX  )
	o.S✻⋂XDGPZ    = o.DGP.S✻⋂XZpar    - o.Repl.S✻⋂XZperp     * o.DGP.invZperpZperpZperpZpar - o.Repl.invZperpZperpZperpX' * (o.DGP.S✻⋂ZperpZpar - o.Repl.S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpZpar)
	o.S✻⋂Xy₁      = o.Repl.S✻⋂Xy₁     - o.Repl.S✻⋂XZperp     * o.Repl.invZperpZperpZperpy₁  - o.Repl.invZperpZperpZperpX' * (o.Repl.S✻⋂Zperpy₁  - o.Repl.S✻⋂ZperpZperp * o.Repl.invZperpZperpZperpy₁ )
	  S✻⋂ZperpX   = o.Repl.S✻⋂XZperp' - o.Repl.S✻⋂ZperpZperp * o.Repl.invZperpZperpZperpX
	o.DGP.restricted &&
		(o.S✻⋂X_DGPZR₁ = o.DGP.S✻⋂XZR₁     - o.Repl.S✻⋂XZperp     * o.DGP.invZperpZperpZperpZR₁  - o.Repl.invZperpZperpZperpX' * (o.DGP.S✻⋂ZperpZR₁  - o.Repl.S✻⋂ZperpZperp * o.DGP.invZperpZperpZperpZR₁ ))

	o.invXXS✻⋂XY₂     = o.Repl.invXX * o.S✻⋂XY₂
	o.invXXS✻⋂XX      = o.Repl.invXX * o.S✻⋂XX
	o.invXXS✻⋂XDGPZ   = o.Repl.invXX * o.S✻⋂XDGPZ
	o.invXXS✻⋂Xy₁     = o.Repl.invXX * o.S✻⋂Xy₁
	o.DGP.restricted &&
		(o.invXXS✻⋂XDGPZR₁ = o.Repl.invXX * o.S✻⋂X_DGPZR₁)

	_S✻ZperpY₂      = @panelsum(o, o.Repl.S✻⋂ZperpY₂ , o.info✻_✻⋂)  # moments of variables _before_ FWL processing
	_S✻Zperpy₁      = dropdims(@panelsum(o, reshape(o.Repl.S✻⋂Zperpy₁,Val(3)), o.info✻_✻⋂); dims=3)
	_S✻ZperpDGPZpar = @panelsum(o, o.DGP.S✻⋂ZperpZpar, o.info✻_✻⋂)
	o.DGP.restricted &&
		(_S✻ZperpDGPZR₁  = @panelsum(o, o.DGP.S✻⋂ZperpZR₁, o.info✻_✻⋂))

	S✻ZperpZperp    = @panelsum(o, o.Repl.S✻⋂ZperpZperp, o.info✻_✻⋂)
	o.S✻XY₂         = @panelsum(o, o.S✻⋂XY₂   , o.info✻_✻⋂)
	o.S✻XX          = @panelsum(o, o.S✻⋂XX    , o.info✻_✻⋂)
	o.S✻XDGPZ       = @panelsum(o, o.S✻⋂XDGPZ, o.info✻_✻⋂)
	o.S✻Xy₁         = dropdims(@panelsum(o, reshape(o.S✻⋂Xy₁,Val(3)), o.info✻_✻⋂); dims=3)
	o.S✻ZperpX      = @panelsum(o, S✻⋂ZperpX, o.info✻_✻⋂)
	o.S✻ZperpY₂     = _S✻ZperpY₂ - S✻ZperpZperp * o.Repl.invZperpZperpZperpY₂
	o.S✻ZperpDGPZ   = _S✻ZperpDGPZpar - S✻ZperpZperp * o.DGP.invZperpZperpZperpZpar
	o.S✻Zperpy₁     = _S✻Zperpy₁ - S✻ZperpZperp * o.Repl.invZperpZperpZperpy₁
	if o.DGP.restricted
		o.S✻XZR₁        = @panelsum(o, o.S✻⋂X_DGPZR₁, o.info✻_✻⋂)
		o.S✻ZperpDGPZR₁ = @panelsum(o, o.DGP.S✻⋂ZperpZR₁ , o.info✻_✻⋂) - S✻ZperpZperp * o.DGP.invZperpZperpZperpZR₁
	end

	if o.NFE>0 && (o.liml || !isone(o.κ) || o.bootstrapt)
		  CT✻⋂FEX  = [crosstabFE(o, o.Repl.X₁, o.info✻⋂) crosstabFE(o, o.Repl.X₂, o.info✻⋂)]
		o.CT✻FEX   = @panelsum(o, CT✻⋂FEX, o.info✻_✻⋂)
		o.CT✻FEY₂  = crosstabFE(o, o.DGP.Y₂, o.info✻)
		o.CT✻FEZ   = crosstabFE(o, o.DGP.Z, o.info✻)
		o.CT✻FEy₁  = crosstabFE(o, o.DGP.y₁, o.info✻)
		o.DGP.restricted &&
			(o.CT✻FEZR₁ = crosstabFE(o, o.DGP.ZR₁, o.info✻))
	end

	if ((o.robust && o.bootstrapt) || o.liml || !o.robust || !isone(o.κ))
		S✻⋂ReplZX = (o.Repl.S✻⋂XZpar - o.Repl.S✻⋂XZperp * o.Repl.invZperpZperpZperpZpar - o.Repl.invZperpZperpZperpX' * (o.Repl.S✻⋂ZperpZpar - o.Repl.S✻⋂ZperpZperp * o.Repl.invZperpZperpZperpZpar))'
	end

	if o.bootstrapt & o.robust
		o.info⋂_✻⋂ = panelsetup(o.ID✻⋂, o.subcluster+1:o.NClustVar)

		o.S⋂ReplZX = @panelsum(o, S✻⋂ReplZX, o.info⋂_✻⋂)
		S⋂ZperpX  = @panelsum(o, S✻⋂ZperpX, o.info⋂_✻⋂)
		o.S⋂Xy₁  = dropdims(@panelsum(o, reshape(o.S✻⋂Xy₁,Val(3)), o.info⋂_✻⋂); dims=3)

		o.J⋂s = isone(o.Nw) ? [Array{T,3}(undef, o.N⋂, ncols(o.v), o.Repl.kZ)] :
		                      [Array{T,3}(undef, o.N⋂, length(o.WeightGrp[1]), o.Repl.kZ), Array{T,3}(undef, o.N⋂, length(o.WeightGrp[end]), o.Repl.kZ)]

		if o.granular
			o.crosstab✻ind = o.Nobs==o.N✻ ? Vector(diagind(FakeArray(o.N✻,o.N✻))) : LinearIndices(FakeArray(o.Nobs,o.N✻))[CartesianIndex.(1:o.Nobs, o.ID✻)]
			o.XinvXX = X₁₂B(o, o.Repl.X₁, o.Repl.X₂, o.Repl.invXX)
			o.PXZ    = X₁₂B(o, o.Repl.X₁, o.Repl.X₂, o.Repl.invXXXZ)
		else
			inds = o.subcluster>0 ?
							[CartesianIndex(j,i) for (j,v) ∈ enumerate(o.info⋂_✻⋂) for i ∈ v] :  # crosstab ∩,* is wide
							o.NClustVar == o.NBootClustVar ?
									[CartesianIndex(i,i) for i ∈ 1:o.N✻⋂] :  # crosstab *,∩ is square
									[CartesianIndex(i,j) for (j,v) ∈ enumerate(o.clust[o.BootClust].info) for i ∈ v]  # crosstab ∩,* is tall
			inds = [CartesianIndex(k,I) for I ∈ inds for k ∈ 1:o.Repl.kX]
			o.crosstab⋂✻ind = LinearIndices(FakeArray(Tuple(max(inds...))...))[inds]

			o.S⋂XZperpinvZperpZperp = S⋂ZperpX' * o.Repl.invZperpZperp

			o.Q    = Array{T,3}(undef, o.N✻, o.N⋂, o.N✻)
			o.NFE>0 && (o.CT⋂FEX = o.invFEwt .* @panelsum(o, CT✻⋂FEX, o.info⋂_✻⋂))

			o.β̈v = isone(o.Nw) ? [Matrix{T}(undef, o.N✻, ncols(o.v))] :
			                     [Matrix{T}(undef, o.N✻, length(o.WeightGrp[1])), Matrix{T}(undef, o.N✻, length(o.WeightGrp[end]))]

		end
	end

	if o.liml || !o.robust || !isone(o.κ)  # cluster-wise moments after FWL
		o.S✻Y₂Y₂     = o.Repl.S✻Y₂Y₂    - _S✻ZperpY₂'   * o.DGP.invZperpZperpZperpY₂   - o.DGP.invZperpZperpZperpY₂'   * o.S✻ZperpY₂
		o.S✻DGPZDGPZ = o.DGP.S✻ZparZpar - _S✻ZperpDGPZpar' * o.DGP.invZperpZperpZperpZpar - o.DGP.invZperpZperpZperpZpar' * o.S✻ZperpDGPZ
		o.S✻DGPZY₂   = o.DGP.S✻ZparY₂   - _S✻ZperpDGPZpar' * o.DGP.invZperpZperpZperpY₂   - o.DGP.invZperpZperpZperpZpar' * o.S✻ZperpY₂
		o.S✻DGPZy₁   = o.DGP.S✻Zpary₁   - _S✻ZperpDGPZpar' * o.DGP.invZperpZperpZperpy₁   - o.DGP.invZperpZperpZperpZpar' * o.S✻Zperpy₁
		o.S✻Y₂y₁     = o.Repl.S✻Y₂y₁    - _S✻ZperpY₂'   * o.DGP.invZperpZperpZperpy₁   - o.DGP.invZperpZperpZperpY₂'   * o.S✻Zperpy₁
		o.S✻y₁y₁     = o.Repl.S✻y₁y₁    - _S✻Zperpy₁'   * o.DGP.invZperpZperpZperpy₁   - o.S✻Zperpy₁' * o.DGP.invZperpZperpZperpy₁
		o.DGP.restricted &&
			(o.S✻DGPZR₁y₁ = o.DGP.S✻ZR₁y₁ - _S✻ZperpDGPZR₁' * o.DGP.invZperpZperpZperpy₁ - o.DGP.invZperpZperpZperpZR₁' * o.S✻Zperpy₁)

		if o.Repl.restricted
			_S✻ZperpReplZR₁ = @panelsum(o, o.Repl.S✻⋂ZperpZR₁, o.info✻_✻⋂)
			_S✻⋂XReplZR₁    = @panelsum(o, o.Repl.S✻⋂XZR₁    , o.info✻_✻⋂)

			o.r₁S✻ReplZR₁Y₂     = o.r₁' * (o.Repl.S✻ZR₁Y₂ - _S✻ZperpReplZR₁' * o.Repl.invZperpZperpZperpY₂ - o.Repl.invZperpZperpZperpZR₁' * o.S✻ZperpY₂)
			o.r₁S✻ReplZR₁X      = o.r₁' * (_S✻⋂XReplZR₁'  - _S✻ZperpReplZR₁' * o.Repl.invZperpZperpZperpX  - o.Repl.invZperpZperpZperpZR₁' * o.S✻ZperpX )
			o.r₁S✻ReplZR₁DGPZ   = o.r₁' * panelcross(o.Repl.ZR₁, o.DGP.Z, o.info✻)
			o.r₁S✻ReplZR₁y₁     = o.r₁' * (o.Repl.S✻ZR₁y₁ - _S✻ZperpReplZR₁' * o.Repl.invZperpZperpZperpy₁ - o.Repl.invZperpZperpZperpZR₁' * o.S✻Zperpy₁)
			o.DGP.restricted &&
				(o.r₁S✻ReplZR₁DGPZR₁ = o.r₁' * panelcross(o.Repl.ZR₁, o.DGP.ZR₁, o.info✻))
		end

		_S✻ZperpReplZpar = @panelsum(o, o.Repl.S✻⋂ZperpZpar, o.info✻_✻⋂)
		_S✻ReplXZ        = @panelsum(o, o.Repl.S✻⋂XZpar    , o.info✻_✻⋂)

		o.S✻ReplZY₂      = o.Repl.S✻ZparY₂ - _S✻ZperpReplZpar' * o.Repl.invZperpZperpZperpY₂ - o.Repl.invZperpZperpZperpZpar' * o.S✻ZperpY₂
		o.S✻ReplZX       = _S✻ReplXZ'      - _S✻ZperpReplZpar' * o.Repl.invZperpZperpZperpX  - o.Repl.invZperpZperpZperpZpar' * o.S✻ZperpX
		o.S✻ReplZDGPZ    = panelcross(o.Repl.Z, o.DGP.Z, o.info✻)
		o.S✻ReplZy₁      = o.Repl.S✻Zpary₁ - _S✻ZperpReplZpar' * o.Repl.invZperpZperpZperpy₁ - o.Repl.invZperpZperpZperpZpar' * o.S✻Zperpy₁

		if o.DGP.restricted
			_S✻⋂XDGPZR₁ = @panelsum(o, o.DGP.S✻⋂XZR₁, o.info✻_✻⋂)

			o.S✻ReplZDGPZR₁  = panelcross(o.Repl.Z, o.DGP.ZR₁, o.info✻)
			o.S✻DGPZR₁Y₂     = o.DGP.S✻ZR₁Y₂  - _S✻ZperpDGPZR₁' * o.Repl.invZperpZperpZperpY₂  - o.DGP.invZperpZperpZperpZR₁' * o.S✻ZperpY₂
			o.S✻DGPZR₁DGPZR₁ = o.DGP.S✻ZR₁ZR₁ - _S✻ZperpDGPZR₁' * o.DGP.invZperpZperpZperpZR₁  - o.DGP.invZperpZperpZperpZR₁' * o.S✻ZperpDGPZR₁
			o.S✻DGPZR₁DGPZ   = o.DGP.S✻ZR₁Z   - _S✻ZperpDGPZR₁' * o.DGP.invZperpZperpZperpZpar - o.DGP.invZperpZperpZperpZR₁' * o.S✻ZperpDGPZ
			o.S✻DGPZR₁X      = _S✻⋂XDGPZR₁'   - _S✻ZperpDGPZR₁' * o.Repl.invZperpZperpZperpX   - o.DGP.invZperpZperpZperpZR₁' * o.S✻ZperpX
		end
	end

	o.invXXS✻XY₂   = @panelsum(o, o.invXXS✻⋂XY₂  , o.info✻_✻⋂)
	o.invXXS✻XX    = @panelsum(o, o.invXXS✻⋂XX   , o.info✻_✻⋂)
	o.invXXS✻XDGPZ = @panelsum(o, o.invXXS✻⋂XDGPZ, o.info✻_✻⋂)
	o.invXXS✻Xy₁   = dropdims(@panelsum(o, reshape(o.invXXS✻⋂Xy₁,Val(3)), o.info✻_✻⋂); dims=3)
	o.invZperpZperpS✻ZperpY₂   = o.Repl.invZperpZperp * o.S✻ZperpY₂
	o.invZperpZperpS✻ZperpX    = o.Repl.invZperpZperp * o.S✻ZperpX
	o.invZperpZperpS✻Zperpy₁   = o.Repl.invZperpZperp * o.S✻Zperpy₁
	o.invZperpZperpS✻ZperpDGPZ = o.Repl.invZperpZperp * o.S✻ZperpDGPZ

	if o.DGP.restricted
		o.invXXS✻XDGPZR₁ = @panelsum(o, o.invXXS✻⋂XDGPZR₁, o.info✻_✻⋂)
		o.invZperpZperpS✻ZperpDGPZR₁ = o.Repl.invZperpZperp * o.S✻ZperpDGPZR₁
	end
end

function PrepWRE!(o::StrBootTest{T}) where T
	r₁ = o.null ? [o.r₁ ; o.r] : o.r₁
  EstimateIV!(o.DGP, o, r₁)
  MakeResidualsIV!(o.DGP, o)
  o.robust && o.bootstrapt && o.granular && (Ü₂par = view(o.DGP.Ü₂ * o.Repl.RparY,:,:))

	o.S✻⋂XU₂ .= o.S✻⋂XY₂ - o.S✻⋂XX * o.DGP.Π̂
	o.S✻⋂XU₂RparY .= o.S✻⋂XU₂ * o.Repl.RparY
	o.S✻XU₂ .= o.S✻XY₂ - o.S✻XX * o.DGP.Π̂
	o.S✻XU₂RparY .= o.S✻XU₂ * o.Repl.RparY
	o.S✻ZperpU₂ .= o.S✻ZperpY₂ - o.S✻ZperpX * o.DGP.Π̂
	o.S✻ZperpU₂RparY .= o.S✻ZperpU₂ * o.Repl.RparY
	o.invZperpZperpS✻ZperpU₂ .= o.invZperpZperpS✻ZperpY₂ - o.invZperpZperpS✻ZperpX * o.DGP.Π̂
	o.invZperpZperpS✻ZperpU₂RparY .= o.invZperpZperpS✻ZperpU₂ * o.Repl.RparY
	o.invXXS✻XU₂ .= o.Repl.invXX * o.S✻XU₂
	o.invXXS✻XU₂RparY .= o.invXXS✻XU₂ * o.Repl.RparY

	if o.liml || !o.robust || !isone(o.κ)
		S✻U₂y₁ = o.S✻Y₂y₁ - o.DGP.Π̂' * o.S✻Xy₁
		S✻U₂RparYy₁ = o.Repl.RparY' * S✻U₂y₁
		S✻ZU₂ = o.S✻ReplZY₂ - o.S✻ReplZX * o.DGP.Π̂
		S✻ZU₂RparY = S✻ZU₂ * o.Repl.RparY
		Π̂S✻XÜ₂γ̈ = o.DGP.Π̂' * o.S✻XU₂ * o.DGP.γ̈
		S✻Ü₂Y₂ = o.S✻Y₂Y₂ - o.DGP.Π̂' * o.S✻XY₂
		S✻Y₂Ü₂γ̈ = S✻Ü₂Y₂' * o.DGP.γ̈
		S✻Ü₂parÜ₂par = o.Repl.RparY' * (S✻Ü₂Y₂ - o.S✻XU₂' * o.DGP.Π̂) * o.Repl.RparY
		S✻UUterm = o.S✻Y₂y₁ - o.S✻DGPZY₂' * o.DGP.β̈ - o.DGP.Π̂' * (o.S✻Xy₁  - o.S✻XDGPZ * o.DGP.β̈)

		if o.Repl.restricted
			S✻ReplZR₁r₁U₂ = o.r₁S✻ReplZR₁Y₂ - o.r₁S✻ReplZR₁X * o.DGP.Π̂
			S✻ReplZR₁r₁U₂RparY = S✻ReplZR₁r₁U₂ * o.Repl.RparY
		end
		if o.DGP.restricted
			r₁S✻DGPZR₁y₁ = r₁' * o.S✻DGPZR₁y₁
			S✻ReplZDGPZR₁r₁ = o.S✻ReplZDGPZR₁ * r₁
			o.Repl.restricted && (S✻ReplZR₁r₁DGPZR₁r₁ = o.r₁S✻ReplZR₁DGPZR₁ * r₁)
		end
	end

	if (o.liml || o.bootstrapt || !isone(o.κ)) && o.NFE>0
		CT✻FEU = o.CT✻FEY₂ - o.CT✻FEX * o.DGP.Π̂
		CT✻FEURparY = CT✻FEU * o.Repl.RparY
	end

  @inbounds for i ∈ 0:o.Repl.kZ  # precompute various clusterwise sums
		# S_✻(u .* X), S_✻(u .* Zperp) for residuals u for each endog var; store transposed
		if iszero(i)
			o.S✻XU[1]      .= o.S✻Xy₁      - o.S✻XDGPZ      * o.DGP.β̈ + o.S✻XU₂      * o.DGP.γ̈
			o.invXXS✻XU[1] .= o.invXXS✻Xy₁ - o.invXXS✻XDGPZ * o.DGP.β̈ + o.invXXS✻XU₂ * o.DGP.γ̈
			if o.DGP.restricted
				o.S✻XU[1]      .-= o.S✻XZR₁         * r₁
				o.invXXS✻XU[1] .-= o.invXXS✻XDGPZR₁ * r₁
			end
		else
			o.S✻XU[i+1]      .= view(o.S✻XU₂RparY,:,:,i)
			o.invXXS✻XU[i+1] .= view(o.invXXS✻XU₂RparY,:,:,i)
		end

		if o.liml || !isone(o.κ) || o.bootstrapt
			if iszero(i)
				o.S✻ZperpU[1]              .= o.S✻Zperpy₁              - o.S✻ZperpDGPZ              * o.DGP.β̈ + o.S✻ZperpU₂              * o.DGP.γ̈
				o.invZperpZperpS✻ZperpU[1] .= o.invZperpZperpS✻Zperpy₁ - o.invZperpZperpS✻ZperpDGPZ * o.DGP.β̈ + o.invZperpZperpS✻ZperpU₂ * o.DGP.γ̈
				if o.DGP.restricted
					o.S✻ZperpU[1]              .-= o.S✻ZperpDGPZR₁              * r₁
					o.invZperpZperpS✻ZperpU[1] .-= o.invZperpZperpS✻ZperpDGPZR₁ * r₁
				end
			else
				o.S✻ZperpU[i+1]              .= view(o.S✻ZperpU₂RparY,:,:,i)
				o.invZperpZperpS✻ZperpU[i+1] .= view(o.invZperpZperpS✻ZperpU₂RparY,:,:,i)
			end

			if o.NFE>0
				if iszero(i)
					o.CTFEU[1] = o.CT✻FEy₁ - o.CT✻FEZ * o.DGP.β̈ + CT✻FEU * o.DGP.γ̈
					o.DGP.restricted &&
						(o.CTFEU[1] .-= o.CT✻FEZR₁ * r₁)
				else
					o.CTFEU[i+1] = @view CT✻FEURparY[:,:,i]
				end
			end
		end

		if o.liml || !isone(o.κ) || !o.robust
			if iszero(i)  # panelsum2(o, o.Repl.y₁par, o.Repl.Z, uwt, o.info✻)
				o.S✻YU[1,1] .= o.S✻y₁y₁ - o.S✻DGPZy₁'o.DGP.β̈ + S✻U₂y₁'o.DGP.γ̈
				o.DGP.restricted &&
					(o.S✻YU[1,1] .-= view(r₁S✻DGPZR₁y₁, 1,:))

				if o.Repl.restricted
					o.S✻YU[1,1] .-= view(o.r₁S✻ReplZR₁y₁ - o.r₁S✻ReplZR₁DGPZ * o.DGP.β̈ + S✻ReplZR₁r₁U₂ * o.DGP.γ̈, 1,:,1)
					o.DGP.restricted &&
						(o.S✻YU[1,1] .+= view(S✻ReplZR₁r₁DGPZR₁r₁, 1,:,1))
				end

				for j ∈ 1:o.Repl.kZ
					o.S✻YU[j+1,1] .= view(o.S✻ReplZy₁ - o.S✻ReplZDGPZ * o.DGP.β̈ + S✻ZU₂ * o.DGP.γ̈, j,:,1)
					o.DGP.restricted &&
						(o.S✻YU[j+1,1] .-= view(S✻ReplZDGPZR₁r₁, j,:,1))
				end
      else
				o.S✻YU[1,i+1] .= @view S✻U₂RparYy₁[i,:]
				o.Repl.restricted &&
					(o.S✻YU[1,i+1] .-= @view S✻ReplZR₁r₁U₂RparY[1,:,i])
				for j ∈ 1:o.Repl.kZ
					o.S✻YU[j+1,i+1] .= @view S✻ZU₂RparY[j,:,i]
				end
			end

			if iszero(i)
		 		o.S✻UU[1,i+1] .= o.S✻y₁y₁ - (2 * o.S✻DGPZy₁ - o.S✻DGPZDGPZ * o.DGP.β̈)'o.DGP.β̈ + (2 * S✻UUterm - Π̂S✻XÜ₂γ̈ + S✻Y₂Ü₂γ̈)'o.DGP.γ̈
				o.DGP.restricted
					(o.S✻UU[1,i+1] .+= dropdims(-2 * r₁S✻DGPZR₁y₁ + r₁' * (o.S✻DGPZR₁DGPZR₁ * r₁) + 2 * r₁' * (o.S✻DGPZR₁DGPZ * o.DGP.β̈ + (o.S✻DGPZR₁X * o.DGP.Π̂ - o.S✻DGPZR₁Y₂) * o.DGP.γ̈); dims=1))  # XXX dropdims vs view...
			else
				o.S✻UU[1,i+1] .= view(o.Repl.RparY' * (S✻UUterm + S✻Y₂Ü₂γ̈ - Π̂S✻XÜ₂γ̈), i,:)

				if o.DGP.restricted
					o.S✻UU[1,i+1] .-= view(r₁' * (o.S✻DGPZR₁Y₂ - o.S✻DGPZR₁X * o.DGP.Π̂) * o.Repl.RparY, 1,:,i)
				end
			end
			for j ∈ 1:i
				o.S✻UU[j+1,i+1] .= view(S✻Ü₂parÜ₂par, j,:,i)
			end
		end

		if o.robust && o.bootstrapt && o.granular
			i>0 && (o.S✻UPX[i+1] .= o.XinvXX * o.S✻XU[i+1])

			o.S✻UMZperp[i+1] .= o.Repl.Zperp * o.invZperpZperpS✻ZperpU[i+1]
			if iszero(i)  # subtract crosstab of observation by ∩-group of u
				o.S✻UMZperp[   1][o.crosstab✻ind] .-= o.DGP.u⃛₁
			else
				o.S✻UMZperp[i+1][o.crosstab✻ind] .-= view(Ü₂par,:,i)
			end

			o.NFE>0 &&
				(o.S✻UMZperp[i+1] .-= view(o.invFEwt .* o.CTFEU[i+1], o._FEID, :))  # CT_(*,FE) (U ̈_(parj) ) (S_FE S_FE^' )^(-1) S_FE
		end
  end

	if o.robust && o.bootstrapt && iszero(o.granular)
		@inbounds for j ∈ 0:o.Repl.kZ
			if o.Repl.Yendog[j+1]
				o.negS✻UMZperpX[j+1] = o.S⋂XZperpinvZperpZperp * o.S✻ZperpU[j+1]  # S_* diag(U ̈_(∥j) ) Z_⊥ (Z_⊥^' Z_⊥ )^(-1) Z_(⊥g)^' X_(∥g)
				if iszero(j)  # - S_*  diag(U ̈_(∥j) ) I_g^' X_(∥g)
					o.negS✻UMZperpX[j+1][o.crosstab⋂✻ind] .-= vec(o.S✻⋂Xy₁ - o.S✻⋂XDGPZ * o.DGP.β̈ + o.S✻⋂XU₂ * o.DGP.γ̈)
					o.DGP.restricted &&
						(o.negS✻UMZperpX[j+1][o.crosstab⋂✻ind] .+= vec(o.S✻⋂X_DGPZR₁ * r₁))
				else
					o.negS✻UMZperpX[j+1][o.crosstab⋂✻ind] .-= vec(view(o.S✻⋂XU₂RparY,:,:,j))
				end
				o.NFE>0 &&
					(o.negS✻UMZperpX[j+1] .+= o.CT⋂FEX'o.CTFEU[j+1])  # CT_(*,FE) (U ̈_(∥j) ) (S_FE S_FE^' )^(-1) S_FE
			end
		end
	end
	nothing
end

# For WRE, and with reference to Y = [y₁ Z], given 0-based columns indexes within it, i, j, return all bootstrap realizations of
# Y[:,i]'((1-κ)*M_Zperp-κ*M_Xpar)*Y[:,j] for κ constant across replications
# i can be a rowvector
# (only really the Hessian when we narrow Y to Z)
function HessianFixedkappa(o::StrBootTest{T}, is::Vector{S} where S<:Integer, j::Integer, κ::Number, w::Integer) where T
  dest = Matrix{T}(undef, length(is), ncols(o.v))
  @inbounds for i ∈ eachindex(is, axes(dest,1))
		_HessianFixedkappa!(o, view(dest,i:i,:), is[i], j, κ, w)
  end
  dest
end

function _HessianFixedkappa!(o::StrBootTest, dest::AbstractMatrix, i::Integer, j::Integer, κ::Number, w::Integer)
  if !(o.Repl.Yendog[i+1] || o.Repl.Yendog[j+1])  # if both vars exog, result = order-0 term only, same for all draws
		!iszero(κ) &&
			(dest .= dot(view(o.Repl.XZ,:,i), view(o.Repl.invXXXZ,:,j)))
		if !isone(κ)
			if iszero(κ)
				fill!(dest, o.Repl.YY[i+1,j+1])
			else
				dest .= κ .* dest .+ (1 - κ) .* o.Repl.YY[i+1,j+1]
			end
		end
	else
		if !iszero(κ)  # repititiveness in this section to maintain type stability
			if o.Repl.Yendog[i+1]
				T1L = o.T1L[isone(o.Nw) || w<o.Nw ? 1 : 2]  # preallocated destinations
				mul!(T1L, o.S✻XU[i+1], o.v)
				if iszero(i)
					T1L .+= o.Repl.Xy₁par
				else
					T1L .+= view(o.Repl.XZ,:,i)
				end
				if o.Repl.Yendog[j+1]
					T1R = o.T1R[isone(o.Nw) || w<o.Nw ? 1 : 2]  # preallocated destinations
					mul!(T1R, o.invXXS✻XU[j+1], o.v)
					if iszero(j)
						T1R .+=  o.Repl.invXXXy₁par
					else
						T1R .+= view(o.Repl.invXXXZ,:,j)
					end
					coldot!(o, dest, T1L, T1R)
				else
					dest .= view(o.Repl.invXXXZ,:,j)'T1L  # coldot!(o, dest, T1L, view(o.Repl.invXXXZ,:,j))
				end
			else
				if o.Repl.Yendog[j+1]
					T1R = o.T1R[isone(o.Nw) || w<o.Nw ? 1 : 2]  # use preallocated destinations
					mul!(T1R, o.invXXS✻XU[j+1], o.v)
					if iszero(j)
						T1R .+=  o.Repl.invXXXy₁par
					else
						T1R .+= view(o.Repl.invXXXZ,:,j)
					end
					dest .= view(o.Repl.XZ,:,i)'T1R
				else
					dest .= dot(view(o.Repl.XZ,:,i), view(o.Repl.invXXXZ,:,j))
				end
			end
		end
		if !isone(κ)
			if o.Repl.Yendog[i+1]
				o.T2 .= o.invZperpZperpS✻ZperpU[i+1]'o.S✻ZperpU[j+1]  # quadratic term
				o.T2[diagind(o.T2)] .-= i ≤ j ? o.S✻UU[i+1, j+1] : o.S✻UU[j+1, i+1]  # minus diagonal crosstab
				o.NFE>0 &&
					(o.T2 .+= o.CTFEU[i+1]' * (o.invFEwt .* o.CTFEU[j+1]))
				if iszero(κ)
					dest .= o.Repl.YY[i+1,j+1] .+ colquadformminus!(o, (                            o.S✻YU[i+1,j+1] .+ o.S✻YU[j+1,i+1])'o.v, o.T2, o.v)
				else
					dest .= κ .* dest .+ (1 - κ)     .* colquadformminus!(o, (o.Repl.YY[i+1,j+1] .+ o.S✻YU[i+1,j+1] .+ o.S✻YU[j+1,i+1])'o.v, o.T2, o.v)
				end
			elseif iszero(κ)
				dest .= o.Repl.YY[i+1,j+1]
			else
				dest .= κ .* dest .+ (1 - κ) .* o.Repl.YY[i+1,j+1]
			end
		end
  end
	nothing
end

# put threaded loops in functions to prevent type instability https://discourse.julialang.org/t/type-inference-with-threads/2004/3
function FillingLoop1!(o::StrBootTest{T}, dest::Matrix{T}, i::Integer, j::Integer, _β̈::AbstractMatrix{T}, β̈v::AbstractMatrix{T}) where T
	Threads.@threads for g ∈ 1:o.N⋂
		PXY✻ = [o.PXZ[g,i]]
		o.Repl.Yendog[i+1] && (PXY✻ = PXY✻ .+ view(o.S✻UPX[i+1],g,:)'o.v)

		if iszero(j)
			dest[g,:]   = dropdims(colsum(PXY✻ .* (o.Repl.y₁[g] .- view(o.S✻UMZperp[1],g,:)'o.v)); dims=1)
		elseif o.Repl.Yendog[j+1]
			dest[g,:] .-= dropdims(colsum(PXY✻ .* (o.Repl.Z[g,j] * _β̈ .- view(o.S✻UMZperp[j+1],g,:)'β̈v)); dims=1)
		else
			dest[g,:] .-= dropdims(colsum(PXY✻ .* (o.Repl.Z[g,j] * _β̈)); dims=1)
		end
	end
	nothing
end
function FillingLoop2!(o::StrBootTest{T}, dest::Matrix{T}, i::Integer, j::Integer, _β̈::AbstractMatrix{T}, β̈v::AbstractMatrix{T}) where T
	Threads.@threads for g ∈ 1:o.N⋂
		S = o.info⋂[g]
		PXY✻ = o.Repl.Yendog[i+1] ? view(o.PXZ,S,i) .+ view(o.S✻UPX[i+1],S,:) * o.v :
												 reshape(view(o.PXZ,S,i), :, 1)

		if iszero(j)
			dest[i,:]   = dropdims(colsum(PXY✻ .* (o.Repl.y₁[S] .- view(o.S✻UMZperp[1],S,:) * o.v)); dims=1)
		else
			dest[i,:] .-= dropdims(colsum(PXY✻ .* (o.Repl.Yendog[j+1] ? o.Repl.Z[S,j] * _β̈ .- view(o.S✻UMZperp[j+1],S,:) * β̈v :
																														       o.Repl.Z[S,j] * _β̈                                       )); dims=1)
		end
	end
	nothing
end

# Workhorse for WRE CRVE sandwich filling
# Given a zero-indexed column index, i>0, and a matrix β̈s of all the bootstrap estimates,
# return all bootstrap realizations of P_X * Z[:,i]_g ' û₁g^*b
# for all groups in the intersection of all error clusterings
# return value has one row per ⋂ cluster, one col per bootstrap replication
# that is, given i, β̈s = δ ̂_CRκ^(*), return, over all g, b (P_(X_∥ g) Z_(∥i)^(*b) )^' (M_(Z_⊥ ) y_(1∥)^(*b) )_g-(P_(X_∥ g) Z_(∥i)^(*b) )^' (M_(Z_⊥ ) Z_∥^(*b) )_g δ ̂_CRκ^(*b)
function Filling!(o::StrBootTest{T}, dest::AbstractMatrix{T}, i::Int64, β̈s::AbstractMatrix) where T
	if o.granular
		β̈v = _β̈ = Matrix{T}(undef,0,0)
   	if o.Nw == 1  # create or avoid NxB matrix?
			PXY✻ = reshape(view(o.PXZ,:,i), :, 1)
			o.Repl.Yendog[i+1] && (PXY✻ = PXY✻ .+ o.S✻UPX[i+1] * o.v)
			dest .= @panelsum(o, PXY✻ .* (o.Repl.y₁ .- o.S✻UMZperp[1] * o.v), o.info⋂)
			@inbounds for j ∈ 1:o.Repl.kZ
				_β̈ = view(β̈s,j,:)'
				dest .-= @panelsum(o, PXY✻ .* (o.Repl.Yendog[j+1] ? view(o.Repl.Z,:,j) * _β̈ .- o.S✻UMZperp[j+1] * (o.v .* _β̈) :
															                                 (view(o.Repl.Z,:,j) * _β̈)                                      ), o.info⋂)
			end
		else  # create pieces of each N x B matrix one at a time rather than whole thing at once
			@inbounds for j ∈ 0:o.Repl.kZ
				j>0 && (β̈v = o.v .* (_β̈ = view(β̈s,j,:)'))
				(o.purerobust ? FillingLoop1! : FillingLoop2!)(o, dest, i, j, _β̈, β̈v)
			end
    end
  else  # coarse error clustering
		# (P_(X_∥ g) Z_∥^* )^' (M_(Z_⊥ ) y_(1∥)^* )_g
		F1₀ = view(o.Repl.invXXXZ,:,i)
		F1₁ = o.invXXS✻XU[i+1]
		F2₀ = o.S⋂Xy₁
		F2₁ = o.negS✻UMZperpX[1]
		if o.Repl.Yendog[i+1]  # add terms that are zero only if Zpar[i] is exogenous, i.e. if a null refers only to exogenous variables
			dest .= reshape(F1₀'F2₀,:,1) .- (dropdims(F1₀'F2₁; dims=1) - F2₀'F1₁) * o.v  # 0th- & 1st-order terms
			o.Q .= F1₁'F2₁
			@inbounds for g ∈ 1:o.N⋂
				o.colquadformminus!(dest, g, o.v, o.Q[:,g,:], o.v)
			end
		else
			dest .= F2₀'F1₀ .- dropdims(F1₀'F2₁; dims=1) * o.v  # 0th- & 1st-order terms
		end

		# -(P_(X_∥ g) Z_∥^* )^' (M_(Z_⊥ ) Z_∥^(*b) )_g δ ̂_CRκ^*
		@inbounds for j ∈ 1:o.Repl.kZ
			F2₀ = view(o.S⋂ReplZX,j,:,:)'
			β̈v = o.β̈v[isone(o.Nw) || w<o.Nw ? 1 : 2]; β̈v .= o.v .* (_β̈ = -view(β̈s,j,:)')
			if o.Repl.Yendog[j+1]
				F2₁ = o.negS✻UMZperpX[j+1]
				dest .+= F2₀'F1₀ .* _β̈ .- (dropdims(F1₀'F2₁; dims=1) - F2₀'F1₁) * β̈v  # "-" because S✻UMZperpX is stored negated as F2₁=negS✻UMZperpX[j+1]
				o.Q .= F1₁'F2₁
				for g ∈ 1:o.N⋂
					o.colquadformminus!(dest, g, o.v, o.Q[:,g,:], β̈v)
				end
			elseif o.Repl.Yendog[i+1]
				dest .+= reshape(F1₀'F2₀,:,1) .* _β̈  .+ F2₀'F1₁ * β̈v
			else
				dest .+= reshape(F1₀'F2₀,:,1) .* _β̈
			end
		end
  end
  nothing
end

function MakeWREStats!(o::StrBootTest{T}, w::Integer) where T
  if isone(o.Repl.kZ)  # optimized code for 1 coefficient in bootstrap regression
		if o.liml
			YY₁₁   = HessianFixedkappa(o, [0], 0, zero(T), w)  # κ=0 => Y*MZperp*Y
			YY₁₂   = HessianFixedkappa(o, [0], 1, zero(T), w)
			YY₂₂   = HessianFixedkappa(o, [1], 1, zero(T), w)
			YPXY₁₁ = HessianFixedkappa(o, [0], 0, one(T) , w)  # κ=1 => Y*PXpar*Y
			YPXY₁₂ = HessianFixedkappa(o, [0], 1, one(T) , w)
			YPXY₂₂ = HessianFixedkappa(o, [1], 1, one(T) , w)
			YY₁₂YPXY₁₂ = YY₁₂ .* YPXY₁₂
			x₁₁ = YY₂₂ .* YPXY₁₁ .- YY₁₂YPXY₁₂      # elements of YY✻^-1 * YPXY✻ up to factor of det(YY✻)
			x₁₂ = YY₂₂ .* YPXY₁₂ .- YY₁₂ .* YPXY₂₂
			x₂₁ = YY₁₁ .* YPXY₁₂ .- YY₁₂ .* YPXY₁₁
			x₂₂ = YY₁₁ .* YPXY₂₂ .- YY₁₂YPXY₁₂
			κs = (x₁₁ .+ x₂₂)./2; κs .= 1 ./ (1 .- (κs .- sqrtNaN.(κs.^2 .- x₁₁ .* x₂₂ .+ x₁₂ .* x₂₁)) ./ (YY₁₁ .* YY₂₂ .- YY₁₂ .* YY₁₂))  # solve quadratic equation for smaller eignenvalue; last term is det(YY✻)
			!iszero(o.fuller) && (κs .-= o.fuller / (o._Nobs - o.kX))
			o.As = κs .* (YPXY₂₂ .- YY₂₂) .+ YY₂₂
			o.β̈s = (κs .* (YPXY₁₂ .- YY₁₂) .+ YY₁₂) ./ o.As
		else
			o.As = HessianFixedkappa(o, [1], 1, o.κ, w)
			o.β̈s = HessianFixedkappa(o, [1], 0, o.κ, w) ./ o.As
		end

		if o.null
			o.numerw = o.β̈s .+ (o.Repl.Rt₁ - o.r) / o.Repl.RRpar
		else
			o.numerw = o.β̈s .- o.DGP.β̈
			isone(w) && (o.numerw[1] = o.β̈s[1] + (o.Repl.Rt₁[1] - o.r[1]) / o.Repl.RRpar[1])
		end

		@storeWtGrpResults!(o.numer, o.numerw)
		if o.bootstrapt
			if o.robust
				J⋂s1 = dropdims(o.J⋂s[isone(o.Nw) || w<o.Nw ? 1 : 2]; dims=3)
				Filling!(o, J⋂s1, 1, o.β̈s);
				J⋂s1 ./= o.As
				@inbounds for c ∈ 1:o.NErrClustCombs  # sum sandwich over error clusterings
					nrows(o.clust[c].order)>0 &&
						(J⋂s1 .= J⋂s1[o.clust[c].order,:])
					@clustAccum!(denom, c, coldot(o, @panelsum(o, J⋂s1, o.clust[c].info)))
				end
			else
				denom = (HessianFixedkappa(o, [0], 0, zero(T), w) .- 2 .* o.β̈s .* HessianFixedkappa(o, [0], 1, zero(T), w) .+ o.β̈s.^2 .* HessianFixedkappa(o, [1], 1, zero(T), w)) ./ o._Nobs ./ o.As  # classical error variance
			end
			@storeWtGrpResults!(o.dist, o.sqrt ? o.numerw ./ sqrtNaN.(denom) : o.numerw .^ 2 ./ denom)
			denom *= o.Repl.RRpar[1]^2
		end
		w==1 && o.bootstrapt && (o.statDenom = fill(denom[1],1,1))  # original-sample denominator

  else  # WRE bootstrap for more than 1 retained coefficient in bootstrap regression

		β̈s = zeros(T, o.Repl.kZ, ncols(o.v))
		A = Vector{Matrix{T}}(undef, ncols(o.v))

		if o.liml
			YY✻   = [HessianFixedkappa(o, collect(0:i), i, zero(T), w) for i ∈ 0:o.Repl.kZ] # κ=0 => Y*MZperp*Y
			YPXY✻ = [HessianFixedkappa(o, collect(0:i), i,  one(T), w) for i ∈ 0:o.Repl.kZ] # κ=1 => Y*PXpar*Y

			@inbounds for b ∈ axes(o.v,2)
				for i ∈ 0:o.Repl.kZ
					o.YY✻_b[1:i+1,i+1]   = YY✻[i+1][:,b]  # fill uppper triangles, which is all that invsym() looks at
					o.YPXY✻_b[1:i+1,i+1] = YPXY✻[i+1][:,b]
				end
				o.κ = 1/(1 - real(eigvals(invsym(o.YY✻_b) * Symmetric(o.YPXY✻_b))[1]))
				!iszero(o.fuller) && (o.κ -= o.fuller / (o._Nobs - o.kX))
				β̈s[:,b] = (A[b] = invsym(o.κ*o.YPXY✻_b[2:end,2:end] + (1-o.κ)*o.YY✻_b[2:end,2:end])) * (o.κ*o.YPXY✻_b[1,2:end] + (1-o.κ)*o.YY✻_b[1,2:end])
			end
		else
			δnumer =  HessianFixedkappa(o, collect(1:o.Repl.kZ), 0, o.κ, w)
			δdenom = [HessianFixedkappa(o, collect(1:i), i, o.κ, w) for i ∈ 1:o.Repl.kZ]

			nt = Threads.nthreads()
  		cs = [round(Int, size(o.v,2)/nt*i) for i ∈ 0:nt]
			@inbounds Threads.@threads for t ∈ 1:nt
				δdenom_b = zeros(T, o.Repl.kZ, o.Repl.kZ)  # thread-safe scratch pad
				@inbounds for b ∈ cs[t]+1:cs[t+1]
					for i ∈ 1:o.Repl.kZ
						δdenom_b[1:i,i] = view(δdenom[i],:,b)  # fill uppper triangle
					end
					β̈s[:,b] = (A[b] = invsym(δdenom_b)) * view(δnumer,:,b)
				end
			end
		end

		if o.bootstrapt
			if o.robust
				J⋂s = o.J⋂s[isone(o.Nw) || w<o.Nw ? 1 : 2]
				@inbounds for i ∈ 1:o.Repl.kZ  # avoid list comprehension construction for compiler-perceived type stability
					Filling!(o, view(J⋂s,:,:,i), i, β̈s)
				end
			else
				YY✻ = [HessianFixedkappa(o, collect(i:o.Repl.kZ), i, zero(T), w) for i ∈ 0:o.Repl.kZ]  # κ=0 => Y*MZperp*Y
			end
		end

		@inbounds for b ∈ reverse(axes(o.v,2))
			o.numer_b .= o.null || w==1 && b==1 ? o.Repl.RRpar * view(β̈s,:,b) + o.Repl.Rt₁ - o.r : o.Repl.RRpar * (view(β̈s,:,b) - o.DGP.β̈₀)
			if o.bootstrapt
				if o.robust  # Compute denominator for this WRE test stat
					J⋂ = view(J⋂s,:,b,:) * (A[b] * o.Repl.RRpar')
					for c ∈ 1:o.NErrClustCombs
						(!isone(o.NClustVar) && nrows(o.clust[c].order)>0) &&
							(J⋂ = J⋂[o.clust[c].order,:])
						J_b = @panelsum(o, J⋂, o.clust[c].info)
						@clustAccum!(denom, c, J_b'J_b)
					end
				else  # non-robust
					for i ∈ 0:o.Repl.kZ
						o.YY✻_b[i+1,i+1:o.Repl.kZ+1] = view(YY✻[i+1],:,b)  # fill upper triangle
					end
					denom = (o.Repl.RRpar * A[b] * o.Repl.RRpar') * [-one(T) ; β̈s[:,b]]'Symmetric(o.YY✻_b) * [-one(T) ; β̈s[:,b]] / o._Nobs  # 2nd half is sig2 of errors
				end
				o.dist[b+first(o.WeightGrp[w])-1] = o.sqrt ? o.numer_b[1] / sqrtNaN(denom[1]) : o.numer_b'invsym(denom)*o.numer_b  # hand-code for 2-dimensional?
			end
			o.numer[:,b+first(o.WeightGrp[w])-1] = o.numer_b  # slight inefficiency: in usual bootstrap-t case, only need to save numerators in numer if getdist("numer") is coming because of svmat(numer)
		end
		w==1 && o.bootstrapt && (o.statDenom = denom)  # original-sample denominator
	end
	nothing
end


###############################################################################
#
# Update carbon and water fluxes
#
# TODO: work more to make this function more customized
#
###############################################################################
"""
    layer_fluxes!(
                node::SPACMono{FT};
                updating::Bool = false
    ) where {FT<:AbstractFloat}

Run carbon, water, energy, and SIF fluxes for all canopy layers, given
- `node` [`SPACMono`](@ref) type struct
- `updating` If true, update cavitation history
"""
function layer_fluxes!(
            node::SPACMono{FT};
            updating::Bool = false
) where {FT<:AbstractFloat}
    # 0.1 unpack data
    @unpack angles, envirs, f_SL, ga, in_rad, leaves_rt, n_canopy, plant_ps, rt_con, rt_dim, soil_opt, stomata_model, wl_set = node;
    canopy_rt = node.canopy_rt;
    can_rad = node.can_rad;
    can_opt = node.can_opt;
    plant_hs = node.plant_hs;
    nSL = canopy_rt.nAzi * canopy_rt.nIncl;

    canopy_geometry!(canopy_rt, angles, can_opt, rt_con);
    canopy_matrices!(leaves_rt, can_opt);
    short_wave!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, rt_con);
    canopy_fluxes!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, leaves_rt,
                   wl_set, rt_con);

    #
    f_H₂O = 0;
    f_GPP = 0;
    f_NPP = 0;
    for i_can in 1:n_canopy
        iEN = envirs[i_can];
        iHS = plant_hs.leaves[i_can];
        iPS = plant_ps[i_can];
        iRT = n_canopy + 1 - i_can;

        iPS.T = can_rad.T_sun[i_can];
        update_leaf_TP!(iPS, iHS, iEN);
        temperature_effects!(iHS, iPS.T);

        # calculate the fraction of sunlit and shaded leaves
        f_view = (can_opt.Ps[iRT] + can_opt.Ps[iRT+1]) / 2;
        for iLF in 1:nSL
            iPS.APAR[iLF] = can_rad.absPAR_sunCab[(iRT-1)*nSL+iLF] * FT(1e6);
            iPS.LAIx[iLF] = f_view * f_SL[iLF];
        end
        iPS.APAR[end] = can_rad.absPAR_shadeCab[iRT] * FT(1e6);
        iPS.LAIx[end] = 1 - f_view;

        # iterate for N times until sum(iPS.Ag) does not change any more
        sum_ag_last = sum(iPS.Ag);
        count       = 0;
        while true
            # calculate the photosynthetic rates
            count += 1;
            gas_exchange!(iPS, iEN, GswDrive());
            if typeof(stomata_model) <: EmpiricalStomatalModel
                #
                #
                #
                #
                # also need to determine which β function is used
                #     to tune g1
                #     to tune Vcmax
                #
                #
                #
                #
                prognostic_gsw!(iPS, iEN, stomata_model, FT(1), FT(120));
            else
                prognostic_gsw!(iPS, iHS, iEN, stomata_model, FT(120));
            end

            # update flow and pressure profile (except for history)
            # TODO move this part to PlantHydraulics.jl
            gsw_control!(iPS, iEN);
            for i_can in 1:n_canopy
                iEN = envirs[i_can];
                iLF = plant_hs.leaves[i_can];
                iPS = plant_ps[i_can];
                iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
            end
            flow_profile!(plant_hs);
            pressure_profile!(plant_hs, SteadyStateMode(); update=false);

            # TODO make sure this is done in future SPAC module
            # update canopy layer p_ups, which will be passed to each leaf
            for _i_can in 1:n_canopy
                _iHS = plant_hs.leaves[_i_can];
                _iPS = plant_ps[_i_can];
                _iPS.p_ups = _iHS.p_ups;
            end;

            sum_ag_curr = sum(iPS.Ag);
            if (abs(sum_ag_curr - sum_ag_last) < 1e-4)
                break
            elseif count > 100000
                @info tinfo("layer_fluxes!: iterations exceed 100000 times");
                break
            else
                sum_ag_last = sum_ag_curr;
            end
        end

        # update the fluorescence quantum yield from leaf level calculation
        can_rad.ϕ_sun[:,:,iRT] .= reshape(view(iPS.φs,1:nSL), canopy_rt.nIncl,
                                          canopy_rt.nAzi);
        can_rad.ϕ_shade[iRT] = iPS.φs[end];

        # update the flow rates
        for iLF in 1:(nSL+1)
            f_GPP += iPS.Ag[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_NPP += iPS.An[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_H₂O += iPS.g_lw[iLF] * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR * iPS.LAIx[iLF] * iPS.LA;
        end
    end

    # do SIF simulation
    SIF_fluxes!(leaves_rt, can_opt, can_rad, canopy_rt, soil_opt, wl_set,
                rt_con, rt_dim);

    # update flow profile and pressure history along the tree
    if updating
        for i_can in 1:n_canopy
            iEN = envirs[i_can];
            iLF = plant_hs.leaves[i_can];
            iPS = plant_ps[i_can];
            iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
        end
        flow_profile!(plant_hs);
        pressure_profile!(plant_hs, SteadyStateMode(); update=true);
    end

    # update the flows in SPACMono
    node.f_gpp = f_GPP / ga;
    node.f_npp = f_NPP / ga;
    node.f_H₂O = f_H₂O / ga;

    return nothing
end

"""
    layer_fluxes!(
                node::SPACMono{FT},
                Δt::FT;
                updating::Bool = false
    ) where {FT<:AbstractFloat}

Run carbon, water, energy, and SIF fluxes for all canopy layers, given
- `node` [`SPACMono`](@ref) type struct
- `Δt` Time step to forward in time
- `updating` If true, update cavitation history
"""
function layer_fluxes!(
            node::SPACMono{FT},
            Δt::FT;
            updating::Bool = false
) where {FT<:AbstractFloat}
    # 0.1 unpack data
    @unpack angles, envirs, f_SL, ga, in_rad, leaves_rt, n_canopy, plant_ps, rt_con, rt_dim, soil_opt, stomata_model, wl_set = node;
    canopy_rt = node.canopy_rt;
    can_rad = node.can_rad;
    can_opt = node.can_opt;
    plant_hs = node.plant_hs;
    nSL = canopy_rt.nAzi * canopy_rt.nIncl;

    canopy_geometry!(canopy_rt, angles, can_opt, rt_con);
    canopy_matrices!(leaves_rt, can_opt);
    short_wave!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, rt_con);
    canopy_fluxes!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, leaves_rt,
                   wl_set, rt_con);

    #
    f_H₂O = 0;
    f_GPP = 0;
    f_NPP = 0;
    for i_can in 1:n_canopy
        iEN = envirs[i_can];
        iHS = plant_hs.leaves[i_can];
        iPS = plant_ps[i_can];
        iRT = n_canopy + 1 - i_can;

        iPS.T = can_rad.T_sun[i_can];
        update_leaf_TP!(iPS, iHS, iEN);
        temperature_effects!(iHS, iPS.T);

        # calculate the fraction of sunlit and shaded leaves
        f_view = (can_opt.Ps[iRT] + can_opt.Ps[iRT+1]) / 2;
        for iLF in 1:nSL
            iPS.APAR[iLF] = can_rad.absPAR_sunCab[(iRT-1)*nSL+iLF] * FT(1e6);
            iPS.LAIx[iLF] = f_view * f_SL[iLF];
        end
        iPS.APAR[end] = can_rad.absPAR_shadeCab[iRT] * FT(1e6);
        iPS.LAIx[end] = 1 - f_view;

        # step forward with Δt
        gas_exchange!(iPS, iEN, GswDrive());
        if typeof(stomata_model) <: EmpiricalStomatalModel
            #
            #
            #
            #
            # also need to determine which β function is used
            #     to tune g1
            #     to tune Vcmax
            #
            #
            #
            #
            prognostic_gsw!(iPS, iEN, stomata_model, FT(1), Δt);
        else
            prognostic_gsw!(iPS, iHS, iEN, stomata_model, Δt);
        end

        # update flow and pressure profile (except for history)
        # TODO move this part to PlantHydraulics.jl
        gsw_control!(iPS, iEN);
        for i_can in 1:n_canopy
            iEN = envirs[i_can];
            iLF = plant_hs.leaves[i_can];
            iPS = plant_ps[i_can];
            iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
        end
        flow_profile!(plant_hs);
        pressure_profile!(plant_hs, SteadyStateMode(); update=false);

        # TODO make sure this is done in future SPAC module
        # update canopy layer p_ups, which will be passed to each leaf
        for _i_can in 1:n_canopy
            _iHS = plant_hs.leaves[_i_can];
            _iPS = plant_ps[_i_can];
            _iPS.p_ups = _iHS.p_ups;
        end;

        # update the fluorescence quantum yield from leaf level calculation
        can_rad.ϕ_sun[:,:,iRT] .= reshape(view(iPS.φs,1:nSL), canopy_rt.nIncl, canopy_rt.nAzi);
        can_rad.ϕ_shade[iRT] = iPS.φs[end];

        # update the flow rates
        for iLF in 1:(nSL+1)
            f_GPP += iPS.Ag[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_NPP += iPS.An[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_H₂O += iPS.g_lw[iLF] * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR * iPS.LAIx[iLF] * iPS.LA;
        end
    end

    # do SIF simulation
    SIF_fluxes!(leaves_rt, can_opt, can_rad, canopy_rt, soil_opt, wl_set, rt_con, rt_dim);

    # update flow profile and pressure history along the tree
    if updating
        for i_can in 1:n_canopy
            iEN = envirs[i_can];
            iLF = plant_hs.leaves[i_can];
            iPS = plant_ps[i_can];
            iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
        end
        flow_profile!(plant_hs);
        pressure_profile!(plant_hs, SteadyStateMode(); update=true);
    end

    # update the flows in SPACMono
    node.f_gpp = f_GPP / ga;
    node.f_npp = f_NPP / ga;
    node.f_H₂O = f_H₂O / ga;

    return nothing
end


# Types for managing event selection
abstract type BitField end

fields(x::BitField)     = fields(typeof(x))
name(x::BitField)       = name(typeof(x))
defaults(x::BitField)   = defaults(typeof(x))
value(x::BitField)      = x.val
Base.convert(::Type{T}, x::T) where {T <: BitField} = x

Base.:(==)(x::BitField, y::BitField) = value(x) == value(y)
Base.hash(x::BitField, u::UInt = UInt(0x12038497)) = hash(value(x), u)

(::Type{T})(;kw...) where {T <: BitField} = construct(T, (;kw...))

# Main entry point
function construct(::Type{T}, kw::NamedTuple) where {T <: BitField}
    # Apply defaults to the provided keywords.
    kw = merge(defaults(T), kw)
    val = zero(UInt64)
    fieldpairs = fields(T)
    for (k, v) in pairs(kw)
        # Get the start and stop points of this bit field.
        start, stop = fieldpairs[k]

        # Mask the provided value to the requested number of bits.
        v = v & mask(0, stop - start)

        # Set the corresponding bits in `x`.
        # Since `x` is initialized to zero, we do not need to explicitly clear these bits.
        val |= (v << start)
    end
    return T(val)
end

function Base.show(io::IO, reg::BitField)
    print(io, name(reg), ": ")
    for k in keys(fields(reg))
        print(io, " $k=$(string(reg[k]; base = 16))")
    end
    println(io)
    return nothing
end
hex(i::BitField) = hex(value(i))

# Allow this to be written to an IO by forwarding to the wrapped value
Base.write(io::IO, reg::BitField) = write(io, value(reg))
Base.convert(::Type{T}, reg::BitField) where {T <: Integer} = convert(T, value(reg))

# For convenience purposes
function Base.getindex(reg::BitField, name::Symbol)
    start, stop = fields(reg)[name]
    return (value(reg) & mask(start, stop)) >> start
end

### Core Event Select
"""
    CoreSelectRegister(; kw...)

Construct a bitmask for programming `Core` level counters.

Keywords
========
- `event::UInt`: Select the event to be counted. Default: `0x00`
- `umask::Uint`: Select the subevent to be counted within the selected event. Default: `0x00`
- `usr::Bool`: Specifies the counter should be active when the processor is operating at
    privilege modes 1, 2, and 3. Default: `true`.
- `os::Bool`: Specifies the counter should be active when the processor is operating at
    privilege mode 0. Default: `true`.
- `e::Bool`: Edge detect. Default: `false`.
- `en::Bool`: Enable the counter. Default: `true`.
- `inv::Bool`: When set, inverts the counter-mask (CMASK) comparison, so that both greater
    than or equal to and less than comparisons can be made (0: greater than or equal; 1:
    less than). Note if counter-mask is programmed to zero, INV flag is ignored. Default: `false`.
- `cmask::Bool`: When this field is not zero, a logical processor compares this mask to the
    events count of the detected microarchitectural condition during a single cycle. If the
    event count is greater than or equal to this mask, the counter is incremented by one.
    Otherwise the counter is not incremented.

    This mask is intended for software to characterize microarchitectural conditions that
    can count multiple occurrences per cycle (for example, two or more instructions retired
    per clock; or bus queue occupations). If the counter-mask field is 0, then the counter
    is incremented each cycle by the event count associated with multiple occurrences.
"""
struct CoreSelectRegister <: BitField
    val::UInt64
end

function fields(::Type{CoreSelectRegister})
    nt = (
        event = (0, 7),
        umask = (8, 15),
        usr   = (16, 16),
        os    = (17, 17),
        e     = (18, 18),
        #pc    = (19, 19), # not implemented on Skylake+
        #int   = (20, 20), # not worrying about this
        en    = (22, 22),
        inv   = (23, 23),
        cmask = (24, 31),
    )
    return nt
end
name(::Type{CoreSelectRegister}) = "Core Event Select Register"
defaults(::Type{CoreSelectRegister}) = (usr = true, os = true, en = true)

### Uncore Select
"""
    UncoreSelectRegister(; kw...)

Construct a bitmask for programming `Uncore` level counters.

Keywords
========

- `event::UInt`: Select event to be counted. Default: `0x00`
- `umask::UInt`: Select subevents to be counted within the selected event. Default: `0x00`
- `reset::Bool`: When set to 1, the corresponding counter will be cleared to 0. Default: `false`
- `edge_detact::Bool`: When set to 1, rather than measuring the event in each cycle it is
    active, the corresponding counter will increment when a 0 to 1 transition (i.e. rising edge)
    is detected.

    When 0, the counter will increment in each cycle that the event is asserted.

    NOTE: `edge_detect` is in series following `thresh`, Due to this, the `thresh` field
    must be set to a non-0 value. For events that increment by no more than 1 per cycle,
    set `thresh` to 0x1. Default: `false`.
- `overflow_enable::Bool`: When this bit is set to 1 and the corresponding counter overflows,
    an overflow message is sent to the UBox’s global logic. The message identifies the unit
    that sent it. Default: `false`.
- `en::Bool`: Local Counter Enable. Default: `true`.
- `invert::Bool`: Invert comparison against Threshold.

    0 - comparison will be ‘is event increment >= threshold?’.

    1 - comparison is inverted - ‘is event increment < threshold?’

    e.g. for a 64-entry queue, if SW wanted to know how many cycles the queue had fewer
    than 4 entries, SW should set the threshold to 4 and set the invert bit to 1.
    Default: `false`.
- `thresh::UInt`: Threshold is used, along with the invert bit, to compare against the
    counter’s incoming increment value. i.e. the value that will be added to the counter.

    For events that increment by more than 1 per cycle, if the threshold is set to a value
    greater than 1, the data register will accumulate instances in which the event
    increment is >= threshold. Default: `0x00`.
"""
struct UncoreSelectRegister <: BitField
    val::UInt64
end

function fields(::Type{UncoreSelectRegister})
    nt = (
        event           = (0, 7),
        umask           = (8, 15),
        reset           = (17, 17),
        edge_detect     = (18, 18),
        tid_enable      = (19, 19),      # Only applicable for CHAS
        overflow_enable = (20, 20),
        en              = (22, 22),
        invert          = (23, 23),
        thresh          = (24, 31),
    )
    return nt
end
name(::Type{UncoreSelectRegister}) = "Uncore Event Select Register"
defaults(::Type{UncoreSelectRegister}) = (en = true,)

### CHA Filter 0
"""
    CHAFilter0(; kw...)
"""
struct CHAFilter0 <: BitField
    val::UInt64
end
fields(::Type{CHAFilter0}) = (
    thread_id   = (0, 2),
    core_id     = (3, 8),
    LLC_I_state = (17, 17),
    SF_S_state  = (18, 18),
    SF_E_state  = (19, 19),
    SF_H_state  = (20, 20),
    LLC_S_state = (21, 21),
    LLC_E_state = (22, 22),
    LLC_M_state = (23, 23),
    LLC_F_state = (24, 24),
)
name(::Type{CHAFilter0}) = "CHA Filter 0"
defaults(::Type{CHAFilter0}) = (
    LLC_I_state = true,
    LLC_S_state = true,
    LLC_E_state = true,
    LLC_M_state = true,
    LLC_F_state = true,
)

# ### CHA Filter 1
"""
    CHAFilter1(; kw...)
"""
struct CHAFilter1 <: BitField
    val::UInt64
end

fields(::Type{CHAFilter1}) = (
    remote          = (0, 0),
    loc             = (1, 1),
    all_opc         = (3, 3),
    near_memory     = (4, 4),
    not_near_memory = (5, 5),
    opc0            = (9, 18),
    opc1            = (19, 28),
    non_coherent    = (30, 30),
    isoc            = (31, 31),
)
name(::Type{CHAFilter1}) = "CHA Filter 1"
defaults(::Type{CHAFilter1}) = (
    remote          = true,
    loc             = true,
    all_opc         = true,
    near_memory     = true,
    not_near_memory = true
)


# ---------------------------------------------------------------------------- #
#
#   genCAD.jl
#
#   Couple to egads.jl to write a STEP file of the geometry
#
#   intrico
#   Spring 2018
#
#   Max Opgenoord
#
# ---------------------------------------------------------------------------- #

"""
    writeCAD( mesh::MeshF,  lat::Lattice, flname::String; eps = 5.e-3, Δeps = 5.e-3 )

Writes a CAD file for the lattice in `mesh` with the areas defined in `lat`.
"""
function genCAD( mesh::MeshF,  lat::Lattice, flname::String; eps = 5.e-3, Δeps = 5.e-3 )

    # open egads
    (context, status) = egads.EG_open( )
    if (status < 0) error("Can't open, failure code: %i", status) end

    @printf( "CAD: %d nodes, %d edges\n", mesh.n, size(mesh.e,1) )

    # 1. connect all nodes to their respective half-length edges
    println("CAD:   generating nodes")
    nodes = genNodesCAD( mesh, lat, context, eps, Δeps )

    # 2. connect those nodal hubs together to generate model
    println("CAD:   connecting nodes")
    finmodel = genModelCAD( mesh, nodes, context )

    # 3. shave off nodes interfering with boundary
    # TODO

    # 4. save model
    println("CAD:   saving model")
    if isfile( flname )
        println("   File exists, overwrite? (y/N)")
        user_input = chomp(readline())
        if isempty(user_input)
            # don't overwrite
            println("   Not overwriting, data lost")
        elseif user_input[1] == 'Y' || user_input[1] == 'y'
            rm( flname )
            status = egads.EG_saveModel( finmodel, flname )
            if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
        else
            println("   Not overwriting, data lost")
        end

    else
        status = egads.EG_saveModel( finmodel, flname )
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
    end

    # close everything
    status = egads.EG_deleteObject( finmodel )
    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
    # delete nodes
    for kk in 1:length( nodes )
        status = egads.EG_deleteObject( nodes[kk] )
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
    end
    egads.cleanup( context )

end

"""
    genNodesCAD( mesh::MeshF3D, lat::Lattice, context::egads.ego, eps::Float64, Δeps::Float64 )

Generates nodes by Boolean operations between cylinders and spheres.
"""
function genNodesCAD( mesh::MeshF3D, lat::Lattice, context::egads.ego, eps::Float64, Δeps::Float64 )

    nodes = fill( egads.ego(0), mesh.n )

    for ii in 1:mesh.n
        @printf(".")
        ar_max = maximum( lat.ar[ mesh.n2e[ii] ] )

        status_boolean = Cint(1)

        emodel = egads.ego(0)
        ebody  = egads.ego(0)

        inod = 0

        while (status_boolean != egads.EGADS_SUCCESS)

            # Generate sphere
            datasph = vcat( mesh.p[ii,:], sqrt(ar_max/π)*(1.0 + eps) )

            (ebody, status) = egads.EG_makeSolidBody(context, egads.SPHERE, datasph)
            if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

            # Generate half length rods
            for jj in 1:length(mesh.n2e[ii])
                ie   = mesh.n2e[ii][jj]
                nods = mesh.e[ie,1:2]
                inod = find( nods .== ii )[]
                ave = mesh.p[nods[1],:] + 0.50 * (mesh.p[nods[2],:] - mesh.p[nods[1],:])
                datacyl = vcat( mesh.p[ nods[1] ,:], # always generate this half cylinder, but then move it into the correct pos
                                ave,
                                sqrt( lat.ar[ie] / π ) )

                (etemp,status) = egads.EG_makeSolidBody(context, egads.CYLINDER, datacyl)
                if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                if inod == 2
                    Δx = 0.50 * (mesh.p[nods[2],:] - mesh.p[nods[1],:])

                    matrix = [ 1.0, 0.0, 0.0, Δx[1],
                               0.0, 1.0, 0.0, Δx[2],
                               0.0, 0.0, 1.0, Δx[3] ]

                    exform_ptr = Ref{egads.ego}()
                    status = egads.EG_makeTransform(context, matrix, exform_ptr)
                    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                    (etemp2,status) = egads.EG_copyObject(etemp, exform_ptr[])
                    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                    status = egads.EG_deleteObject(exform_ptr[])
                    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                    status = egads.EG_deleteObject(etemp)
                    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                    (etemp,status) = egads.EG_copyObject(etemp2, C_NULL)
                    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                    status = egads.EG_deleteObject(etemp2)
                    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                end

                # join to rest of node
                (emodel, status) = egads.EG_solidBoolean(ebody, etemp, egads.FUSION)
                # if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
                status_boolean = status

                # delete temporary bodies
                status = egads.EG_deleteObject(ebody)
                if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
                status = egads.EG_deleteObject(etemp)
                if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

                if (status_boolean != egads.EGADS_SUCCESS)
                    eps += Δeps
                    @printf("   NOTE: increasing radius of node %d by %3.2f%%\n", ii, Δeps*100.)
                    break
                end

                # get body from model
                (ebody, status) = egads.getBodyFromModel( emodel )
                if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

            end

        end

        # save model
        (nodes[ii], status) = egads.EG_copyObject(emodel, C_NULL)
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

        # delete body and model
        status = egads.EG_deleteObject(ebody)
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
        status = egads.EG_deleteObject(emodel)
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

    end

    @printf("\n")

    return nodes # NOTE: nodes are models

end

"""
    genModelCAD( mesh::MeshF3D, nodes::Vector{egads.ego}, context::egads.ego )

Stitches the nodes together into one final model.
"""
function genModelCAD( mesh::MeshF3D, nodes::Vector{egads.ego}, context::egads.ego )

    efaces_ptr = Vector{ Ptr{egads.ego} }( mesh.n )
    nface_vec  = Vector{Int64}( mesh.n )

    # get faces of all nodes first
    toler = Cdouble(0.0)
    for kk in 1:mesh.n

        # get body from model
        (ebody,status) = egads.getBodyFromModel( nodes[ kk ] )
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

        # get faces from body
        nface_vec[kk], ef_ptr, status = egads.EG_getBodyFaces( ebody, toler )
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

        efaces_ptr[kk] = ef_ptr[]

    end

    # get matching faces
    tolerf = Cdouble(0.0); cntmax = 14
    matches_vec = Vector{ Vector{Int64} }( mesh.n )
    for ii in 1:mesh.n
        matches_vec[ii] = fill( 0, 0 )
    end
    for jj in 1:size(mesh.e,1)

        nod1 = mesh.e[jj,1]
        nod2 = mesh.e[jj,2]

        # get bodies from model
        (ebody1,status) = egads.getBodyFromModel( nodes[ nod1 ] )
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
        (ebody2,status) = egads.getBodyFromModel( nodes[ nod2 ] )
        if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

        # match faces
        nmatch  = 0
        matches = fill( 0, 0 )

        cnt = 0
        while nmatch != 1 && cnt < cntmax
            (nmatch,matches,status) = egads.EG_matchBodyFaces( ebody1, ebody2, tolerf )
            if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end
            if cnt == 0
                tolerf += 1e-15
            else
                tolerf *= 10.0
                warn("WriteCAD: Was not able to find matching faces between nodes ",
                    nod1, " and ", nod2, " on edge ",jj, " increasing tolerance to ", tolerf)
                println("nmatch: ",nmatch)

            end
            cnt += 1
        end
        if cnt == cntmax
            error("WriteCAD: Was not able to find matching faces between nodes ",
                nod1, " and ", nod2, " on edge ",jj)
        end
        tolerf *= 0.0

        append!( matches_vec[nod1], matches[1] )
        append!( matches_vec[nod2], matches[2] )

    end

    # generate list of faces
    efaces = Vector{ egads.ego }( sum(nface_vec) )
    fcnt = 1
    for kk in 1:mesh.n

        for ii in 1:nface_vec[kk]
            if !any( ii .== matches_vec[kk] )
                efaces[fcnt] = unsafe_load(efaces_ptr[kk],ii); fcnt += 1
            end
        end

    end
    fcnt -= 1

    # join model
    tolerj = Cdouble(0.0)
    (emodel, status) = egads.EG_sewFaces( Cint(fcnt), efaces, tolerj, Cint(0) )
    if (status != egads.EGADS_SUCCESS) egads.cleanup(status, context) end

    return emodel

end