## This functions accepts the following arguments:

## y: univariate outcome
## z: endogenous predictors
## w: instruments
## x: exogenous predictors

## zeval: optional evaluation data for the endogenous predictors
## weval: optional evaluation data for the instruments
## xeval: optional evaluation data for the exogenous predictors

## ... optional arguments for crs()

## This function returns a list with the following elements:

## phi: the IV estimator of phi(z) corresponding to the estimated
## derivative phihat(z)
## phi.prime: the IV derivative estimator
## phi.mat: the matrix with colums phi_1, phi_2 etc. over all iterations
## phi.prime.mat: the matrix with colums phi'_1, phi'_2 etc. over all iterations
## num.iterations: number of iterations taken by Landweber-Fridman
## norm.stop: the stopping rule for each Landweber-Fridman iteration
## norm.value: the norm not multiplied by the number of iterations
## convergence: a character string indicating whether/why iteration terminated

crsivderiv <- function(y, ...) UseMethod("crsivderiv")

crsivderiv.default <- function(y,
                       z,
                       w,
                       x=NULL,
                       zeval=NULL,
                       weval=NULL,
                       xeval=NULL,
                       constant=0.5,
                       display.nomad.progress=TRUE,
                       display.warnings=TRUE,
                       iterate.diff.tol=1.0e-08,
                       iterate.max=1000,
                       opts=list("MAX_BB_EVAL"=10000,
                                 "EPSILON"=.Machine$double.eps,
                                 "INITIAL_MESH_SIZE"="r1.0e-01",
                                 "MIN_MESH_SIZE"=paste("r",sqrt(.Machine$double.eps),sep=""),
                                 "MIN_FRAME_SIZE"=paste("r",1,sep=""),
                                 "DISPLAY_DEGREE"=0),
                       penalize.iteration=TRUE,
                       smooth.residuals=TRUE,
                       start.from=c("Eyz","EEywz"),
                       starting.values=NULL,
                       stop.on.increase=TRUE,
                       ...) {

  ptm.start <- proc.time()
  old.crs.messages <- getOption("crs.messages")
  on.exit(options(crs.messages = old.crs.messages), add = TRUE)
  crs.messages <- isTRUE(old.crs.messages)
  is.eval.train <- is.null(zeval) && is.null(weval) && is.null(xeval)
  iv.public.call <- .crs_iv_compact_public_call(match.call())
  iv.public.call[[1L]] <- quote(crsivderiv)

  dot.args <- list(...)
  dot.prep <- .crsiv_prepare_dot_args(dot.args)
  nmulti <- dot.prep$nmulti
  nmulti.loop <- dot.prep$nmulti.loop
  dots.preloop <- dot.prep$dots.preloop
  dots.loop <- dot.prep$dots.loop

  progress.status <- .crs_progress_status_begin(
    enabled = display.nomad.progress,
    surface = "iv_solve"
  )
  on.exit(.crs_progress_status_clear(progress.status), add = TRUE)
  set_status <- function(msg = NULL) {
    if (is.null(msg)) {
      .crs_progress_status_clear(progress.status)
    } else {
      .crs_progress_status_update(progress.status, msg)
    }
    invisible(NULL)
  }

  iv_nested_context <- function(label, iteration = NULL) {
    if (is.null(label) || !nzchar(label)) {
      return(NULL)
    }

    if (is.null(iteration)) {
      return(label)
    }

    sprintf("%s, iteration %s", label, format(iteration))
  }

  with_nested_crs_progress <- function(expr) {
    expr <- substitute(expr)
    .crs_set_messages(crs.messages, FALSE)
    on.exit(.crs_set_messages(crs.messages, TRUE), add = TRUE)

    eval(expr, envir = parent.frame())
  }

  fit.crs <- function(formula, data, dots, deriv = NULL, degree = NULL,
                      segments = NULL, lambda = NULL, include = NULL,
                      nmulti = NULL) {
    with_nested_crs_progress(
      .crsiv_fit_crs(formula = formula,
                     data = data,
                     dots = dots,
                     opts = opts,
                     display.nomad.progress = FALSE,
                     display.warnings = display.warnings,
                     deriv = deriv,
                     degree = degree,
                     segments = segments,
                     lambda = lambda,
                     include = include,
                     nmulti = nmulti)
    )
  }

  run.crs <- function(...) {
    args <- list(...)
    args$display.nomad.progress <- FALSE
    with_nested_crs_progress(do.call(crs, args))
  }

  ## Basic error checking

  if(!is.logical(stop.on.increase)) stop("stop.on.increase must be logical (TRUE/FALSE)")
  if(!is.logical(smooth.residuals)) stop("smooth.residuals must be logical (TRUE/FALSE)")
  start.from <- match.arg(start.from)

  if(constant <= 0 || constant >=1) stop("constant must lie in (0,1)")
  if(missing(y)) stop("You must provide y")
  if(missing(z)) stop("You must provide z")
  if(missing(w)) stop("You must provide w")
  if(NCOL(y) > 1) stop("y must be univariate")
  if(NROW(y) != NROW(z) || NROW(y) != NROW(w)) stop("y, z, and w have differing numbers of rows")
  if(!is.null(x) && NROW(y) != NROW(x)) stop("y and x have differing numbers of rows")
  if(iterate.max < 2) stop("iterate.max must be at least 2")
  if(iterate.diff.tol < 0) stop("iterate.diff.tol must be non-negative")

  ## Cast as data frames

  w <- data.frame(w)
  z <- data.frame(z)
  if(!is.null(x)) x <- data.frame(x)

  ## Check for evaluation data

  if(is.null(zeval)) zeval <- z
  if(is.null(weval)) weval <- w
  if(!is.null(x) && is.null(xeval)) xeval <- x

  ## Set up formulas for multivariate w, z, and x if provided

  wnames <- names(w)
  znames <- names(z)
  names(weval) <- wnames
  names(zeval) <- znames

  ## If there exist exogenous regressors X, append these to the
  ## formulas involving Z (can be manually added to W by the user if
  ## desired)

  if(!is.null(x)) {
    xnames <- names(x)
    names(xeval) <- xnames
  }

  ## Now create evaluation data

  if(is.null(x)) {
    traindata <- data.frame(y,z,w)
    evaldata <- data.frame(zeval,weval)
  } else {
    traindata <- data.frame(y,z,w,x)
    evaldata <- data.frame(zeval,weval,xeval)
  }

  finalize.iv.model <- function(model) {
    .crs_iv_attach_native_metadata(
      object = model,
      estimator = "crsivderiv",
      public.call = iv.public.call,
      y = y,
      z = z,
      w = w,
      x = x,
      zeval = zeval,
      is.training.grid = is.eval.train
    )
  }

  if(!is.null(starting.values) && (NROW(starting.values) != NROW(evaldata))) stop(paste("starting.values must be of length",NROW(evaldata)))

  ## Formulae for derivative estimation

  formula.muw <- as.formula(paste("mu ~ ", paste(wnames, collapse= "+")))
  formula.yw <- as.formula(paste("y ~ ", paste(wnames, collapse= "+")))
  formula.phiw <- as.formula(paste("phi ~ ", paste(wnames, collapse= "+")))
  if(is.null(x)) {
    formula.yz <- as.formula(paste("y ~ ", paste(znames, collapse= "+")))
    formula.Eywz <- as.formula(paste("E.y.w ~ ", paste(znames, collapse= "+")))
  } else {
    formula.yz <- as.formula(paste("y ~ ", paste(znames, collapse= "+"), " + ", paste(xnames, collapse= "+")))
    formula.Eywz <- as.formula(paste("E.y.w ~ ", paste(znames, collapse= "+"), " + ", paste(xnames, collapse= "+")))
  }

  ## Landweber-Fridman

  ## We begin the iteration computing phi.prime.0

  set_status()
  if(is.null(x)) {
    set_status("Computing f(z) and S(z) for the initialization state...")
  } else {
    set_status("Computing f(z) and S(z) for the initialization state...")
  }

  ## The current derivative implementation only supports a continuous
  ## univariate endogenous regressor. Compute the unconditional density,
  ## survivor, and integral operator locally using the same gaussian
  ## normal-reference smoothing that was previously sourced from np.

  z.kernel <- .crsiv_extract_continuous_z(z, arg = "z")
  zeval.kernel <- .crsiv_extract_continuous_z(zeval, arg = "zeval")
  bw <- .crsiv_normal_reference_bw(z, display.warnings = display.warnings)
  f.z <- .crsiv_gaussian_density(z.kernel, zeval.kernel, bw)
  S.z <- .crsiv_gaussian_survivor(z.kernel, zeval.kernel, bw)

  if(is.null(starting.values)) {

    set_status()
    if(is.null(x)) {
      set_status("Computing E(y|z) for the initialization state...")
    } else {
      set_status("Computing E(y|z,x) for the initialization state...")
    }

    if(start.from == "Eyz") {
      ## Start from E(Y|z)
      .crs_set_messages(crs.messages, FALSE)
      model.E.y.z <- run.crs(formula.yz,
                         opts=opts,display.nomad.progress=display.nomad.progress,display.warnings=display.warnings,
                         data=traindata,
                         deriv=1,
                         ...)
      .crs_set_messages(crs.messages, TRUE)
      phi.0 <- model.E.y.z
      if(is.eval.train) {
        E.y.z <- model.E.y.z$fitted.values
        phi.prime <- model.E.y.z$deriv.mat[,1]
      } else {
        E.y.z_tmp <- predict(model.E.y.z,newdata=evaldata)
        E.y.z <- as.numeric(E.y.z_tmp)
        phi.prime <- attr(E.y.z_tmp,"deriv.mat")[,1]
      }
    } else {
      ## Start from E(E(Y|w)|z)
      tmp.model <- run.crs(formula.yw,opts=opts,display.nomad.progress=display.nomad.progress,display.warnings=display.warnings,data=traindata,...)
      E.y.w <- fitted(tmp.model)
      model.E.E.y.w.z <- run.crs(formula.Eywz,opts=opts,display.nomad.progress=display.nomad.progress,display.warnings=display.warnings,data=traindata,deriv=1,...)
      phi.0 <- model.E.E.y.w.z
      if(is.eval.train) {
        E.E.y.w.z <- model.E.E.y.w.z$fitted.values
        phi.prime <- model.E.E.y.w.z$deriv.mat[,1]
      } else {
        E.E.y.w.z_tmp <- predict(model.E.E.y.w.z,newdata=evaldata,...)
        E.E.y.w.z <- as.numeric(E.E.y.w.z_tmp)
        phi.prime <- attr(E.E.y.w.z_tmp,"deriv.mat")[,1]
      }
    }

  } else {
    phi.prime <- starting.values
    .crs_set_messages(crs.messages, FALSE)
    phi.0 <- run.crs(formula.yz,opts=opts,data=traindata,display.nomad.progress=display.nomad.progress,display.warnings=display.warnings,...)
    .crs_set_messages(crs.messages, TRUE)
  }

  ## Initialize the recursion at phi'_0 and its integrated curve phi_0.

  set_status()
  if(is.null(x)) {
    set_status("Computing E(y|w) for the initialization state...")
  } else {
    set_status("Computing E(y|w) for the initialization state...")
  }

  ## NOTE - this presumes univariate z case... in general this would
  ## be a continuous variable's index

  phi <- integrate.trapezoidal(z[,1],phi.prime)

  ## In the definition of phi we have the integral minus the mean of
  ## the integral with respect to z, so subtract the mean here

  phi <- phi - mean(phi) + mean(y)

  starting.values.phi <- phi
  starting.values.phi.prime <- phi.prime

  ## For stopping rule...

    .crs_set_messages(crs.messages, FALSE)
    model.E.y.w <- run.crs(formula.yw,
                       opts=opts,display.nomad.progress=display.nomad.progress,display.warnings=display.warnings,
                       data=traindata,
                       ...)
    .crs_set_messages(crs.messages, TRUE)

    ## Capture instrument parameters for summary
    degree.w <- model.E.y.w$degree
    segments.w <- model.E.y.w$segments
    lambda.w <- model.E.y.w$lambda
    include.w <- model.E.y.w$include
    num.x.w <- model.E.y.w$num.x
    num.z.w <- model.E.y.w$num.z
    xnames.w <- model.E.y.w$xnames
    znames.w <- model.E.y.w$znames

    E.y.w <- if(is.eval.train) fitted(model.E.y.w) else predict(model.E.y.w,newdata=evaldata,...)
  ## For the stopping rule, we require E.phi.w

  .crs_set_messages(crs.messages, FALSE)
  traindata$phi <- phi
  model.E.phi.w <- run.crs(formula.phiw,
                       opts=opts,display.nomad.progress=display.nomad.progress,display.warnings=display.warnings,
                       data=traindata,
                       ...)
  .crs_set_messages(crs.messages, TRUE)

  E.phi.w <- if(is.eval.train) fitted(model.E.phi.w) else predict(model.E.phi.w,newdata=evaldata)

  ## Now we compute mu.0 (a residual of sorts)

  mu <- y - phi

  ## Now we repeat this entire process using mu = y - phi.0 rather
  ## than y

  if(smooth.residuals) {

    ## Smooth residuals (smooth of (y-phi) on w)

    .crs_set_messages(crs.messages, FALSE)
    traindata$mu <- mu

    model.E.mu.w <- fit.crs(formula = formula.muw,
                            data = traindata,
                            dots = dots.preloop)

    ## Capture initial parameters for warm start
    degree.muw <- model.E.mu.w$degree
    segments.muw <- model.E.mu.w$segments
    lambda.muw <- model.E.mu.w$lambda
    include.muw <- model.E.mu.w$include

    ## Initialize unused warm start parameters
    degree.phiw <- NULL
    segments.phiw <- NULL
    lambda.phiw <- NULL
    include.phiw <- NULL

    .crs_set_messages(crs.messages, TRUE)

    ## We require the fitted values...

    predicted.model.E.mu.w <- if(is.eval.train) fitted(model.E.mu.w) else predict(model.E.mu.w,newdata=evaldata)

    ## We again require the mean of the fitted values

    mean.predicted.model.E.mu.w <- mean(predicted.model.E.mu.w)

  } else {

    ## Not smoothing residuals (difference of E(Y|w) and smooth of phi
    ## on w)

    .crs_set_messages(crs.messages, FALSE)
    traindata$phi <- phi

    model.E.phi.w <- fit.crs(formula = formula.phiw,
                             data = traindata,
                             dots = dots.preloop)

    ## Capture initial parameters for warm start
    degree.phiw <- model.E.phi.w$degree
    segments.phiw <- model.E.phi.w$segments
    lambda.phiw <- model.E.phi.w$lambda
    include.phiw <- model.E.phi.w$include

    ## Initialize unused warm start parameters
    degree.muw <- NULL
    segments.muw <- NULL
    lambda.muw <- NULL
    include.muw <- NULL

    .crs_set_messages(crs.messages, TRUE)

    ## We require the fitted values...

    predicted.model.E.mu.w <- E.y.w - (if(is.eval.train) fitted(model.E.phi.w) else predict(model.E.phi.w,newdata=evaldata))

    ## We again require the mean of the fitted values

    mean.predicted.model.E.mu.w <- mean(predicted.model.E.mu.w)

  }

  ## Now we compute T^* applied to mu

  cdf.weighted.average <- .crsiv_gaussian_integral_apply(
    z.train = z.kernel,
    z.eval = zeval.kernel,
    rhs = predicted.model.E.mu.w,
    bw = bw
  )

  survivor.weighted.average <- mean.predicted.model.E.mu.w - cdf.weighted.average

  ## Equation (14) uses the same fitted conditional-residual vector in both
  ## empirical-adjoint terms.

  T.star.mu <- (survivor.weighted.average-S.z*mean.predicted.model.E.mu.w)/f.z

  phi.prime.mat <- matrix(NA, nrow=length(phi.prime), ncol=iterate.max)
  phi.mat <- matrix(NA, nrow=length(phi), ncol=iterate.max)
  norm.stop <- numeric(iterate.max)

  ## Column N records the complete state after N derivative updates:
  ## phi_N, phi'_N, and the stopping rule evaluated at phi_N.

  convergence <- "ITERATE_MAX"
  N.evaluated <- 0L
  for (N in seq_len(iterate.max)) {

    phi.prime <- phi.prime + constant*T.star.mu

    set_status()
    if(is.null(x)) {
      set_status(paste("Computing optimal smoothing and phi(z) for iteration ", N," of at most ", iterate.max,"...",sep=""))
    } else {
      set_status(paste("Computing optimal smoothing and phi(z,x) for iteration ", N," of at most ", iterate.max,"...",sep=""))
    }

    ## NOTE - this presumes univariate z case... in general this would
    ## be a continuous variable's index

    phi <- integrate.trapezoidal(z[,1],phi.prime)

    ## In the definition of phi we have the integral minus the mean of
    ## the integral with respect to z, so subtract the mean here

    phi <- phi - mean(phi) + mean(y)

    ## Now we compute mu.0 (a residual of sorts)

    mu <- y - phi

    ## Now we repeat this entire process using mu = y = phi.0 rather than y
    ## Next, we regress require \mu_{0,i} W

    if(smooth.residuals) {

      ## Smooth residuals (smooth of (y-phi) on w)

      .crs_set_messages(crs.messages, FALSE)
      traindata$mu <- mu

      model.E.mu.w <- fit.crs(formula = formula.muw,
                              data = traindata,
                              dots = dots.loop,
                              degree = degree.muw,
                              segments = segments.muw,
                              lambda = lambda.muw,
                              include = include.muw,
                              nmulti = nmulti.loop)

      degree.muw <- model.E.mu.w$degree
      segments.muw <- model.E.mu.w$segments
      lambda.muw <- model.E.mu.w$lambda
      include.muw <- model.E.mu.w$include

      ## We require the fitted values...

      predicted.model.E.mu.w <- if(is.eval.train) fitted(model.E.mu.w) else predict(model.E.mu.w,newdata=evaldata)
      .crs_set_messages(crs.messages, TRUE)

      ## We again require the mean of the fitted values

      mean.predicted.model.E.mu.w <- mean(predicted.model.E.mu.w)

    } else {

      ## Not smoothing residuals (difference of E(Y|w) and smooth of
      ## phi on w)

      .crs_set_messages(crs.messages, FALSE)
      traindata$phi <- phi

      model.E.phi.w <- fit.crs(formula = formula.phiw,
                               data = traindata,
                               dots = dots.loop,
                               degree = degree.phiw,
                               segments = segments.phiw,
                               lambda = lambda.phiw,
                               include = include.phiw,
                               nmulti = nmulti.loop)

      degree.phiw <- model.E.phi.w$degree
      segments.phiw <- model.E.phi.w$segments
      lambda.phiw <- model.E.phi.w$lambda
      include.phiw <- model.E.phi.w$include

      ## We require the fitted values...

      predicted.model.E.mu.w <- E.y.w - (if(is.eval.train) fitted(model.E.phi.w) else predict(model.E.phi.w,newdata=evaldata))
      .crs_set_messages(crs.messages, TRUE)

      ## We again require the mean of the fitted values

      mean.predicted.model.E.mu.w <- mean(predicted.model.E.mu.w)

    }

    norm.raw <- sum(predicted.model.E.mu.w^2) / sum(E.y.w^2)
    norm.stop[N] <- if (penalize.iteration) N * norm.raw else norm.raw
    phi.prime.mat[,N] <- phi.prime
    phi.mat[,N] <- phi
    N.evaluated <- N

    ## The number of iterations in LF is asymptotically equivalent to
    ## 1/alpha (where alpha is the regularization parameter in
    ## Tikhonov).  Plus the criterion function we use is increasing
    ## for very small number of iterations. So we need a threshold
    ## after which we can pretty much confidently say that the
    ## stopping criterion is decreasing.  In Darolles et al. (2011)
    ## \alpha ~ O(N^(-1/(min(beta,2)+2)), where beta is the so called
    ## qualification of your regularization method. Take the worst
    ## case in which beta = 0 and then the number of iterations is ~
    ## N^0.5. Note that derivative estimation seems to require more
    ## iterations hence the heuristic sqrt(N)

    should.break <- FALSE
    if(N > round(sqrt(nrow(traindata))) &&
       !is.monotone.increasing(norm.stop[seq_len(N)])) {
      ## If stopping rule criterion increases or we are below stopping
      ## tolerance then break

      if(stop.on.increase && norm.stop[N] > norm.stop[N-1L]) {
        convergence <- "STOP_ON_INCREASE"
        should.break <- TRUE
      }
      if(abs(norm.stop[N-1L]-norm.stop[N]) < iterate.diff.tol) {
        convergence <- "ITERATE_DIFF_TOL"
        should.break <- TRUE
      }

    }

    if(should.break) break

    ## The current state's residual supplies the adjoint for update N+1.
    ## Do not compute it when state N is the terminal iterate.max state.

    if(N < iterate.max) {
      cdf.weighted.average <- .crsiv_gaussian_integral_apply(
        z.train = z.kernel,
        z.eval = zeval.kernel,
        rhs = predicted.model.E.mu.w,
        bw = bw
      )

      survivor.weighted.average <- mean.predicted.model.E.mu.w - cdf.weighted.average

      T.star.mu <- (survivor.weighted.average-S.z*mean.predicted.model.E.mu.w)/f.z
    }

  }

  norm.stop <- norm.stop[seq_len(N.evaluated)]
  phi.mat <- phi.mat[, seq_len(N.evaluated), drop = FALSE]
  phi.prime.mat <- phi.prime.mat[, seq_len(N.evaluated), drop = FALSE]

  ## Extract minimum, and check for monotone increasing function and
  ## issue warning in that case. Otherwise allow for an increasing
  ## then decreasing (and potentially increasing thereafter) portion
  ## of the stopping function, ignore the initial increasing portion,
  ## and take the min from where the initial inflection point occurs
  ## to the length of norm.stop

  stop.pick <- .crsiv_select_stop_index(norm.stop)
  norm.value <- stop.pick$norm.value

  if(stop.pick$monotone.failure) {
    .crsiv_warn_monotone_increasing(display.warnings)
    convergence <- "FAILURE_MONOTONE_INCREASING"
    #    phi <- starting.values.phi
    #    phi.prime <- starting.values.phi.prime
  }
  N.selected <- stop.pick$index
  phi <- phi.mat[,N.selected]
  phi.prime <- phi.prime.mat[,N.selected]

  set_status()

  .crsiv_warn_iterate_max(display.warnings, N.evaluated, iterate.max)

  .crs_set_messages(crs.messages, FALSE)
  traindata$y <- traindata$y - (fitted(phi.0)-phi)

  model <- run.crs(formula.yz,
               cv="none",
               degree=phi.0$degree,
               segments=phi.0$segments,
               lambda=phi.0$lambda,
               include=phi.0$include,
               kernel=phi.0$kernel,
               basis=phi.0$basis,
               knots=phi.0$knots,
               tau=phi.0$tau,
               deriv=1,
               data=traindata,
               weights=phi.0$weights)
  .crs_set_messages(crs.messages, TRUE)

  model$phi <- phi
  model$phi.prime <- phi.prime
  model$phi.mat <- phi.mat
  model$phi.prime.mat <- phi.prime.mat
  model$num.iterations <- N.selected
  model$norm.stop <- norm.stop
  model$norm.value <- norm.value
  model$convergence <- convergence
  model$starting.values.phi <- starting.values.phi
  model$starting.values.phi.prime <- starting.values.phi.prime
  model$nmulti <- nmulti
  model$ptm <- proc.time() - ptm.start

  ## Attach instrument parameters for summary
  model$degree.w <- degree.w
  model$segments.w <- segments.w
  model$lambda.w <- lambda.w
  model$include.w <- include.w
  model$num.x.w <- num.x.w
  model$num.z.w <- num.z.w
  model$xnames.w <- xnames.w
  model$znames.w <- znames.w

  class(model) <- c("crsivderiv", "crs")

  return(finalize.iv.model(model))

}

print.crsivderiv <- function(x, ...) {
  cat("Call:\n")
  print(.crs_iv_public_call(x))
  invisible(x)
}

summary.crsivderiv <- function(object, ...) {
  ans <- .crs_iv_summary_payload(object, derivative = TRUE)
  class(ans) <- "summary.crsivderiv"
  ans
}

plot.crsivderiv <- function(x,
                            plot.data = FALSE,
                            phi = FALSE,
                            ...) {

  object <- x
  .crs_plot_iv_deriv_public(
    object = object,
    plot.call = match.call(expand.dots = FALSE),
    plot.data = plot.data,
    phi = phi,
    ...
  )
}
