/*
 * Copyright (c) Radim Kolar 2013, 2018, 2023, 2025.
 * SPDX-License-Identifier: MIT
 *
 * getopt3 library is licensed under MIT license:
 *   https://spdx.org/licenses/MIT.html
*/

//!  # getopt(3) parser
//!
//!  ### Command line argument parser with GNU parsing rules and double dash '--' support.
//!
//!  `getopt3::`[`new`] parses the command line elements and isolates arguments from options.
//!  It returns a [`getopt`] structure where you can query options and use isolated arguments.
//!
//!  An element that starts with '-' (and is not exactly "-" or "--") is an option element.
//!  The characters of this element (aside from the initial '-') are option characters.
//!  A double dash `--` can be used to indicate the end of options; any arguments following
//!  it are treated as positional arguments.
//!
//!  #### Example
//!  ```rust
//!  use std::env::args;
//!  use getopt3::hideBin;
//!  let rc = getopt3::new(hideBin(args()), "ab:c");
//!  if let Ok(g) = rc {
//!     // command line options parsed sucessfully
//!     if let Some(str) = g.options.get(&'b') {
//!        // handle b argument
//!        println!("option -b have {} argument", str);
//!     };
//!  };
//!  ```
//!  #### Reference
//!
//!  1. [POSIX getopt](https://pubs.opengroup.org/onlinepubs/9699919799/functions/getopt.html) function.
//!  1. [GNU libc getopt](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) function.
//!
//!  [`new`]: ./fn.new.html
//!  [`getopt`]: ./struct.getopt.html

#![forbid(unsafe_code)]
#![forbid(missing_docs)]

#![allow(non_camel_case_types)]
#![allow(unused_parens)]
#![allow(non_snake_case)]

#![deny(rustdoc::bare_urls)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::missing_crate_level_docs)]
#![deny(rustdoc::invalid_codeblock_attributes)]
#![deny(rustdoc::invalid_rust_codeblocks)]

// If crate feature "no_std" is turned on and we are not running
//   tests, turn no_std attribute on to avoid linking with
//   std library.
#![cfg_attr(all(feature = "no_std", not(test)), no_std)]

#[cfg(not(feature = "no_std"))]
use { std::collections::HashMap,
      std::io::ErrorKind,
      std::io::Error,
      std::io::Result
};

#[cfg(feature = "no_std")]
extern crate alloc;

#[cfg(feature = "no_std")]
use {
   alloc::collections::BTreeMap as HashMap,
   alloc::vec::Vec,
   alloc::format,
   alloc::string::String,
   alloc::string::ToString,
   core::error::Error
};

/**
Parsed command line options. 

Created by [`new`] function. Structure contains isolated command line
arguments and collected options with their optional values.

Structure can contain options which are not listed in *optstring*
passed to [`new`] function. For strict parse mode pass this
structure to [`validate`] function.

[`new`]: ./fn.new.html
[`validate`]: ./fn.validate.html
*/
pub struct getopt {
   /**
   Map of command line options and their optional values extracted from command line arguments.

   If an option does not have a value, an empty string "" is stored.
   */
   pub options   : HashMap <char,String>,
   /** Isolated command line arguments without options. */
   pub arguments : Vec <String>,
   /** Map indicating whether an option has a required argument.

   This map contains all recognized options.
   Inclusion of an option in this map does not mean that the option must always be present
   or supplied as a command line argument. */
   pub option_has_arg: HashMap <char, bool>
}

impl getopt {
   /**
      Return number of command line arguments.

      Its convience shortcut for getopt.arguments.len()

     #### Example
     ```rust
     use std::env::args;
     use getopt3::hideBin;

     let getopt_rc = getopt3::new(hideBin(args()), "ab:c");
     if let Ok(g) = getopt_rc {
        println!("Number of command line arguments is {}", g.len());
     };
     ```
   */
   pub fn len(self: &Self) -> usize {
      self.arguments.len()
   }
   /**
      Returns Iterator over command line arguments.

      #### Example
      ```rust
      use std::env::args;
      use getopt3::hideBin;

      let getopt_rc = getopt3::new(hideBin(args()), "abc");
      if let Ok(my_getopt) = getopt_rc {
         for arg in my_getopt.iter() {
            println!("Argument: {}", arg);
         }
      }
      ```
   */
   pub fn iter(&self) -> std::slice::Iter<'_, String> {
      self.arguments.iter()
   }

   /**
      Return command line option value.

      Its convience shortcut for getopt.options.get()

      #### Return value
      1. If option were supplied on command line returned value is `Some`.
      1. If option doesn't have argument or argument is
         missing, reference to empty `String` is returned.
      1. If option were not supplied by user returned value is `None`.

      #### Example
      ```rust
      use std::env::args;
      use getopt3::hideBin;

      let getopt_rc = getopt3::new(hideBin(args()), "ab:c");
      if let Ok(my_getopt) = getopt_rc {
         if let Some(b_value) = my_getopt.get('b') {
            println!("-b argument is: {}", b_value);
         }
      }
      ```
   */
   pub fn get(&self, option: char ) -> Option<&String>
   {
      self.options.get(&option)
   }
}

