! Handle command line argument parsing
module MW_command
   implicit none
   private

   public :: MW_command_t
   public :: setup_parser
   public :: parse
   public :: get_option

   public :: ARGUMENT_VALUE_LEN

   integer, parameter :: LONG_OPTION_LEN = 32
   integer, parameter :: OPTION_VALUE_LEN = 64
   integer, parameter :: ARGUMENT_VALUE_LEN = 512
   integer, parameter :: OPTION_ACTION_LEN = 11

   character(OPTION_ACTION_LEN), parameter :: OPTION_ACTION_STORE       = "store"
   character(OPTION_ACTION_LEN), parameter :: OPTION_ACTION_STORE_TRUE  = "store_true"
   character(OPTION_ACTION_LEN), parameter :: OPTION_ACTION_COUNT       = "count"

   type MW_command_t

      ! options
      integer :: num_options
      character(1),                allocatable  :: shortn(:)       ! short options names
      character(LONG_OPTION_LEN),  allocatable  :: longn(:)        ! long options names
      character(OPTION_ACTION_LEN), allocatable :: actions(:)     ! options actions
      integer,                      allocatable :: counts(:)      ! count the number of appearance
      character(OPTION_VALUE_LEN), allocatable  :: values(:)       ! parsed option values
      logical,                     allocatable  :: values_stored(:) ! flag if value was set

      ! positional arguments
      integer :: num_arguments
      character(ARGUMENT_VALUE_LEN), allocatable :: arguments(:) ! parsed positional argument

      ! show help
      procedure(MW_command_help), pointer, nopass :: show_help => NULL()
   end type MW_command_t

   interface get_option
      module procedure get_option_log
      module procedure get_option_int
      module procedure get_option_char
      module procedure get_option_real
      module procedure get_option_double
   end interface get_option

contains

   !================================================================================
   ! Setup the command line parser for Metalwalls
   !
   ! -h, --help     show this help message and exit
   ! -v, --version  show program's version number and exit
   subroutine setup_parser(this)
      implicit none
      type(MW_command_t), intent(inout) :: this

      integer :: num_options = 5
      integer :: num_arguments = 0

      call set_num_options(this, num_options)

      ! Option definitions
      ! call add_option(this, short_opt, long_opt, action_opt, default_val)
      ! -h, --help     show this help message and exit
      call add_option(this, "h", "help", OPTION_ACTION_STORE_TRUE)
      ! -v, --version  show program's version number and exit
      call add_option(this, "v", "version", OPTION_ACTION_STORE_TRUE)
      ! -i, --input    set path to input file
      call add_option(this, "i", "input", OPTION_ACTION_STORE, "runtime.inpt")
      ! -d, --data    set path to data input file
      call add_option(this, "d", "data", OPTION_ACTION_STORE, "data.inpt")
      ! --output-rank set which ranks do output
      call add_option(this, long_opt="output-rank", action_opt=OPTION_ACTION_STORE, default_val="root")

      if (num_arguments > 0) call set_num_arguments(this, num_arguments)
      this%show_help => MW_command_help

   end subroutine setup_parser

   !================================================================================
   ! Print help message to ounit
   subroutine MW_command_help(ounit)
      implicit none
      ! Parameters
      ! ----------
      integer, intent(in) :: ounit

      write(ounit,'("Usage: metalwalls [options]")')
      write(ounit,*)
      write(ounit,'("Mandatory arguments to long options are mandatory for short options too.")')
      write(ounit,'("Options:")')
      write(ounit,'("  -h, --help                 show this message and exit")')
