*! _xtpkpss_engine.ado
*! Engine subroutines for xtpkpss (Mata version)
*! Carrion-i-Silvestre, del Barrio-Castro & López-Bazo (2005)
*! Maps to: pd_kpss.src (GAUSS)
*!
*! Author: Dr Merwan Roudane (merwanroudane920@gmail.com)
*! Version 2.0.0 — 26 March 2026 (Mata rewrite for performance)

mata:
mata clear

// LRV estimation (Bartlett kernel)
real scalar _kp_lrvar(real matrix u, real scalar bw) {
  real scalar T, s0, sl, j, w, Tj, gam

  T = rows(u)
  s0 = (u' * u)[1,1] / T
  sl = 0
  for (j=1; j<=bw; j++) {
    w = 1 - j/(bw+1)
    gam = (u[(j+1)..T]' * u[1..(T-j)])[1,1] / T
    sl = sl + 2 * w * gam
  }
  return(s0 + sl)
}

// Build deterministic matrix (GAUSS: __pd_dekpss)
real matrix _kp_determ(real scalar model, real scalar T, real matrix tb, real scalar nbr) {
  real matrix z, du, dt
  real scalar i

  if (model == 1) {
    z = J(T, 1, 1)
  }
  else if (model == 2) {
    z = J(T, 1, 1), (1::T)
  }
  else if (model == 3) {
    z = J(T, 1, 1)
    for (i=1; i<=nbr; i++) {
      du = J(tb[i], 1, 0) \ J(T - tb[i], 1, 1)
      z = z, du
    }
  }
  else if (model == 4) {
    z = J(T, 1, 1), (1::T)
    for (i=1; i<=nbr; i++) {
      du = J(tb[i], 1, 0) \ J(T - tb[i], 1, 1)
      dt = J(tb[i], 1, 0) \ (1::(T - tb[i]))
      z = z, du, dt
    }
  }
  return(z)
}

// Individual KPSS (GAUSS: __pd_kpss)
void _kp_individual(real matrix y, real scalar model,
                    real matrix tb, real scalar nbr, real scalar bw,
                    real scalar kpss_out, real scalar num_out, real scalar den_out) {
  real scalar T, lrv, num
  real matrix z, beta, e, St

  T = rows(y)
  z = _kp_determ(model, T, tb, nbr)
  beta = invsym(z' * z) * (z' * y)
  e = y - z * beta
  St = runningsum(e)
  lrv = _kp_lrvar(e, bw)
  num = (St' * St)[1,1] / T^2

  kpss_out = num / lrv
  num_out = num
  den_out = lrv
}

// Moments (GAUSS: __pd_calcdem)
void _kp_moments(real scalar model, real matrix tb, real scalar nbr, real scalar T,
                 real scalar mu_out, real scalar var_out) {
  real scalar A, B, k, nsegs
  real scalar prev_lam, lam_k, diff

  if (model == 1 | model == 3) {
    A = 1/6
    B = 1/45
  }
  else {
    A = 1/15
    B = 11/6300
  }

  nsegs = nbr + 1
  mu_out = 0
  var_out = 0
  prev_lam = 0

  for (k=1; k<=nsegs; k++) {
    if (k <= nbr) {
      lam_k = tb[k] / T
    }
    else {
      lam_k = 1
    }
    diff = lam_k - prev_lam
    mu_out = mu_out + A * diff^2
    var_out = var_out + B * diff^4
    prev_lam = lam_k
  }
}

// Break detection via BIC (simplified Bai-Perron)
void _kp_breaks(real matrix y, real matrix z, real scalar T,
                real scalar maxbrk, real scalar trimm,
                real scalar nbrk_out, real matrix brk_out) {
  real scalar seg, m, b, ssr, min_ssr, best_b
  real scalar bic0, bic_m, best_bic, ncol
  real matrix resid0, du, zb, rb
  real matrix curr_brk, best_brk
  real scalar kk, k, s1, s2

  seg = max((2, round(trimm * T)))

  // BIC for no-break model
  resid0 = y - z * invsym(z' * z) * (z' * y)
  bic0 = T * ln(sum(resid0:^2)/T) + cols(z) * ln(T)
  best_bic = bic0
  nbrk_out = 0
  best_brk = J(maxbrk, 1, 0)

  for (m=1; m<=maxbrk; m++) {
    curr_brk = J(m, 1, 0)

    if (m == 1) {
      // Grid search for single break
      min_ssr = .
      best_b = seg
      for (b=seg; b<=T-seg; b++) {
        du = J(b, 1, 0) \ J(T-b, 1, 1)
        zb = z, du
        rb = y - zb * invsym(zb' * zb) * (zb' * y)
        ssr = sum(rb:^2)
        if (ssr < min_ssr) {
          min_ssr = ssr
          best_b = b
        }
      }
      curr_brk[1] = best_b
    }
    else {
      // Initialize equally spaced
      for (k=1; k<=m; k++) {
        curr_brk[k] = round(seg + (k-1) * (T - 2*seg) / m)
      }

      // Refine each break
      for (k=1; k<=m; k++) {
        if (k == 1) {
          s1 = seg
        }
        else {
          s1 = curr_brk[k-1] + seg
        }
        if (k == m) {
          s2 = T - seg
        }
        else {
          s2 = curr_brk[k+1] - seg
        }

        min_ssr = .
        best_b = s1
        for (b=s1; b<=s2; b++) {
          curr_brk[k] = b
          zb = z
          for (kk=1; kk<=m; kk++) {
            du = J(curr_brk[kk], 1, 0) \ J(T - curr_brk[kk], 1, 1)
            zb = zb, du
          }
          rb = y - zb * invsym(zb' * zb) * (zb' * y)
          ssr = sum(rb:^2)
          if (ssr < min_ssr) {
            min_ssr = ssr
            best_b = b
          }
        }
        curr_brk[k] = best_b
      }
    }

    // BIC with m breaks (LWZ criterion)
    zb = z
    for (k=1; k<=m; k++) {
      du = J(curr_brk[k], 1, 0) \ J(T - curr_brk[k], 1, 1)
      zb = zb, du
    }
    rb = y - zb * invsym(zb' * zb) * (zb' * y)
    ncol = cols(zb)
    bic_m = T * ln(sum(rb:^2)/T) + ncol * 0.299 * ln(T)^2.1

    if (bic_m < best_bic) {
      best_bic = bic_m
      nbrk_out = m
      best_brk = J(maxbrk, 1, 0)
      for (k=1; k<=m; k++) {
        best_brk[k] = curr_brk[k]
      }
    }
  }

  brk_out = best_brk
}

// ---- Wrapper functions for single-line mata: calls from Stata ----

void _kp_mom_wrap(real scalar model, real matrix tb, real scalar nbr, real scalar T,
                  string scalar mu_name, string scalar var_name) {
  real scalar mu_v, var_v
  _kp_moments(model, tb, nbr, T, mu_v, var_v)
  st_numscalar(mu_name, mu_v)
  st_numscalar(var_name, var_v)
}

void _kp_brk_wrap(string scalar yname, string scalar zname,
                  real scalar T, real scalar maxbrk, real scalar trimm) {
  real scalar nbrk_v
  real matrix brk_v
  _kp_breaks(st_matrix(yname), st_matrix(zname), T, maxbrk, trimm, nbrk_v, brk_v)
  st_numscalar("__nbrk", nbrk_v)
  st_matrix("__brk", brk_v)
}

void _kp_ind_wrap(string scalar yname, real scalar model,
                  real matrix tb, real scalar nbr, real scalar bw,
                  string scalar kname, string scalar nname, string scalar dname) {
  real scalar kv, nv, dv
  _kp_individual(st_matrix(yname), model, tb, nbr, bw, kv, nv, dv)
  st_numscalar(kname, kv)
  st_numscalar(nname, nv)
  st_numscalar(dname, dv)
}

end

* ====================================================================
* Stata wrapper programs
* ====================================================================

capture program drop _kpss_deterministics
program define _kpss_deterministics, rclass
  args T model breakmat nbreaks
  * Not called directly anymore, but kept for compatibility
  tempname z
  if `nbreaks' == 0 {
    mata: st_matrix("`z'", _kp_determ(`model', `T', J(1,1,0), 0))
  }
  else {
    mata: st_matrix("`z'", _kp_determ(`model', `T', st_matrix("`breakmat'"), `nbreaks'))
  }
  return matrix result = `z'
end

capture program drop _kpss_moments
program define _kpss_moments, rclass
  args model breakmat nbreaks T

  tempname mu_s var_s
  if `nbreaks' == 0 {
    mata: _kp_mom_wrap(`model', J(1,1,0), 0, `T', "`mu_s'", "`var_s'")
  }
  else {
    mata: _kp_mom_wrap(`model', st_matrix("`breakmat'"), `nbreaks', `T', "`mu_s'", "`var_s'")
  }
  return scalar mu = `mu_s'
  return scalar var = `var_s'