impl IntoIterator for getopt {
    type Item = String;
    type IntoIter = std::vec::IntoIter<String>;

    fn into_iter(self) -> Self::IntoIter {
        self.arguments.into_iter()
    }
}

impl<'a> IntoIterator for &'a getopt {
    type Item = &'a String;
    type IntoIter = std::slice::Iter<'a, String>;

    fn into_iter(self) -> Self::IntoIter {
        self.arguments.iter()
    }
}

/**
  Parse command line arguments.

  Parses command line arguments using GNU getopt(3) parsing rules with
  double dash "--" support.
  Long arguments starting with double dash "--" are not supported.

  Parsing is done in non strict and non POSIX mode.
  Call [`validate`] on result to detect missing or unknown arguments.

  POSIX parsing mode is not yet supported.

  ## Arguments

  * `arg` - String Iterator with command line arguments. Can be empty.

  * `optstring` - List of legitimate alphanumeric plus '?' option characters.
                   If character is followed by colon, the option requires
                   an argument. Must not be empty.
  ## Parsing rules
  1. GNU argument parsing rules. It means that options can be anywhere in command line before --
  1. Double dash -- support. Everything after -- is not treated as options.
  1. Long options are not supported.
  1. Multiple options not requiring argument can be chained together.
     -abc is the same as -a -b -c
  1. Last chained option can have an argument.
     -abcpasta is the same as -a -b -c pasta
  1. Argument does not require space. -wfile is same as -w file
  1. optional argument `::` GNU optstring extension is not implemented.
  1. Strict POSIX parse mode where first non option stops option parsing is not supported.
     This mode is triggered in GNU getopt by setting `POSIXLY_CORRECT` variable or by
     optstring starting with a plus sign `+`.
  1. The POSIX-specified extension for the *getopt* function, which allows the optstring to
     start with a colon (:), is unsupported.
     This extension enables the use of the '?' character as a command-line option.
     We *always allow* the '?' option without requiring manual activation via optstring.

  ## Errors
  1. Parsing error **only happens** if optstring parameter is invalid or empty.
  1. If required argument is missing function _new()_ still returns succesfully and missing argument will
     be replaced by empty String.

  ### See also
  1. [GNU libc getopt](https://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html) function.
  1. [POSIX getopt](https://pubs.opengroup.org/onlinepubs/9699919799/functions/getopt.html) function.

[`validate`]: ./fn.validate.html
*/
pub fn new(arg: impl IntoIterator<Item=impl AsRef<str>>, optstring: impl AsRef<str>) -> Result<getopt> {
    let mut opts = HashMap::new();
    let mut args = Vec::new();
    let mut next_opt: Option<char> = None;
    let mut stop_parsing = false;

    let options_map = build_options_map(validate_optstring(optstring.as_ref())?);

    for element in arg {
        let element = element.as_ref();
        if stop_parsing {
            args.push(element.to_string());
        } else if let Some(next_opt_char) = next_opt {
            opts.insert(next_opt_char, element.to_string());
            next_opt = None;
        } else {
            match parseElement(&element, &options_map) {
                Ok((omap, el, nopt)) => {
                    opts.extend(omap);
                    if let Some(el_str) = el {
                        args.push(el_str);
                    }
                    next_opt = nopt;
                }
                Err(_) => stop_parsing = true,
            }
        }
    }

    // HANDLE MISSING ARGUMENT FOR LAST OPTION
    if let Some(next_opt_char) = next_opt {
        opts.insert(next_opt_char, String::new());
    }

   Ok ( getopt {options:opts, arguments:args, option_has_arg:options_map} )
}

