/*---------------------------------------------------------------------\
|                          ____ _   __ __ ___                          |
|                         |__  / \ / / . \ . \                         |
|                           / / \ V /|  _/  _/                         |
|                          / /__ | | | | | |                           |
|                         /_____||_| |_| |_|                           |
|                                                                      |
\---------------------------------------------------------------------*/
/** \file zypp/base/StringV.h
 * c++17: std::string_view tools
 */
#ifndef ZYPP_BASE_STRINGV_H
#define ZYPP_BASE_STRINGV_H
#include <string_view>
#ifdef __cpp_lib_string_view

#include <zypp-core/Globals.h>
#include <zypp-core/base/String.h>
#include <zypp-core/base/Regex.h>
#include <zypp-core/base/Flags.h>

///////////////////////////////////////////////////////////////////
namespace zypp
{
  namespace strv
  {
    using regex = str::regex;
    using smatch = str::smatch;

    /** Define how to trim. */
    enum class Trim {
      notrim = 0,
      left   = 1<<0,
      right  = 1<<1,
      trim   = (left|right),
    };

    ZYPP_DECLARE_FLAGS_AND_OPERATORS( TrimFlag, Trim );

    /** The default blank. */
    inline constexpr std::string_view blank = " \t";

    /** Trim \a chars_r at the beginning of \a str_r. */
    inline std::string_view ltrim( std::string_view str_r, std::string_view chars_r = blank )
    {
      if ( str_r.empty() )
        return str_r;
      auto pos = str_r.find_first_not_of( chars_r );
      if ( pos == str_r.npos )
        str_r.remove_prefix( str_r.size() );
      else if ( pos )
        str_r.remove_prefix( pos );
      return str_r;
    }

    /** Trim \a chars_r at the end of \a str_r. */
    inline std::string_view rtrim( std::string_view str_r, std::string_view chars_r = blank )
    {
      if ( str_r.empty() )
        return str_r;
      auto pos = str_r.find_last_not_of( chars_r );
      if ( pos == str_r.npos )
        str_r.remove_suffix( str_r.size() );
      else if ( (pos = str_r.size()-1-pos) )
        str_r.remove_suffix( pos );
      return str_r;
    }

    /** Trim \a chars_r at both sides of \a str_r. */
    inline std::string_view trim( std::string_view str_r, std::string_view chars_r = blank )
    {
      if ( str_r.empty() )
        return str_r;
      str_r = ltrim( std::move(str_r), chars_r );
      str_r = rtrim( std::move(str_r), chars_r );
      return str_r;
    }

    /** Trim \a chars_r at \a trim_r sides of \a str_r. */
    inline std::string_view trim( std::string_view str_r, std::string_view chars_r, TrimFlag trim_r )
    {
      if ( str_r.empty() || trim_r == Trim::notrim )
        return str_r;
      if ( trim_r.testFlag( Trim::left ) )
        str_r = ltrim( std::move(str_r), chars_r );
      if ( trim_r.testFlag( Trim::right ) )
        str_r = rtrim( std::move(str_r), chars_r );
      return str_r;
    }
    /** \overload Trimming blanks at \a trim_r sides of \a str_r. */
    inline std::string_view trim( std::string_view str_r, TrimFlag trim_r )
    { return trim( std::move(str_r), blank, std::move(trim_r) ); }

    /** Return whether \a str_r has prefix \a prefix_r. */
    inline bool hasPrefix( std::string_view str_r, std::string_view prefix_r )
    { return( str_r.size() >= prefix_r.size() && ::strncmp( str_r.data(), prefix_r.data(), prefix_r.size() ) == 0 ); }

    /** \overload Case insensitive */
    inline bool hasPrefixCI( std::string_view str_r, std::string_view prefix_r  )
    { return( str_r.size() >= prefix_r.size() && ::strncasecmp( str_r.data(), prefix_r.data(), prefix_r.size()  ) == 0 ); }

    ///////////////////////////////////////////////////////////////////
    namespace detail
    {
      /** Split* functions working horse callback. */
      using WordConsumer = std::function<bool(std::string_view,unsigned,bool)>;