end

capture program drop _kpss_breaks
program define _kpss_breaks, rclass
  args ymat zmat T maxbreaks trimm

  mata: _kp_brk_wrap("`ymat'", "`zmat'", `T', `maxbreaks', `trimm')

  return scalar nbreaks = __nbrk
  tempname br
  matrix `br' = __brk
  capture scalar drop __nbrk
  capture matrix drop __brk
  return matrix breaks = `br'
end

capture program drop _kpss_individual
program define _kpss_individual, rclass
  args ymat model breakmat nbreaks bw kernel

  tempname kpss_s num_s den_s
  if `nbreaks' == 0 {
    mata: _kp_ind_wrap("`ymat'", `model', J(1,1,0), 0, `bw', "`kpss_s'", "`num_s'", "`den_s'")
  }
  else {
    mata: _kp_ind_wrap("`ymat'", `model', st_matrix("`breakmat'"), `nbreaks', `bw', "`kpss_s'", "`num_s'", "`den_s'")
  }

  return scalar kpss = `kpss_s'
  return scalar num = `num_s'
  return scalar den = `den_s'
  return scalar lrv = `den_s'
end

capture program drop _kpss_panel_stats

program define _kpss_panel_stats, rclass
  args kpss_vec mu_vec var_vec num_vec den_vec N

  tempname kmat mumat vmat nmat dmat
  matrix `kmat' = `kpss_vec'
  matrix `mumat' = `mu_vec'
  matrix `vmat' = `var_vec'
  matrix `nmat' = `num_vec'
  matrix `dmat' = `den_vec'

  * Heterogeneous: mean of individual KPSS
  local lm_het = 0
  forvalues i = 1/`N' {
    local lm_het = `lm_het' + `kmat'[`i', 1]
  }
  local lm_het = `lm_het' / `N'

  * Homogeneous: meanc(num)/meanc(den)
  local num_bar = 0
  local den_bar = 0
  forvalues i = 1/`N' {
    local num_bar = `num_bar' + `nmat'[`i', 1]
    local den_bar = `den_bar' + `dmat'[`i', 1]
  }
  local lm_hom = (`num_bar'/`N') / (`den_bar'/`N')

  * Moments
  local mu_bar = 0
  local var_bar = 0
  forvalues i = 1/`N' {
    local mu_bar = `mu_bar' + `mumat'[`i', 1]
    local var_bar = `var_bar' + `vmat'[`i', 1]
  }
  local mu_bar = `mu_bar' / `N'
  local var_bar = `var_bar' / `N'

  local z_hom = sqrt(`N') * (`lm_hom' - `mu_bar') / sqrt(`var_bar')
  local z_het = sqrt(`N') * (`lm_het' - `mu_bar') / sqrt(`var_bar')

  return scalar z_hom = `z_hom'
  return scalar z_het = `z_het'
  return scalar lm_hom = `lm_hom'
  return scalar lm_het = `lm_het'
  return scalar mu_bar = `mu_bar'
  return scalar var_bar = `var_bar'
end