/**
   Posix option parsing mode. First non option stops option parsing.
*/
#[cfg(feature = "posix")]
pub fn posix(arg: impl IntoIterator<Item=impl AsRef<str>>, optstring: impl AsRef<str>) -> Result<getopt> {
   let options_map = build_options_map(validate_optstring(optstring.as_ref())?);

   let mut opts = HashMap::<char,String>::new();
   let mut args = Vec::<String>::new();
   let mut next_opt: Option<char> = None;
   let mut stop_parsing = false;

   for element in arg {
      let element = element.as_ref();
      if stop_parsing {
         // we do not parse options anymore all what's left
         // are arguments
         args.push(element.to_string());
      } else
      {
         // check if we need to process option from previous loop
         if next_opt.is_some() {
            opts.insert(next_opt.unwrap(), element.to_string());
            next_opt = None;
         } else {
            // check if argument is a option in form -X otherwise
            // it is an argument and we stop parsing
            if element.starts_with('-') && element.len() == 2 &&
               validate_optchar(element.chars().nth(1).unwrap()).is_ok()
            {
               let opt = element.chars().nth(1).unwrap();
               if let Some(&value) = options_map.get(&opt) {
                  if value == true {
                     // option will follow in next loop iteration
                     next_opt = Some(opt);
                  } else {
                     // option is without an argument insert it now
                     opts.insert(opt,"".to_string());
                  }
               } else {
                  // unknown option treat is as option without an argument
                  opts.insert(opt,"".to_string());
               }
            } else {
               // not an option. store argument and stop parsing
               args.push(element.to_string());
               stop_parsing = true;
            }
         }
      }
   }
   // if there is non finished option. finish it
   if let Some(o) = next_opt {
      opts.insert(o, String::new());
   }

   Ok ( getopt {options:opts, arguments:args, option_has_arg:options_map} )
}

/**
  Checks if optchar is valid option character.

  Allowed characters are a-zA-Z0-9
*/
#[allow(dead_code)]
fn validate_optchar(optchar: char) -> Result<char> {
   match optchar {
      'a'..='z' => Ok(optchar),
      'A'..='Z' => Ok(optchar),
      '0'..='9' => Ok(optchar),
      '?'       => Ok(optchar),
       _        => Err(Error::new(ErrorKind::InvalidInput, "unsupported option character"))
   }
}

/**
  Checks if optstring is valid.

  optstring can't start with ':' or have double '::' or be empty.
  allowed optstring characters are 7-bit ASCII letters and digits.
*/
fn validate_optstring(optstring: &str) -> Result<&str> {
   if optstring.is_empty() {
      Err(Error::new(ErrorKind::UnexpectedEof, "option string can't be empty"))
   } else {
      for c in optstring.chars() {
         match c {
            'a'..='z' => Ok(()),
            'A'..='Z' => Ok(()),
            '0'..='9' => Ok(()),
                 '?'  => Ok(()),
                 ':'  => Ok(()),
                   _  => Err(Error::new(ErrorKind::InvalidInput, "unsupported characters in optstring"))
         }?
      }
      if optstring.contains("::") {
         Err(Error::new(ErrorKind::InvalidInput, "double : are not permited in optstring"))
      }
      else if let Some(':') = optstring.chars().next() {
         Err(Error::new(ErrorKind::InvalidInput, "optstring cant start with :"))
      } else {
         Ok(optstring)
      }
   }
}

/**
 * Build options map from validated optstring.
 *
 * returns map <option,hasArgument>
*/ 
fn build_options_map(optstring: &str) -> HashMap<char, bool> {
   let mut rc : HashMap<char, bool> = HashMap::with_capacity(optstring.len());
   let mut previous: char = ':';
   let mut insert_one = |c:char| -> () { match c {
         ':' => rc.insert(*&previous,true),
          _ if previous != ':' => rc.insert(*&previous,false),
          _ => None
   }; previous = c;};

   for c in optstring.chars() {
      insert_one(c);
   }
   // re-run option map building logic on last character in optstring if it is not ':'
   // if last character is ':', it has been already inserted to map and running it
   // again would cause insertion of ':' into options map because it is previous character
   optstring.chars().last().filter(|c| *c != ':').into_iter().for_each(|c| insert_one(c));
   rc
}