!      write(ounit,'("  -i, --input=FILE           set FILE as runtime input file")')
      write(ounit,'("  -v, --version              show program''s version number and exit")')
      write(ounit,'("      --output-rank=VALUE    enables output on some of the ranks")')
      write(ounit,'("                             possible VALUE are:")')
      write(ounit,'("                               root     - only rank 0 performs output (default)")')
      write(ounit,'("                               all      - all mpi processes perform output")')
      write(ounit,'("                               r1[,r2]* - comma separated list of rank ids which perform output")')

   end subroutine MW_command_help

   !================================================================================
   ! Set the number of options to be stored in the parser
   subroutine set_num_options(this, num_options)
      use MW_errors, only: MW_errors_parameter_error => parameter_error, &
            MW_errors_allocate_error => allocate_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_command_t), intent(inout) :: this
      integer,            intent(in)  :: num_options

      integer :: ierr
      integer :: i

      if (num_options <= 0) then
         call MW_errors_parameter_error("set_num_options", "command.f90", &
               "num_options", num_options)
      end if

      allocate(this%shortn(num_options), this%longn(num_options), &
            this%actions(num_options), this%counts(num_options), &
            this%values(num_options), this%values_stored(num_options), stat=ierr)
      if (ierr /= 0) then
         call MW_errors_allocate_error("set_num_options", "command.f90", ierr)
      end if

      this%num_options = 0
      do i = 1, num_options
         this%shortn(i)         = ""
         this%longn(i)          = ""
         this%actions(i)       = ""
         this%counts(i)        = 0
         this%values(i)        = ""
         this%values_stored(i) = .false.
      end do

   end subroutine set_num_options

   !================================================================================
   ! Set the number of potisional arguments to be stored in the parser
   subroutine set_num_arguments(this, num_arguments)
      use MW_errors, only: MW_errors_parameter_error => parameter_error, &
            MW_errors_allocate_error => allocate_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_command_t), intent(inout) :: this
      integer,                   intent(in)  :: num_arguments

      integer :: ierr
      integer :: i

      if (num_arguments <= 0) then
         call MW_errors_parameter_error("set_num_options", "command.f90", &
               "num_arguments", num_arguments)
      end if

      allocate(this%arguments(num_arguments), stat=ierr)
      if (ierr /= 0) then
         call MW_errors_allocate_error("set_num_arguments", "command.f90", ierr)
      end if

      this%num_arguments = 0
      do i = 1, num_arguments
         this%arguments(i) = ""
      end do

   end subroutine set_num_arguments

   !================================================================================
   ! Add an option to the parser
   subroutine add_option(this, short_opt, long_opt, action_opt, default_val)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      ! Parameters
      ! ----------
      type(MW_command_t), intent(inout) :: this
      character(*),              intent(in), optional :: short_opt   ! option short name
      character(*),              intent(in), optional :: long_opt    ! option long  name
      character(*),              intent(in), optional :: action_opt  ! option action
      character(*),              intent(in), optional :: default_val ! default value

      character(1) :: short
      character(LONG_OPTION_LEN) :: long
      character(OPTION_ACTION_LEN) :: action
      character(OPTION_VALUE_LEN) :: val
      integer :: i

      short = ""
      if (present(short_opt)) then
         ! short_opt must be only 1 character
         if (len_trim(adjustl(short_opt)) /= 1) then
            call MW_errors_parameter_error("add_option", "command.f90", &
                  "short_opt", short_opt)
         end if
         short = adjustl(short_opt)
      end if

      long  = ""
      if (present(long_opt)) then
         ! long_opt must be more than 1 character
         if (len_trim(adjustl(long_opt)) < 2) then
            call MW_errors_parameter_error("add_option", "command.f90", &
                  "long_opt", long_opt)
         end if
         long = long_opt
      end if

      action = OPTION_ACTION_STORE
      if (present(action_opt)) action = action_opt

      val = ""
      if (present(default_val)) val = default_val

      ! Check that at least one of the two option is provided...
      if ((short == "") .and. (long == "")) then
         call MW_errors_parameter_error("add_option", "command.f90", &
               "short_opt, long_opt", short_opt // ", " // long_opt)
      end if

      ! Check that there is enough space to store this options
      if (this%num_options >= size(this%shortn)) then
         call MW_errors_parameter_error("add_option", "command.f90", &
               "num_options", this%num_options)
      end if

      ! Add the option in the list
      this%num_options = this%num_options + 1
      i = this%num_options
      this%shortn(i)   = short
      this%longn(i)    = long
      this%actions(i) = action
      this%counts(i)  = 0
      this%values(i)  = val
      this%values_stored(i) = .false.

   end subroutine add_option

   !================================================================================
   ! Parse the command line
   !
   ! useful intrinsic functions/subroutines
   !
   ! command_argument_count(), inquiry function
   !   The result is the number of command arguments used to invoke the execution
   !   of the program containing the reference to this function. If the processor
   !   does not support command arguments, the result is zero. If the processor
   !   has a concept of command name, the count does not include the command name.
   !
   ! get_command({command, length, status}), subroutine
   !   Obtain the entire command initiating the program
   !
   ! get_command_argument(number{, value, length, status}), subroutine
   !   Obtain a specified command argument
   !
   subroutine parse(this, arg_string_in)
      use MW_errors, only: MW_errors_allocate_error => allocate_error, &
            MW_errors_runtime_error => runtime_error
      implicit none
      type(MW_command_t), intent(inout) :: this
      character(len=*), intent(in), optional :: arg_string_in(0:)
      integer :: ierr
      integer :: i
      integer :: arg_len, max_len
      integer :: arg_count
      logical :: positional_argument

      character(ARGUMENT_VALUE_LEN), allocatable :: arg_string(:)

      if (present(arg_string_in)) then
         arg_count = ubound(arg_string_in,1)
         max_len = len(arg_string_in(0))
      else
         arg_count = command_argument_count()
         ! Determine argument maximum length
         max_len = 0
         do i = 0, arg_count
            call get_command_argument(i, length=arg_len)
            if (max_len < arg_len) max_len = arg_len
         end do
      end if

      ! Allocate array to store command line arguments
      allocate(arg_string(0:arg_count), stat=ierr)
      if (ierr /= 0) then
         call MW_errors_allocate_error("parse", "command.f90", ierr)
      end if

      if (present(arg_string_in)) then
         do i = 0, arg_count
            arg_string(i) = arg_string_in(i)
         end do
      else
         ! Read command line arguments
         do i = 0, arg_count
            call get_command_argument(i,value=arg_string(i), status=ierr)
            if (ierr /= 0) then
               call MW_errors_runtime_error("parse", "command.f90", &
                     "Failed to retrieve command line arguments")
            end if
         end do
      end if

      ! Parse the command lines
      i = 1
      do while (i <= arg_count)

         arg_len = len_trim(arg_string(i))
         if (arg_string(i)(1:2) == "--") then
            call parse_long_option(this, arg_string(i)(3:arg_len))
         else if (arg_string(i)(1:1) == "-") then
            call parse_short_option(this, arg_string(i)(2:arg_len))
         else
            !  it is either the value of the preceding option or a positional argument
            positional_argument = .true.

            ! Check if last option read needs a value
            if (this%num_options > 0) then
               if ((this%actions(this%num_options) == OPTION_ACTION_STORE) .and. &
                     (.not. this%values_stored(this%num_options))) then
                  this%values(i) = arg_string(i)(1:arg_len)
                  positional_argument = .false.
               end if
            end if

            if (positional_argument) then
               if (this%num_arguments >= size(this%arguments)) then
                  call MW_errors_runtime_error("parse", "command.f90", &
                        "Too many positional arguments on command line.")
               end if
               this%num_arguments = this%num_arguments + 1
               this%arguments(this%num_arguments) = arg_string(i)
            end if
         end if

         i = i + 1
      end do

   end subroutine parse

   !================================================================================
   ! Parse a long option on the command line
   !
   ! Long option format is
   ! --long-option [VALUE]
   ! --long-option[=VALUE]
   !
   subroutine parse_long_option(this, opt_string)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none

      type(MW_command_t), intent(inout) :: this
      character(*),              intent(in)    :: opt_string

      character(LONG_OPTION_LEN) :: opt_name
      character(OPTION_VALUE_LEN) :: opt_value

      integer :: i
      integer :: iequal
      logical :: found_it

      ! Check if '=' is in the string
      iequal = index(opt_string, "=")
      if (iequal > 0) then
         opt_name = opt_string(:iequal-1)
         opt_value = opt_string(iequal+1:)
      else
         opt_name = opt_string
         opt_value = ""
      end if

      ! Look for option string
      found_it = .false.
      do i = 1, this%num_options
         if (opt_name == this%longn(i)) then
            found_it = .true.
            this%counts(i) = this%counts(i) + 1
            if (this%actions(i) == OPTION_ACTION_STORE) then
               if (iequal > 0) then
                  this%values(i) = opt_value
                  this%values_stored(i) = .true.
               else
                  this%values_stored(i) = .false.
               end if
            end if
            exit
         end if
      end do

      if (.not. found_it) then
         call MW_errors_parameter_error("parse_long_option", "command.f90", &
               "opt_string", opt_string)
      end if

   end subroutine parse_long_option

   !================================================================================
   ! Parse a long option on the command line
   !
   ! short option format is
   ! -a[b..][cVALUE]
   subroutine parse_short_option(this, opt_string)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      type(MW_command_t), intent(inout) :: this
      character(*),              intent(in)    :: opt_string

      character(1)                :: opt_name

      integer :: i, ichar
      logical :: found_it

      ichar = 1
      do while (ichar <= len(opt_string))
         opt_name = opt_string(ichar:ichar)

         ! Look for option string
         found_it = .false.
         do i = 1, this%num_options
            if (opt_name == this%shortn(i)) then
               found_it = .true.
               this%counts(i) = this%counts(i) + 1
               if (this%actions(i) == OPTION_ACTION_STORE) then
                  if (ichar < len(opt_string)) then
                     this%values(i) = opt_string(ichar+1:)
                     this%values_stored(i) = .true.
                     ichar = len(opt_string) + 1
                  else
                     this%values_stored(i) = .false.
                  end if
               end if
               exit
            end if
         end do

         if (.not. found_it) then
            call MW_errors_parameter_error("parse_short_option", "command.f90", &
                  "opt_string", opt_string)
         end if

         ichar = ichar + 1
      end do
   end subroutine parse_short_option

   !================================================================================
   ! Check wether an option was present on the command line or not
   subroutine get_option_log(this, name, value_log)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      type (MW_command_t), intent(in)  :: this
      character(*),               intent(in)  :: name
      logical,                    intent(out) :: value_log

      integer :: i
      logical :: found_it

      value_log = .false.

      ! look for option name
      found_it = .false.
      if (len(name) == 1) then
         ! look for option string in short
         do i = 1, this%num_options
            if (name == this%shortn(i)) then
               found_it = .true.
               if (this%counts(i) > 0) value_log = .true.
               exit
            end if
         end do
      else
         ! look for option string in long
         do i = 1, this%num_options
            if (name == this%longn(i)) then
               found_it = .true.
               if (this%counts(i) > 0) value_log = .true.
               exit
            end if
         end do
      end if

      if (.not. found_it) then
         call MW_errors_parameter_error("get_option_log", "command.f90", &
               "name", name)
      end if

   end subroutine get_option_log

   !================================================================================
   ! Return the value stored by an option as an int
   subroutine get_option_int(this, name, value_int)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      type (MW_command_t), intent(in)  :: this
      character(*),               intent(in)  :: name
      integer,                    intent(out) :: value_int

      integer :: i
      logical :: found_it

      value_int = 0

      ! look for option name
      found_it = .false.
      if (len(name) == 1) then
         ! look for option string in short
         do i = 1, this%num_options
            if (name == this%shortn(i)) then
               found_it = .true.
               if (this%actions(i) == OPTION_ACTION_STORE) then
                  read(this%values(i),'(i64)') value_int
               else if (this%actions(i) == OPTION_ACTION_COUNT) then
                  value_int = this%counts(i)
               end if
               exit
            end if
         end do
      else
         ! look for option string in long
         do i = 1, this%num_options
            if (name == this%longn(i)) then
               found_it = .true.
               if (this%actions(i) == OPTION_ACTION_STORE) then
                  read(this%values(i),*) value_int
               else if (this%actions(i) == OPTION_ACTION_COUNT) then
                  value_int = this%counts(i)
               end if
               exit
            end if
         end do
      end if

      if (.not. found_it) then
         call MW_errors_parameter_error("get_option_int", "command.f90", &
               "name", name)
      end if

   end subroutine get_option_int

   !================================================================================
   ! Return the value stored by an option as a character string
   subroutine get_option_char(this, name, value_char)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      type (MW_command_t), intent(in)  :: this
      character(*),               intent(in)  :: name
      character(*),               intent(out) :: value_char

      integer :: i
      logical :: found_it

      value_char = ""

      ! look for option name
      found_it = .false.
      if (len(name) == 1) then
         ! look for option string in short
         do i = 1, this%num_options
            if (name == this%shortn(i)) then
               found_it = .true.
               value_char = this%values(i)
               exit
            end if
         end do
      else
         ! look for option string in long
         do i = 1, this%num_options
            if (name == this%longn(i)) then
               found_it = .true.
               value_char = this%values(i)
               exit
            end if
         end do
      end if

      if (.not. found_it) then
         call MW_errors_parameter_error("get_option_char", "command.f90", &
               "name", name)
      end if

   end subroutine get_option_char


   !================================================================================
   ! Return the value stored by an option as a real number
   subroutine get_option_real(this, name, value_real)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      type (MW_command_t), intent(in)  :: this
      character(*),               intent(in)  :: name
      real,                       intent(out) :: value_real

      integer :: i
      logical :: found_it

      value_real = 0.0

      ! look for option name
      found_it = .false.
      if (len(name) == 1) then
         ! look for option string in short
         do i = 1, this%num_options
            if (name == this%shortn(i)) then
               found_it = .true.
               read(this%values(i),*) value_real
               exit
            end if
         end do
      else
         ! look for option string in long
         do i = 1, this%num_options
            if (name == this%longn(i)) then
               found_it = .true.
               read(this%values(i),*) value_real
               exit
            end if
         end do
      end if

      if (.not. found_it) then
         call MW_errors_parameter_error("get_option_real", "command.f90", &
               "name", name)
      end if

   end subroutine get_option_real

   !================================================================================
   ! Return the value stored by an option as a double precision number
   subroutine get_option_double(this, name, value_double)
      use MW_errors, only: MW_errors_parameter_error => parameter_error
      implicit none
      type (MW_command_t), intent(in)  :: this
      character(*),               intent(in)  :: name
      double precision,           intent(out) :: value_double

      integer :: i
      logical :: found_it

      value_double = 0.0

      ! look for option name
      found_it = .false.
      if (len(name) == 1) then
         ! look for option string in short
         do i = 1, this%num_options
            if (name == this%shortn(i)) then
               found_it = .true.
               read(this%values(i),*) value_double
               exit
            end if
         end do
      else
         ! look for option string in long
         do i = 1, this%num_options
            if (name == this%longn(i)) then
               found_it = .true.
               read(this%values(i),*) value_double
               exit
            end if
         end do
      end if

      if (.not. found_it) then
         call MW_errors_parameter_error("get_option_double", "command.f90", &
               "name", name)
      end if

   end subroutine get_option_double

end module MW_command
