MODULE moduleSolver
  USE moduleAverage

  !Generic type for pusher of particles
  TYPE, PUBLIC:: pusherGeneric
    PROCEDURE(push_interafece), POINTER, NOPASS:: pushParticle => NULL()
    !Boolean to indicate if the species is moved in the iteration
    LOGICAL:: pushSpecies
    !How many interations between advancing the species
    INTEGER:: every
    CONTAINS
      PROCEDURE, PASS:: init => initPusher

  END TYPE pusherGeneric

  !Generic type for solver
  TYPE, PUBLIC:: solverGeneric
    TYPE(pusherGeneric), ALLOCATABLE:: pusher(:)
    TYPE(averageGeneric), ALLOCATABLE:: averageScheme
    PROCEDURE(solveEM_interface), POINTER, NOPASS:: solveEM => NULL()
    PROCEDURE(weightingScheme_interface), POINTER, NOPASS:: weightingScheme => NULL()
    CONTAINS
      PROCEDURE, PASS:: initEM
      PROCEDURE, PASS:: initWS
      PROCEDURE, PASS:: updateParticleCell
      PROCEDURE, PASS:: updatePushSpecies

  END TYPE solverGeneric

  INTERFACE
    !Push a particle
    PURE SUBROUTINE push_interafece(part, tauIn)
      USE moduleSpecies

      TYPE(particle), INTENT(inout):: part
      REAL(8), INTENT(in):: tauIn

    END SUBROUTINE push_interafece

    !Solve the electromagnetic field
    SUBROUTINE solveEM_interface()
    
    END SUBROUTINE solveEM_interface

    !Apply nonAnalogue scheme to a particle
    SUBROUTINE weightingScheme_interface(part, cellOld, cellNew)
      USE moduleSpecies
      USE moduleMesh
      IMPLICIT NONE

      TYPE(particle), INTENT(inout):: part
      CLASS(meshCell), POINTER, INTENT(in):: cellOld
      CLASS(meshCell), POINTER, INTENT(inout):: cellNew

    END SUBROUTINE weightingScheme_interface

  END INTERFACE

  TYPE(solverGeneric):: solver

  CONTAINS
    !Init Pusher
    SUBROUTINE initPusher(self, pusherType, tau, tauSp)
      USE moduleErrors
      USE modulePusher
      USE moduleMesh, ONLY: mesh
      IMPLICIT NONE

      CLASS(pusherGeneric), INTENT(out):: self
      CHARACTER(:), ALLOCATABLE:: pusherType
      REAL(8):: tau, tauSp

      SELECT CASE(mesh%dimen)
      CASE(0)
        self%pushParticle => push0D

      CASE(1:3)
        SELECT CASE(mesh%geometry)
        CASE ('Cart')
          SELECT CASE(pusherType)
          CASE('Neutral')
            self%pushParticle => pushCartNeutral

          CASE('Electrostatic')
            self%pushParticle => pushCartElectrostatic

          CASE('Electromagnetic')
            self%pushParticle => pushCartElectromagnetic

          CASE DEFAULT
            CALL criticalError('Pusher ' // pusherType // ' not found for Cart','initPusher')

          END SELECT

        CASE('Cyl')
          SELECT CASE(pusherType)
          CASE('Neutral')
            self%pushParticle => push2DCylNeutral

          CASE('Electrostatic')
            self%pushParticle => push2DCylElectrostatic

          CASE DEFAULT
            CALL criticalError('Pusher ' // pusherType // ' not found for Cyl','initPusher')

          END SELECT

        CASE('Rad')
          SELECT CASE(pusherType)
          CASE('Neutral')
            self%pushParticle => push1DRadNeutral

          CASE('Electrostatic')
            self%pushParticle => push1DRadElectrostatic

          CASE DEFAULT
            CALL criticalError('Pusher ' // pusherType // ' not found for Rad','initPusher')

          END SELECT

        END SELECT

      END SELECT

      self%pushSpecies = .FALSE.
      self%every = INT(tauSp/tau)

    END SUBROUTINE initPusher

    SUBROUTINE initEM(self, EMType)
      USE moduleEM
      USE moduleErrors, only: criticalError
      IMPLICIT NONE

      CLASS(solverGeneric), INTENT(inout):: self
      CHARACTER(:), ALLOCATABLE:: EMType

      SELECT CASE(EMType)
      CASE('Electrostatic','ConstantB')
        self%solveEM => solveElecField

      CASE('ElectrostaticBoltzmann')
        self%solveEM => solveElecFieldBoltzmann

      CASE DEFAULT
        CALL criticalError('EM Solver ' // EMType // ' not found', 'readSolver')

      END SELECT

    END SUBROUTINE initEM

    !Initialize the non-analogue scheme
    SUBROUTINE initWS(self, NAType)
      USE moduleMesh
      IMPLICIT NONE

      CLASS(solverGeneric), INTENT(inout):: self
      CHARACTER(:), ALLOCATABLE:: NAType

      SELECT CASE(NAType)
      CASE ('Volume')
        self%weightingScheme => volumeWScheme

      END SELECT

    END SUBROUTINE initWS
    
    !Do all pushes for particles
    SUBROUTINE doPushes()
      USE moduleSpecies
      USE moduleMesh
      IMPLICIT NONE

      INTEGER:: n
      INTEGER:: sp

      !$OMP DO
      DO n = 1, nPartOld
        !Select species type
        sp = partOld(n)%species%n
        !Checks if the species sp is update this iteration
        IF (solver%pusher(sp)%pushSpecies) THEN
          !Push particle
          CALL solver%pusher(sp)%pushParticle(partOld(n), tau(sp))
          !Find cell in wich particle reside
          CALL solver%updateParticleCell(partOld(n))

        END IF

      END DO
      !$OMP END DO

    END SUBROUTINE doPushes

    !Takes the particles from a list and put them into an array
    !nStart indicates the last fill index in the array
    SUBROUTINE resetList(partList, partArray, nStart)
      USE moduleSpecies
      USE moduleList
      USE moduleProbe
      IMPLICIT NONE

      TYPE(listNode), INTENT(inout):: partList
      TYPE(particle), INTENT(inout), DIMENSION(1:):: partArray
      INTEGER, INTENT(in):: nStart
      TYPE(lNode), POINTER:: partCurr, partNext
      INTEGER:: n

      partCurr => partList%head
      DO n = 1, partList%amount
       partNext => partCurr%next
       partArray(nStart + n) = partCurr%part
       IF (nProbes > 0) THEN
         CALL doProbes(partArray(nStart+n))

       END IF
       DEALLOCATE(partCurr)
       partCurr => partNext

      END DO

      !Resest head, tail and number of nodes in the list
      IF (ASSOCIATED(partList%head)) NULLIFY(partList%head)
      IF (ASSOCIATED(partList%tail)) NULLIFY(partList%tail)
      partList%amount = 0

    END SUBROUTINE resetList

    SUBROUTINE doReset()
      USE moduleSpecies
      USE moduleMesh
      USE moduleList
      USE moduleProbe
      IMPLICIT NONE

      INTEGER:: nn, n, e
      INTEGER, SAVE:: nPartNew
      INTEGER, SAVE:: nOldIn, nInj, nWScheme, nCollisions, nSurfaces
      TYPE(particle), ALLOCATABLE, SAVE:: partTemp(:)
      INTEGER:: s

      !$OMP SECTIONS
      !$OMP SECTION
      nOldIn = 0
      IF (ALLOCATED(partOld)) THEN
        nOldIn = COUNT(partOld%n_in)

      END IF

      !$OMP SECTION
      nInj = partInj%amount

      !$OMP SECTION
      nWScheme = partWScheme%amount

      !$OMP SECTION
      nCollisions = partCollisions%amount

      !$OMP SECTION
      nSurfaces = partSurfaces%amount

      !$OMP END SECTIONS

      !$OMP BARRIER

      !$OMP SINGLE
      CALL MOVE_ALLOC(partOld, partTemp)
      nPartNew = nInj + nOldIn + nWScheme + nCollisions + nSurfaces
      ALLOCATE(partOld(1:nPartNew))
      !$OMP END SINGLE

      !$OMP SECTIONS
      !$OMP SECTION
      !Reset particles from previous iteration
      nn = 0
      DO n = 1, nPartOld
        IF (partTemp(n)%n_in) THEN
          nn = nn + 1
          partOld(nn) = partTemp(n)
          IF (nProbes > 0) THEN
            CALL doProbes(partOld(nn))

          END IF

        END IF

      END DO

      !$OMP SECTION
      !Reset particles from injection
      nn = nOldIn
      call resetList(partInj, partOld, nn)

      !$OMP SECTION
      !Reset particles from weighting scheme
      nn = nOldIn + nInj
      CALL resetList(partWScheme, partOld, nn)

      !$OMP SECTION
      !Reset particles from collisional process
      nn = nOldIn + nInj + nWScheme
      CALL resetList(partCollisions, partOld, nn)

      !$OMP SECTION
      !Reset particles from surface process
      nn = nOldIn + nInj + nWScheme + nCollisions
      CALL resetList(partSurfaces, partOld, nn)

      !$OMP SECTION
      !Reset output in nodes
      DO e = 1, mesh%numNodes
        CALL mesh%nodes(e)%obj%resetOutput()

      END DO

      !$OMP SECTION
      !Erase the list of particles inside the cell if particles have been pushed
      IF (listInCells) THEN
        DO s = 1, nSpecies
          DO e = 1, mesh%numCells
            IF (solver%pusher(s)%pushSpecies) THEN 
              CALL mesh%cells(e)%obj%listPart_in(s)%erase()
              mesh%cells(e)%obj%totalWeight(s) = 0.D0

            END IF

          END DO

        END DO

      END IF

      !$OMP SECTION
      !Erase the list of particles inside the cell in coll mesh
      IF (doubleMesh) THEN
        DO s = 1, nSpecies
          DO e = 1, meshColl%numCells
            IF (solver%pusher(s)%pushSpecies) THEN 
              CALL meshColl%cells(e)%obj%listPart_in(s)%erase()
              meshColl%cells(e)%obj%totalWeight(s) = 0.D0

            END IF

          END DO

        END DO
      
      END IF

      !$OMP END SECTIONS

      !$OMP SINGLE
      nPartOld = nPartNew
      !$OMP END SINGLE

    END SUBROUTINE doReset

    !Scatter particles in the grid
    SUBROUTINE doScatter
      USE moduleSpecies
      USE moduleMesh
      IMPLICIT NONE

      INTEGER:: n
      CLASS(meshCell), POINTER:: cell

      !Loops over the particles to scatter them
      !$OMP DO
      DO n = 1, nPartOld
        cell => mesh%cells(partOld(n)%cell)%obj
        CALL cell%scatter(cell%nNodes, partOld(n))

      END DO
      !$OMP END DO

    END SUBROUTINE doScatter

    SUBROUTINE doEMField()
      IMPLICIT NONE

      IF (ASSOCIATED(solver%solveEM)) THEN
        CALL solver%solveEM()

      END IF

    END SUBROUTINE doEMField

    !Split particles as a function of cell volume and splits particle
    SUBROUTINE volumeWScheme(part, cellOld, cellNew)
      USE moduleSpecies
      USE moduleMesh
      USE moduleRandom
      IMPLICIT NONE

      TYPE(particle), INTENT(inout):: part
      CLASS(meshCell), POINTER, INTENT(in):: cellOld
      CLASS(meshCell), POINTER, INTENT(inout):: cellNew
      REAL(8):: fractionVolume, pSplit

      !If particle changes volume to smaller cell
      IF (cellOld%volume > cellNew%volume .AND. &
          part%weight >= part%species%weight*1.0D-1) THEN
        fractionVolume = cellOld%volume/cellNew%volume

        !Calculate probability of splitting particle
        pSplit = 1.D0 - DEXP(-fractionVolume*1.0D-1)

        IF (random() < pSplit) THEN
          !Split particle in two
          CALL splitParticle(part, 2, cellNew)

        END IF

      END IF

    END SUBROUTINE volumeWScheme

    !Subroutine to split the particle 'part' into a number 'nSplit' of particles.
    !'nSplit-1' particles are added to the partNAScheme list
    SUBROUTINE splitParticle(part, nSplit, cell)
      USE moduleSpecies
      USE moduleList
      USE moduleMesh
      USE OMP_LIB
      IMPLICIT NONE

      TYPE(particle), INTENT(inout):: part
      INTEGER, INTENT(in):: nSplit
      CLASS(meshCell), INTENT(inout):: cell
      REAL(8):: newWeight
      TYPE(particle), POINTER:: newPart
      INTEGER:: p
      INTEGER:: sp

      newWeight = part%weight / nSplit

      !Assign new weight to original particle
      part%weight = newWeight
      
      !Add new particles to list of NA particles
      DO p = 2, nSplit
        !Allocate the pointer for the new particles
        ALLOCATE(newPart)
        !Copy data from original particle
        newPart = part
        !Add particle to list of new particles from weighting scheme
        CALL partWScheme%setLock()
        CALL partWScheme%add(newPart)
        CALL partWScheme%unsetLock()
        !Add particle to cell list
        sp = part%species%n
        IF (listInCells) THEN
          CALL OMP_SET_lock(cell%lock)
          CALL cell%listPart_in(sp)%add(newPart)
          CALL OMP_UNSET_lock(cell%lock)

        END IF

      END DO

    END SUBROUTINE splitParticle

    !Injection of particles
    SUBROUTINE doInjects()
      USE moduleSpecies
      use moduleInject
      IMPLICIT NONE

      INTEGER:: i, sp
      
      DO i=1, nInject
        associate(inject => injects(i)%obj)
        sp = inject%species%n
        IF (solver%pusher(sp)%pushSpecies) THEN 

          CALL inject%addParticles()

        END IF
        end associate

      END DO

    END SUBROUTINE doInjects

    SUBROUTINE updateParticleCell(self, part)
      USE moduleSpecies
      USE moduleMesh
      IMPLICIT NONE

      CLASS(solverGeneric), INTENT(in):: self
      TYPE(particle), INTENT(inout):: part
      CLASS(meshCell), POINTER:: cellOld, cellNew

      !Assume that particle is outside the domain
      part%n_in = .FALSE.

      cellOld => mesh%cells(part%cell)%obj
      CALL cellOld%findCell(part)
      CALL findCellColl(part)
      !Call the NA shcme
      IF (ASSOCIATED(self%weightingScheme)) THEN
        cellNew => mesh%cells(part%cell)%obj
        CALL self%weightingScheme(part, cellOld, cellNew)

      END IF

    END SUBROUTINE updateParticleCell

    !Update the information about if a species needs to be moved this iteration
    SUBROUTINE updatePushSpecies(self)
      USE moduleSpecies
      USE moduleCaseparam, ONLY: timeStep
      IMPLICIT NONE

      CLASS(solverGeneric), INTENT(inout):: self
      INTEGER:: s

      DO s=1, nSpecies
        self%pusher(s)%pushSpecies = MOD(timeStep, self%pusher(s)%every) == 0

      END DO

    END SUBROUTINE updatePushSpecies

    !Output the different data and information
    SUBROUTINE doOutput()
      USE moduleMesh
      USE moduleOutput
      USE moduleSpecies
      USE moduleCompTime
      USE moduleProbe
      use moduleInject, only: writeInjects
      USE moduleCaseParam, ONLY: timeStep
      IMPLICIT NONE

      CALL outputProbes()

      counterOutput = counterOutput + 1
      IF (counterOutput >= triggerOutput .OR. &
          timeStep == tFinal .OR. timeStep == tInitial) THEN

        !Resets output counter
        counterOutput=0

        CALL mesh%printOutput()
        IF (ASSOCIATED(meshForMCC)) CALL meshForMCC%printColl()
        CALL mesh%printEM()

        ! Output of boundaries
        call boundariesParticle_write()
        call boundariesEM_write()

        ! Output of inejcts
        call writeInjects()

        WRITE(*, "(5X,A21,I10,A1,I10)") "t/tFinal: ", timeStep, "/", tFinal
        WRITE(*, "(5X,A21,I10)")        "Particles: ", nPartOld
        IF (timeStep == 0) THEN
          WRITE(*, "(5X,A21,F8.1,A2)")   "     init time: ",    1.D3*tStep, "ms"

        ELSE
          WRITE(*, "(5X,A21,F8.1,A2)")   "iteration time: ",    1.D3*tStep, "ms"

        END IF

        IF (nPartOld > 0) THEN
          WRITE(*, "(5X,A21,F8.1,A2)") "avg t/particle: ", 1.D9*tStep/DBLE(nPartOld), "ns"

        END IF
        WRITE(*,*)

      END IF

      counterCPUTime = counterCPUTime + 1
      IF (counterCPUTime >= triggerCPUTime .OR. &
          timeStep == tFinal .OR. timeStep == tInitial) THEN

        !Reset CPU Time counter
        counterCPUTime = 0

        CALL writeTime(timeStep == 0)

      END IF

      !Output average values
      IF (useAverage .AND. timeStep == tFinal) THEN
        CALL mesh%printAverage()

      END IF

    END SUBROUTINE doOutput

    SUBROUTINE doAverage()
      USE moduleAverage
      USE moduleMesh
      IMPLICIT NONE

      INTEGER:: tAverage, n

      IF (useAverage) THEN
        tAverage = timeStep - tAverageStart

        IF (tAverage == 1) THEN
          !First iteration in which average scheme is used
          DO n = 1, mesh%numNodes
            CALL averageScheme(n)%firstAverage(mesh%nodes(n)%obj%output)

          END DO
        ELSEIF (tAverage > 1) THEN
          DO n = 1, mesh%numNodes
            CALL averageScheme(n)%updateAverage(mesh%nodes(n)%obj%output, tAverage)

          END DO

        END IF

      END IF

    END SUBROUTINE doAverage


END MODULE moduleSolver