/** parse Command line Element
   @param element command line element (one word on command line)
   @returns map(Option->value). If option is unknown value is null, otherwise
              value is option or "" if option do not have argument.
            String - command line argument or null if element is option type.
            Character - what option will be next element or null for none.
*/
fn parseElement(element: &str, options_map: &HashMap<char, bool>) ->
         Result<(HashMap<char,String>, Option<String>, Option<char>)> {

      if( element == "--" ) {
        Err( Error::new(ErrorKind::UnexpectedEof, "no more arguments") )
      } else 
      if( element.is_empty() ) {
        Err( Error::new(ErrorKind::InvalidInput, "element must not be empty") )
      } else
      if( ! element.starts_with('-') || element == "-" ) {
         Ok( (HashMap::new(), Some(element.to_string()), None ) )
      } else
      {
           // parsing options block starting with -
           let mut opts = HashMap::<char,String>::new();
           let mut argfollows = false;
           let mut opt_name: Option<char> = None;
           let mut opt_argument = String::new();

           for opt in element.chars().skip(1) {
              if( argfollows == true ) {
               // ARGUMENT FOLLOWS TO END OF ELEMENT
               opt_argument.push(opt);
             } else {
                match options_map.get(&opt) {
                   None => {
                      // UNKNOWN OPTION
                      opts.insert(opt, String::new());
                   } 
                   Some(optarg) => {
                      // KNOWN OPTION
                      if( *optarg == true ) {
                         // OPTION WITH ARGUMENT
                         argfollows = true;
                         opt_name = Some(opt);
                       } else {
                         // OPTION WITHOUT ARGUMENT
                         opts.insert(opt, String::new());
                      }
                   }
                }
             }
           }
           // RETURN RESULT
           if ( argfollows == true ) {
              if ( opt_argument.is_empty() ) {
                 // ARGUMENT FOLLOWS IN NEXT ELEMENT
                 Ok ( (opts, None, opt_name) )
              } else {
                 // SAVE ARGUMENT INTO MAP
                 opts.insert(opt_name.unwrap(), opt_argument);
                 Ok ( ( opts, None, None ) )
              }
           } else {
              // ONLY SHORT OPTIONS
              Ok ( (opts, None, None ) )
           }
      }
}


/**
* Validate parsed options in strict mode.
*
* ## Success
*
*   If strict mode validation passes, unchanged argument wrapped in [`Result`]
*   is returned.
*
* ## Errors
* 
*   Returns [`Err`] if option not listed in optstring is encountered or
*   required argument for an option is missing.
**/
pub fn validate(getopt: getopt) -> Result<getopt> {
    // validate unknown options
    for opt in getopt.options.keys() {
        if !getopt.option_has_arg.contains_key(opt) {
            return Err(Error::new(ErrorKind::InvalidInput, format!("Unknown option -{}", opt)));
        }
    }

    // validate missing arguments
    for (opt,_) in getopt.option_has_arg.iter().filter(|(_,arg)| **arg) {
        if let Some(val) = getopt.options.get(opt) {
            if val.is_empty() {
                return Err(Error::new(ErrorKind::InvalidInput, format!("Option -{} does not have required argument", opt)));
            }
        }
    }

    Ok(getopt)
}

/**
 * Removes first element from the IntoIterator.
 *
 * This utility function is supposed to be used on value returned by
 * `std::env::args()` before passing it to [`new`].
 *
 * First argument returned by `args()` corresponds to the program
 * executable name and its undesirable to have program name included
 * between parsed arguments.
 *
 * This function exists for making code more readable and because
 * widely used [yargs npm](https://www.npmjs.com/package/yargs) argument
 * parser have same function.
 *
 * `hideBin` function can be replaced by calling `.skip(1)`
 * on `Iterator` before passing it to [`new`]. Choose what is more
 * readable for you.
 *
 * #### Example
 * ```rust
 * use std::env::args;
 * use getopt3::hideBin;
 *
 * let rc = getopt3::new(hideBin(args()), "ab:c");
 * if let Ok(g) = rc {
 *    // command line options parsed sucessfully
 *    if let Some(_) = g.options.get(&'a') {
 *       // -a option found on command line
 *    };
 *  };
 * ```
 * [`new`]: ./fn.new.html
*/
pub fn hideBin(argv: impl IntoIterator<Item=String>) -> impl IntoIterator<Item=String> {
   argv.into_iter().skip(1)
}

// unit tests

#[cfg(test)]
#[path = "getopt_helpers_tests.rs"]
mod helpers;

#[cfg(test)]
#[macro_use]
extern crate quickcheck;

#[cfg(test)]
#[path = "getopt_helpers_qc.rs"]
mod helpers_qc;

#[cfg(test)]
#[path = "getopt_tests.rs"]
mod tests;

#[cfg(test)]
#[cfg(feature = "posix")]
#[path = "getopt_posix_tests.rs"]
mod posix;

#[cfg(test)]
#[path = "getopt_validate_tests.rs"]
mod validations;

#[cfg(test)]
#[path = "getopt_hidebin_tests.rs"]
mod hidebin;

#[cfg(test)]
#[path = "getopt_doubledash_tests.rs"]
mod doubledash;

#[cfg(test)]
#[path = "getopt_iterator_tests.rs"]
mod it;
