! Copyright © 2021-2026 Dynare Team
!
! This file is part of Dynare.
!
! Dynare is free software: you can redistribute it and/or modify
! it under the terms of the GNU General Public License as published by
! the Free Software Foundation, either version 3 of the License, or
! (at your option) any later version.
!
! Dynare is distributed in the hope that it will be useful,
! but WITHOUT ANY WARRANTY; without even the implied warranty of
! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
! GNU General Public License for more details.
!
! You should have received a copy of the GNU General Public License
! along with Dynare.  If not, see <https://www.gnu.org/licenses/>.
!
!  input:
!       order    the order of approximation, needs order+1 derivatives
!       nstat
!       npred
!       nboth
!       nforw
!       nexog
!       ystart   starting value (full vector of endogenous)
!       shocks   matrix of shocks (nexog x number of period)
!       ysteady  full vector of decision rule's steady
!       dr       structure containing matrices of derivatives (g_0, g_1,…)
!       pruning  boolean stating whether the simulation should be pruned
!  output:
!       res      simulated results

subroutine mexFunction(nlhs, plhs, nrhs, prhs) bind(c, name='mexFunction')
   use iso_c_binding
   use struct
   use simulation
   implicit none (type, external)

   type(c_ptr), dimension(*), intent(in) :: prhs
   type(c_ptr), dimension(*), intent(out) :: plhs
   integer(c_int), intent(in), value :: nlhs, nrhs
   type(c_ptr) :: order_mx, nstatic_mx, npred_mx, nboth_mx, nfwrd_mx, &
                 &nexog_mx, ystart_mx, shocks_mx, ysteady_mx, dr_mx, &
                 &pruning_mx
   integer :: order, nstatic, npred, nboth, nfwrd, exo_nbr, endo_nbr, nys, nvar, nper
   real(real64), dimension(:), allocatable :: dy
   real(real64), pointer, contiguous :: ysteady(:), ystart(:), sim(:,:), shocks(:,:)
   logical :: pruning

   order_mx = prhs(1)
   nstatic_mx = prhs(2)
   npred_mx = prhs(3)
   nboth_mx = prhs(4)
   nfwrd_mx = prhs(5)
   nexog_mx = prhs(6)
   ystart_mx = prhs(7)
   shocks_mx = prhs(8)
   ysteady_mx = prhs(9)
   dr_mx = prhs(10)
   pruning_mx = prhs(11)

   ! Checking the consistence and validity of input arguments
   if (nrhs /= 11 .or. nlhs /= 1) &
        call mexErrMsgTxt("Must have exactly 11 inputs and 1 output")
   if (.not. mxIsScalar(order_mx) .and. mxIsNumeric(order_mx)) &
        call mexErrMsgTxt("1st argument (order) should be a numeric scalar")
   if (.not. mxIsScalar(nstatic_mx) .and. mxIsNumeric(nstatic_mx)) &
        call mexErrMsgTxt("2nd argument (nstat) should be a numeric scalar")
   if (.not. mxIsScalar(npred_mx) .and. mxIsNumeric(npred_mx)) &
        call mexErrMsgTxt("3rd argument (npred) should be a numeric scalar")
   if (.not. mxIsScalar(nboth_mx) .and. mxIsNumeric(nboth_mx)) &
        call mexErrMsgTxt("4th argument (nboth) should be a numeric scalar")
   if (.not. mxIsScalar(nfwrd_mx) .and. mxIsNumeric(nfwrd_mx)) &
        call mexErrMsgTxt("5th argument (nforw) should be a numeric scalar")
   if (.not. mxIsScalar(nexog_mx) .and. mxIsNumeric(nexog_mx)) &
        call mexErrMsgTxt("6th argument (nexog) should be a numeric scalar")
   if (.not. (mxIsDouble(ystart_mx) .and. (mxGetM(ystart_mx) == 1 .or. mxGetN(ystart_mx) == 1)) &
        .or. mxIsComplex(ystart_mx) .or. mxIsSparse(ystart_mx)) &
        call mexErrMsgTxt("7th argument (ystart) should be a real dense vector")
   if (.not. mxIsDouble(shocks_mx) .or. mxIsComplex(shocks_mx) .or. mxIsSparse(shocks_mx)) &
        call mexErrMsgTxt("8th argument (shocks) should be a real dense matrix")
   if (.not. (mxIsDouble(ysteady_mx) .and. (mxGetM(ysteady_mx) == 1 .or. mxGetN(ysteady_mx) == 1)) &
        .or. mxIsComplex(ysteady_mx) .or. mxIsSparse(ysteady_mx)) &
        call mexErrMsgTxt("9th argument (ysteady) should be a real dense vector")
   if (.not. mxIsStruct(dr_mx)) &
        call mexErrMsgTxt("10th argument (dr) should be a struct")
   if (.not. mxIsLogicalScalar(pruning_mx)) &
        call mexErrMsgTxt("11th argument (pruning) should be a logical scalar")

   ! Converting inputs in Fortran format
   order = int(mxGetScalar(order_mx))
   nstatic = int(mxGetScalar(nstatic_mx))
   npred = int(mxGetScalar(npred_mx))
   nboth = int(mxGetScalar(nboth_mx))
   nfwrd = int(mxGetScalar(nfwrd_mx))
   exo_nbr = int(mxGetScalar(nexog_mx))
   endo_nbr = nstatic+npred+nboth+nfwrd
   nys = npred+nboth
   pruning = mxGetScalar(pruning_mx) == 1._c_double
   nvar = nys+exo_nbr

   if (endo_nbr /= int(mxGetM(ystart_mx))) &
        call mexErrMsgTxt("ystart should have nstat+npred+nboth+nforw rows")
   ystart => mxGetDoubles(ystart_mx)

   if (exo_nbr /= int(mxGetM(shocks_mx))) call mexErrMsgTxt("shocks should have nexog rows")
   nper = int(mxGetN(shocks_mx))
   shocks(1:exo_nbr,1:nper) => mxGetDoubles(shocks_mx)

   if (.not. (int(mxGetM(ysteady_mx)) == endo_nbr)) &
        call mexErrMsgTxt("ysteady should have nstat+npred+nboth+nforw rows")
   ysteady => mxGetDoubles(ysteady_mx)
   ! Initial value for between the states' starting value and the states'
   ! steady-state value
   dy = ystart(nstatic+1:nstatic+nys)-ysteady(nstatic+1:nstatic+nys)

   if (pruning) then
      dr_mx = mxGetField(dr_mx, 1_mwIndex, "pruning")
      if (.not. mxIsStruct(dr_mx)) call mexErrMsgTxt("dr.pruning should be a struct")
   end if

   ! Generating output
   plhs(1) = mxCreateDoubleMatrix(int(endo_nbr, mwSize), int(nper+1, mwSize), mxREAL)
   sim(1:endo_nbr,1:(nper+1)) => mxGetDoubles(plhs(1))
   sim(:,1) = ystart

   if (pruning) then
      call simulate_pruning(sim, dr_mx, ysteady, dy, shocks, order, nstatic, nvar)
   else
      call simulate(sim, dr_mx, ysteady, dy, shocks, order, nstatic, nvar)
   end if

end subroutine mexFunction