      /** \name Split* callback signatures accepted for convenience.
       *
       * Details may vary for specific split* functions, but basically each separator found
       * in line will be enclosed by 2 words being reported. The working horse callback
       * offers the \a word, the \a index of word (starting with 0) and a boolean indicating whether
       * this is going to be the \a last report.
       *
       * A callback may optionally return a value convertible to \c bool. Returning \c false
       * usually causes split* to stop looking further separators and to finish the reporting.
       *
       * Accepted callbacks: [bool|void] callback( [std::string_view[, unsigned[, bool]]] )
       * (or no callback at all )
       */
      //@{
      /** bool(std::string_view,unsigned,bool) */
      template <typename Callable, std::enable_if_t<
      std::is_invocable_r_v<bool,Callable,std::string_view,unsigned,bool>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return std::forward<Callable>(fnc_r); }
      /** void(std::string_view,unsigned,bool) */
      template <typename Callable, std::enable_if_t<
      ! std::is_invocable_r_v<bool,Callable,std::string_view,unsigned,bool>
      && std::is_invocable_r_v<void,Callable,std::string_view,unsigned,bool>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view a1,unsigned a2,bool a3)->bool { fnc_r(a1,a2,a3); return true; }; }

      /** bool(std::string_view,unsigned) */
      template <typename Callable, std::enable_if_t<
      std::is_invocable_r_v<bool,Callable,std::string_view,unsigned>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view a1,unsigned a2,bool)->bool { return fnc_r(a1,a2); }; }
      /** void(std::string_view,unsigned) */
      template <typename Callable, std::enable_if_t<
      ! std::is_invocable_r_v<bool,Callable,std::string_view,unsigned>
      && std::is_invocable_r_v<void,Callable,std::string_view,unsigned>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view a1,unsigned a2,bool)->bool { fnc_r(a1,a2); return true; }; }

      /** bool(std::string_view) */
      template <typename Callable, std::enable_if_t<
      std::is_invocable_r_v<bool,Callable,std::string_view>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view a1,unsigned,bool)->bool { return fnc_r(a1); }; }
      /** void(std::string_view) */
      template <typename Callable, std::enable_if_t<
      ! std::is_invocable_r_v<bool,Callable,std::string_view>
      && std::is_invocable_r_v<void,Callable,std::string_view>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view a1,unsigned,bool)->bool { fnc_r(a1); return true; }; }

      /** bool() */
      template <typename Callable, std::enable_if_t<
      std::is_invocable_r_v<bool,Callable>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view,unsigned,bool)->bool { return fnc_r(); }; }
      /** void() */
      template <typename Callable, std::enable_if_t<
      ! std::is_invocable_r_v<bool,Callable>
      && std::is_invocable_r_v<void,Callable>
      , bool> = true>
      WordConsumer wordConsumer( Callable && fnc_r )
      { return [&fnc_r](std::string_view,unsigned,bool)->bool { fnc_r(); return true; }; }
      //@}

      /** \ref split working horse */
      unsigned _split( std::string_view line_r, std::string_view sep_r, Trim trim_r, WordConsumer && fnc_r ) ZYPP_API;

      /** \ref splitRx working horse */
      unsigned _splitRx(std::string_view line_r, const regex & rx_r, const WordConsumer& fnc_r );
    }  // namespace detail
    ///////////////////////////////////////////////////////////////////

    /** Split \a line_r into words separated by the regular expression \a rx_r and invoke \a fnc_r with each word.
     *
     * Each separator match found in \a line_r will be enclosed by 2 words being reported.
     * Words may be empty if the separator match is located at the beginning or at the end
     * of \a line_r, or if there are consecutive separator match occurrences.
     *
     * Accepted callbacks: [bool|void]( [std::string_view[, unsigned[, bool]]] )
     * (or no callback at all)
     *
     * A callback may take the \a word, the \a index of word (starting with 0) and a boolean
     * indicating whether this is going to be the \a last report.
     *
     * A callback may optionally return a value convertible to \c bool. Returning \c false
     * causes split to stop looking further separators. The final report will contain the
     * remaining part of the \a line_r then.
     *
     * If the separator does not occur on the line the whole string is reported.
     *
     * \returns the number of words reported.
     */
    template <typename Callable = detail::WordConsumer>
    unsigned splitRx( std::string_view line_r, const regex & rx_r, Callable && fnc_r = Callable() )
    { return detail::_splitRx( line_r, rx_r, detail::wordConsumer( std::forward<Callable>(fnc_r) ) ); }


    /** Split \a line_r into words separated by \a sep_r and invoke \a fnc_r with each word.
     *
     * - If \a sep_r is not empty, each separator found in \a line_r will be enclosed by 2
     * words being reported. Words may be empty if the separator is located at the beginning
     * or the end of \a line_r or if there are consecutive occurrences.
     *
     * - If \a sep_r is unspecified or empty, it splits on whitespace /[BLANK,TAB]+/. In this
     * case only the (not empty) words found on the line are reported (trimmed).
     *
     * The optional \a trim_r argument tells whether whitespace around the words found
     * should be trimmed before reporting them. The default is not to trim.
     *
     * Accepted callbacks: [bool|void]( [std::string_view[, unsigned[, bool]]] )
     * (or no callback at all)
     *
     * A callback may take the \a word, the \a index of word (starting with 0) and a boolean
     * indicating whether this is going to be the \a last report.
     *
     * A callback may optionally return a value convertible to \c bool. Returning \c false
     * causes split to stop looking further separators. The final report will contain the
     * remaining part of the \a line_r then.
     *
     * If the separator does not occur on the line the whole string is reported.
     *
     * \returns the number of words reported.
     *
     * \code
     *   str = ""
     *
     *   split( str, fnc );
     *   // []
     *
     *   split( str, " ", fnc );
     *   // ['']
     *
     *
     *   str = " "
     *
     *   split( str, fnc );
     *   // []
     *
     *   split( str, " ", fnc );
     *   // ['', '']
     *
     *
     *   str = " 1 2 3 4 5 ";
     *
     *   split( str, fnc );
     *   // ['1', '2', '3', '4', '5']
     *
     *   split( str, " ", fnc );
     *   // ['', '1', '2', '3', '4', '5', '']
     *
     *   split( str, " 2", fnc );
     *   // [' 1', ' 3 4 5 ']
     *
     *   split( str, " 2", Trim::all, fnc );
     *   // ['1', '3 4 5']
     * \endcode
     */
    template <typename Callable = detail::WordConsumer>
    unsigned split( std::string_view line_r, std::string_view sep_r, Trim trim_r, Callable && fnc_r = Callable() )
    { return detail::_split( line_r, sep_r, trim_r, detail::wordConsumer( std::forward<Callable>(fnc_r) ) ); }

    /** \overload  Split at \a sep_r and Trim::notrim */
    template <typename Callable = detail::WordConsumer>
    inline unsigned split( std::string_view line_r, std::string_view sep_r, Callable && fnc_r = Callable() )
    { return detail::_split( line_r, sep_r, Trim::notrim, detail::wordConsumer( std::forward<Callable>(fnc_r) ) ); }

    /** \overload  Split at whitespace */
    template <typename Callable = detail::WordConsumer>
    inline unsigned split( std::string_view line_r, Callable && fnc_r = Callable() )
    { return detail::_split( line_r, std::string_view(), Trim::notrim, detail::wordConsumer( std::forward<Callable>(fnc_r) ) ); }

    inline std::string_view asStringView( const char * t )
    { return t == nullptr ? std::string_view() : t; }


    /** Handling \-escaped strings.
     *
     * Splitting and trimming of \-escaped strings.
     */
    //@{
    /** Return a copy of \a word_r with all \a special_r chars (and '\') being \-escaped */
    std::string doBSEscape( std::string_view word_r, std::string_view special_r );

    /** Remove any remaining \-escapes from \a word_r. */
    std::string unBSEscape( std::string_view word_r );

    /** Remove any remaining \-escapes from the words in \a words_r. */
    std::vector<std::string> unBSEscape( const std::vector<std::string_view> & words_r );

    /** Split a \-escaped string at literal occurrences of \a sep_r.
     *
     * Returns a (not empty) vector of string_views in \a line_r. If N is the number of un-escaped occurrences of
     * \a sep_r then the size of the returned vector is N+1. Joining the entries in the vector by \a sep_r restores
     * the initial \a  line_r.
     *
     * If \a maxSplits_r is set stop after this number of splits and return the reminder of the string as last entry
     * in the vector.
     *
     * splitBSEscaped( "; \\;a;; b;", ';' );
     * // [ "", " \\;a", "", " b", "" ]
     *
     * splitBSEscaped( "; \\;a;; b;", ';', 2 );
     * // [ "", " \\;a", "; b;" ]
     *
     * \note The returned string_views are still \-escaped. Use \ref unBSEscape to turn them into
     * literal strings.
     */
    std::vector<std::string_view> splitBSEscaped( std::string_view line_r, std::string_view chars_r = blank, unsigned maxSplits_r = 0 );
    inline std::vector<std::string_view> splitBSEscaped( std::string_view line_r, unsigned maxSplits_r )
    { return splitBSEscaped( line_r, blank, maxSplits_r ); }

    /** Trim \a chars_r at the beginning of \a str_r, respecting backslash escapes. */
    inline std::string_view ltrimBSEscaped( std::string_view str_r, std::string_view chars_r = blank )
    {
      // Leading characters are never escaped by definition, so we can reuse ltrim.
      return ltrim( str_r, chars_r );
    }

    /** Trim \a chars_r at the end of \a str_r, respecting backslash escapes. */
    inline std::string_view rtrimBSEscaped( std::string_view str_r, std::string_view chars_r = blank )
    {
      while ( !str_r.empty() ) {
        char lastChar = str_r.back();

        // If the last character isn't in our trim set, we are done.
        if ( chars_r.find( lastChar ) == std::string_view::npos ) {
          break;
        }

        // Check if this character is escaped by counting backslashes preceding it.
        size_t pos = str_r.size() - 1;
        size_t bsCount = 0;
        while ( pos > 0 && str_r[pos - 1] == '\\' ) {
          ++bsCount;
          --pos;
        }

        // If bsCount is odd, the character is escaped (e.g., " \ ") and shouldn't be trimmed.
        if ( bsCount % 2 != 0 ) {
          break;
        }

        // Otherwise, it's a literal separator and can be removed.
        str_r.remove_suffix( 1 );
      }
      return str_r;
    }

    /** Trim \a chars_r at both sides of \a str_r, respecting backslash escapes. */
    inline std::string_view trimBSEscaped( std::string_view str_r, std::string_view chars_r = blank )
    {
      if ( str_r.empty() ) {
        return str_r;
      }
      // Order matters: ltrim first so we don't have to worry about
      // shifted indices when calculating backslashes for the right side.
      str_r = ltrimBSEscaped( str_r, chars_r );
      str_r = rtrimBSEscaped( str_r, chars_r );
      return str_r;
    }
    //@}
  } // namespace strv
} // namespace zypp
///////////////////////////////////////////////////////////////////
#endif // __cpp_lib_string_view
#endif // ZYPP_BASE_STRINGV_H
