// Perform HDFE partialling in a common sample

cap program drop partialhdfejl
program define partialhdfejl
  version 15

  syntax varlist [if] [in] [aw pw fw iw/], Absorb(string) [GENerate(string) PREfix(string) replace ITerations(integer 16000) gpu TOLerance(real 1e-8) compact]

  _assert `iterations'>0, msg("{cmdab:It:erations()} must be positive.") rc(198)
  _assert `tolerance'>0, msg("{cmdab:tol:erance()} must be positive.") rc(198)

  if (`"`generate'"'!="") + (`"`prefix'"'!="") != 1 {
    di as err "Specify exactly one of {cmdab:gen:erate()} or {cmdab:pre:fix()}."
    exit 198
  }

  marksample touse

  local gpulib = cond(c(os)=="MacOSX", "Metal", "CUDA")
  if "`gpu'"!="" local methodopt , method = :`gpulib'

  if strpos("`varlist'", ".") | strpos("`varlist'", "#") | strpos("`varlist'", "-") | strpos("`varlist'", "?") | strpos("`varlist'", "*") | strpos("`varlist'", "~") {
    fvexpand `varlist' if `touse'
    local varlist
    foreach var in `r(varlist)' {
      _ms_parse_parts `var'
      if !r(omit) local varlist `varlist' `var'
    }
    fvrevar `varlist' if `touse'
    local varlist `r(varlist)'
  }

  if `"`generate'"'!="" _assert `:word count `generate''==`:word count `varlist'', msg("generate() option has wrong number of variable names") rc(198)

  if `"`exp'"' != "" {
    cap confirm var `exp'
    if _rc {
      tempname wtvar
      gen double `wtvar' = `exp' if `touse'
    }
    else local wtvar `exp'
    local wtopt , weights = :`wtvar'
  }

  reghdfejl_load

  tokenize `absorb'
  local absorb `*'  // remove extra spaces
  local feterms i.`: subinstr local absorb " " " i.", all'

  local absorbvars `feterms'
  local feterms: subinstr local feterms "##c." ")*(", all
  local feterms: subinstr local feterms "#c." ")&(", all
  local feterms: subinstr local feterms "##i." ")*fe(", all
  local feterms: subinstr local feterms "##" "#", all
  local feterms: subinstr local feterms "#" "#i.", all
  local feterms: subinstr local feterms "i.i." "i.", all
  local feterms: subinstr local feterms "#i." ")&fe(", all
  local feterms: subinstr local feterms "i." "fe(", all
  local feterms: subinstr local feterms " " ") + ", all
  local feterms: subinstr local feterms ")" " )", all
  local feterms: subinstr local feterms "(" "( ", all
  local feterms + `feterms' )

  local absorbvars: subinstr local absorbvars "i." " ", all
  local absorbvars: subinstr local absorbvars "c." " ", all
  local absorbvars: subinstr local absorbvars "#" " ", all

  foreach var in `absorbvars' {
    cap confirm numeric var `var'
    if _rc {
      tempvar t
      egen long `t' = group(`var') if `touse'
      local absorbvars: subinstr local absorbvars "`var'" "`t'", word all
      local feterms   : subinstr local feterms    "`var'" "`t'", word all
    }
  }

  markout `touse' `absorbvars'

  if "`compact'"!="" {
    tempfile compactfile
    save "`compactfile'"
    keep `varlist' `absorbvars' `wtvar' `touse'
    qui keep if `touse'
  }

  local vars `varlist' `absorbvars' `wtvar'
  local vars: list uniq vars

  jl PutVarsToDF `vars' if `touse', nomissing double
  qui _jl: size(df,1)
  _assert `r(ans)', rc(2001) msg(insufficient observations)

  if "`compact'" !="" drop _all

  _jl: p = partial_out(df, @formula(`:subinstr local varlist " " " + ", all' ~ 1 `feterms') `wtopt', tol=`tolerance', maxiter=`iterations' `methodopt');

  if "`compact'"!="" {
    _jl: GC.gc();
    use `compactfile'
  }

  if `"`prefix'"' != "" local generate `prefix'`: subinstr local varlist " " " `prefix'", all'
    else if "`replace'"!="" {
      tempname t
      _jl: SF_scal_save("`t'", size(df)[1]);
      if `t' < _N {
        foreach var in `generate' {
          cap replace `var' = .
        }
      }
    }

  jl GetVarsFromDF `generate' if `touse', source(p[1]) cols(`varlist') `replace'

  foreach var in `generate' {
    label var `var' "Residuals"
  }

  _jl: df = p = nothing;
end