text: // SPDX-License-Identifier: Apache-2.0
  // Copyright 2014 Network Geographics
  
  /** @file
  
      Example use of IPSpace for property mapping.
  */
  
  #include "catch.hpp"
  
  #include <memory>
  #include <limits>
  
  #include "swoc/TextView.h"
  #include "swoc/swoc_ip.h"
  #include "swoc/bwf_ip.h"
  #include "swoc/bwf_std.h"
  
  using namespace std::literals;
  using namespace swoc::literals;
  using swoc::TextView;
  using swoc::IPEndpoint;
  
  using swoc::IP4Addr;
  using swoc::IP4Range;
  
  using swoc::IP6Addr;
  using swoc::IP6Range;
  
  using swoc::IPAddr;
  using swoc::IPRange;
  using swoc::IPSpace;
  
  using swoc::MemSpan;
  using swoc::MemArena;
  
  using W = swoc::LocalBufferWriter<256>;
  namespace {
  bool Verbose_p =
  #if VERBOSE_EXAMPLE_OUTPUT
      true
  #else
      false
  #endif
      ;
  }
  
  TEST_CASE("IPSpace bitset blending", "[libswoc][ipspace][bitset][blending]") {
    // Color each address with a set of bits.
    using PAYLOAD = std::bitset<32>;
    // Declare the IPSpace.
    using Space = swoc::IPSpace<PAYLOAD>;
    // Example data type.
    using Data = std::tuple<TextView, PAYLOAD>;
  
    // Dump the ranges to stdout.
    auto dump = [](Space&space) -> void {
      if (Verbose_p) {
        std::cout << W().print("{} ranges\n", space.count());
        for (auto &&[r, payload] : space) {
          std::cout << W().print("{:25} : {}\n", r, payload);
        }
      }
    };
  
    // Convert a list of bit indices into a bitset.
    auto make_bits = [](std::initializer_list<unsigned> indices) -> PAYLOAD {
      PAYLOAD bits;
      for (auto idx : indices) {
        bits[idx] = true;
      }
      return bits;
    };
  
    // Bitset blend functor which computes a union of the bitsets.
    auto blender = [](PAYLOAD& lhs, PAYLOAD const& rhs) -> bool {
      lhs |= rhs;
      return true;
    };
  
    // Example marking functor.
    auto marker = [&](Space & space, swoc::MemSpan<Data> ranges) -> void {
      // For each test range, compute the bitset from the list of bit indices.
      for (auto &&[text, bits] : ranges) {
        space.blend(IPRange{text}, bits, blender);
      }
    };
  
    // The IPSpace instance.
    Space space;
  
    // test ranges 1
    std::array<Data, 7> ranges_1 = {{
          { "100.0.0.0-100.0.0.255", make_bits({ 0 }) }
        , { "100.0.1.0-100.0.1.255", make_bits({ 1 }) }
        , { "100.0.2.0-100.0.2.255", make_bits({ 2 }) }
        , { "100.0.3.0-100.0.3.255", make_bits({ 3 }) }
        , { "100.0.4.0-100.0.4.255", make_bits({ 4 }) }
        , { "100.0.5.0-100.0.5.255", make_bits({ 5 }) }
        , { "100.0.6.0-100.0.6.255", make_bits({ 6 }) }
    }};
  
    marker(space, MemSpan<Data>{ranges_1.data(), ranges_1.size()});
    dump(space);
  
    // test ranges 2
    std::array<Data, 3> ranges_2 = {{
        { "100.0.0.0-100.0.0.255", make_bits({ 31 }) }
      , { "100.0.1.0-100.0.1.255", make_bits({ 30 }) }
      , { "100.0.2.128-100.0.3.127", make_bits({ 29 }) }
    }};
  
    marker(space, MemSpan<Data>{ranges_2.data(), ranges_2.size()});
    dump(space);
  
    // test ranges 3
    std::array<Data, 1> ranges_3 = {{
        { "100.0.2.0-100.0.4.255", make_bits({ 2, 3, 29 })}
    }};
  
    marker(space, MemSpan<Data>{ranges_3.data(), ranges_3.size()});
    dump(space);
  
    // reset blend functor
    auto resetter = [](PAYLOAD& lhs, PAYLOAD const& rhs) -> bool {
      auto mask = rhs;
      lhs &= mask.flip();
      return lhs != 0;
    };
  
    // erase bits
    space.blend(IPRange{"0.0.0.0-255.255.255.255"}, make_bits({2,3,29}), resetter);
    dump(space);
  
    // ragged boundaries
    space.blend(IPRange{"100.0.2.19-100.0.5.117"}, make_bits({16,18,20}), blender);
    dump(space);
  
    // bit list blend functor which computes a union of the bitsets.
    auto bit_blender = [](PAYLOAD& lhs, std::initializer_list<unsigned> const& rhs) -> bool {
      for ( auto idx : rhs )
        lhs[idx] = true;
      return true;
    };
  
    std::initializer_list<unsigned> bit_list = { 10,11 };
    space.blend(IPRange{"0.0.0.1-255.255.255.254"}, bit_list, bit_blender);
    dump(space);
  
  }
  
  // ---
  
  /** A "table" is conceptually a table with the rows labeled by IP address and a set of
   * property columns that represent data for each IP address.
   */
  class Table {
    using self_type = Table; ///< Self reference type.
  public:
    static constexpr char SEP = ','; /// Value separator for input file.
  
    /** A property is the description of data for an address.
     * The table consists of an ordered list of properties, each corresponding to a column.
     */
    class Property {
      using self_type = Property; ///< Self reference type.
    public:
      /// A handle to an instance.
      using Handle = std::unique_ptr<self_type>;
  
      /** Construct an instance.
       *
       * @param name Property name.
       */
      Property(TextView const& name) : _name(name) {};
  
      /// Force virtual destructor.
      virtual ~Property() = default;
  
      /** The size of the property in bytes.
       *
       * @return The amount of data needed for a single instance of the property value.
       */
      virtual size_t size() const = 0;
  
      /** The index in the table of the property.
       *
       * @return The column index.
       */
      unsigned idx() const { return _idx; }
  
      /** Token persistence.
       *
       * @return @c true if the token needs to be preserved, @c false if not.
       *
       * If the token for the value is consumed, this should be left as is. However, if the token
       * itself needs to be persistent for the lifetime of the table, this must be overridden to
       * return @c true.
       */
      virtual bool needs_localized_token() const { return false; }
  
      /// @return The row data offset in bytes for this property.
      size_t offset() const { return _offset; }
  
      /** Parse the @a token.
       *
       * @param token Value from the input file for this property.
       * @param span Row data storage for this property.
       * @return @c true if @a token was correctly parse, @c false if not.
       *
       * The table parses the input file and handles the fields in a line. Each value is passed to
       * the corresponding property for parsing via this method. The method should update the data
       * pointed at by @a span.
       */
      virtual bool parse(TextView token, MemSpan<std::byte> span) = 0;
  
    protected:
      friend class Table;
  
      TextView _name; ///< Name of the property.
      unsigned _idx = std::numeric_limits<unsigned>::max(); ///< Column index.
      size_t _offset = std::numeric_limits<size_t>::max(); ///< Offset into a row.
  
      /** Set the column index.
       *
       * @param idx Index for this property.
       * @return @a this.
       *
       * This is called from @c Table to indicate the column index.
       */
      self_type & assign_idx(unsigned idx) { _idx = idx; return *this; }
  
      /** Set the row data @a offset.
       *
       * @param offset Offset in bytes.
       * @return @a this
       *
       * This is called from @c Table to store the row data offset.
       */
      self_type & assign_offset(size_t offset) { _offset = offset; return *this; }
    };
  
    /// Construct an empty Table.
    Table() = default;
  
    /** Add a property column to the table.
     *
     * @tparam P Property class.
     * @param col Column descriptor.
     * @return @a A pointer to the property.
     *
     * The @c Property instance must be owned by the @c Table because changes are made to it specific
     * to this instance of @c Table.
     */
    template < typename P >
    P * add_column(std::unique_ptr<P> && col);
  
    /// A row in the table.
    class Row {
      using self_type = Row; ///< Self reference type.
    public:
      /// Default cconstruct an row with uninitialized data.
      Row(MemSpan<std::byte> span) : _data(span) {}
      /** Extract property specific data from @a this.
       *
       * @param prop Property that defines the data.
       * @return The range of bytes in the row for @a prop.
       */
      MemSpan<std::byte> span_for(Property const& prop) const;
  
    protected:
      MemSpan<std::byte> _data; ///< Raw row data.
    };
  
    /** Parse input.
     *
     * @param src The source to parse.
     * @return @a true if parsing was successful, @c false if not.
     *
     * In general, @a src will be the contents of a file.
     *
     * @see swoc::file::load
     */
    bool parse(TextView src);
  
    /** Look up @a addr in the table.
     *
     * @param addr Address to find.
     * @return A @c Row for the address, or @c nullptr if not found.
     */
    Row* find(IPAddr const& addr);
  
    /// @return The number of ranges in the container.
    size_t size() const { return _space.count(); }
  
    /** Property for column @a idx.
     *
     * @param idx Index.
     * @return The property.
     */
    Property * column(unsigned idx) { return _columns[idx].get(); }
  
  protected:
    size_t _size = 0; ///< Size of row data.
    /// Defined properties for columns.
    std::vector<Property::Handle> _columns;
  
    /// IPSpace type.
    using space = IPSpace<Row>;
    space _space; ///< IPSpace instance.
  
    MemArena _arena; ///< Arena for storing rows.
  
    /** Extract the next token from the line.
     *
     * @param line Current line [in,out]
     * @return Extracted token.
     */
    TextView token(TextView & line);
  
    /** Localize view.
     *
     * @param src View to localize.
     * @return The localized view.
     *
     * This copies @a src to the internal @c MemArena and returns a view of the copied data.
     */
    TextView localize(TextView const& src);
  };
  
  template < typename P >
  P * Table::add_column(std::unique_ptr<P> &&col) {
    auto prop = col.get();
    auto idx = _columns.size();
    col->assign_offset(_size);
    col->assign_idx(idx);
    _size += static_cast<Property*>(prop)->size();
    _columns.emplace_back(std::move(col));
    return prop;
  }
  
  TextView Table::localize(TextView const&src) {
    auto span = _arena.alloc(src.size()).rebind<char>();
    memcpy(span, src);
    return span.view();
  }
  
  TextView Table::token(TextView & line) {
    TextView::size_type idx = 0;
    // Characters of interest.
    static char constexpr separators[2] = { '"', SEP };
    static TextView sep_list { separators, 2 };
    bool in_quote_p  = false;
    while (idx < line.size()) {
      // Next character of interest.
      idx = line.find_first_of(sep_list, idx);
      if (TextView::npos == idx) { // nothing interesting left, consume all of @a line.
        break;
      } else if ('"' == line[idx]) { // quote, skip it and flip the quote state.
        in_quote_p = !in_quote_p;
        ++idx;
      } else if (SEP == line[idx]) { // separator.
        if (in_quote_p) { // quoted separator, skip and continue.
          ++idx;
        } else { // found token, finish up.
          break;
        }
      }
    }
  
    // clip the token from @a src and trim whitespace, quotes
    auto zret = line.take_prefix(idx).trim_if(&isspace).trim('"');
    return zret;
  }
  
  bool Table::parse(TextView src) {
    unsigned line_no = 0;
    while (src) {
      auto line = src.take_prefix_at('\n').ltrim_if(&isspace);
      ++line_no;
      // skip blank and comment lines.
      if (line.empty() || '#' == *line) {
        continue;
      }
  
      auto range_token = line.take_prefix_at(',');
      IPRange range{range_token};
      if (range.empty()) {
        std::cout << W().print("{} is not a valid range specification.", range_token);
        continue; // This is an error, real code should report it.
      }
  
      auto span = _arena.alloc(_size).rebind<std::byte>(); // need this broken out.
      Row row{span}; // store the original span to preserve it.
      for ( auto const& col : _columns) {
        auto token = this->token(line);
        if (col->needs_localized_token()) {
          token = this->localize(token);
        }
        if (! col->parse(token, span.subspan(0, col->size()))) {
          std::cout << W().print("Value \"{}\" at index {} on line {} is invalid.", token, col->idx(), line_no);
        }
        // drop reference to storage used by this column.
        span.remove_prefix(col->size());
      }
      _space.mark(range, std::move(row));
    }
    return true;
  }
  
  auto Table::find(IPAddr const &addr) -> Row * {
    auto spot = _space.find(addr);
    return spot == _space.end() ? nullptr : &std::get<1>(*spot);
  }
  
  bool operator == (Table::Row const&, Table::Row const&) { return false; }
  
  MemSpan<std::byte> Table::Row::span_for(Table::Property const&prop) const {
    return _data.subspan(prop.offset(), prop.size());
  }
  
  // ---
  
  /** A set of keys, each of which represents an independent property.
   * The set of keys must be specified at construction, keys not in the list are invalid.
   */
  class FlagGroupProperty : public Table::Property {
    using self_type = FlagGroupProperty; ///< Self reference type.
    using super_type = Table::Property; ///< Parent type.
  public:
    /** Construct with a @a name and a list of @a tags.
     *
     * @param name of the property
     * @param tags List of valid tags that represent attributes.
     *
     * Input tokens must consist of lists of tokens, each of which is one of the @a tags.
     * This is stored so that the exact set of tags present can be retrieved.
     */
    FlagGroupProperty(TextView const& name, std::initializer_list<TextView> tags);
  
    /** Check for a tag being present.
     *
     * @param idx Tag index, as specified in the constructor tag list.
     * @param row Row data from the @c Table.
     * @return @c true if the tag was present, @c false if not.
     */
    bool is_set(Table::Row const&row, unsigned idx) const;
  
  protected:
    size_t size() const override; ///< Storeage required in a row.
  
    /** Parse a token.
     *
     * @param token Token to parse (list of tags).
     * @param span Storage for parsed results.
     * @return @c true on a successful parse, @c false if not.
     */
    bool parse(TextView token, MemSpan<std::byte> span) override;
    /// List of tags.
    std::vector<TextView> _tags;
  };
  
  /** Enumeration property.
   * The tokens for this property are assumed to be from a limited set of tags. Each token, the
   * value for that row, must be one of those tags. The tags do not need to be specified, but will be
   * accumulated as needed. The property supports a maximum of 255 distinct tags.
   */
  class EnumProperty : public Table::Property {
    using self_type = EnumProperty; ///< Self reference type.
    using super_type = Table::Property; ///< Parent type.
    using store_type = __uint8_t; ///< Row storage type.
  public:
    using super_type::super_type; ///< Inherit super type constructors.
  
    /// @return The enumeration tag for this @a row.
    TextView operator() (Table::Row const& row) const;
  protected:
    std::vector<TextView> _tags; ///< Tags in the enumeration.
  
    /// @a return Size of required storage.
    size_t size() const override { return sizeof(store_type); }
  
    /** Parse a token.
     *
     * @param token Token to parse (an enumeration tag).
     * @param span Storage for parsed results.
     * @return @c true on a successful parse, @c false if not.
     */
    bool parse(TextView token, MemSpan<std::byte> span) override;
  };
  
  class StringProperty : public Table::Property {
    using self_type = StringProperty;
    using super_type = Table::Property;
  public:
    static constexpr size_t SIZE = sizeof(TextView);
    using super_type::super_type;
  
  protected:
    size_t size() const override { return SIZE; }
    bool parse(TextView token, MemSpan<std::byte> span) override;
    bool needs_localized_token() const override { return true; }
  };
  
  // ---
  bool StringProperty::parse(TextView token, MemSpan<std::byte> span) {
    memcpy(span.data(), &token, sizeof(token));
    return true;
  }
  
  FlagGroupProperty::FlagGroupProperty(TextView const& name, std::initializer_list<TextView> tags)
      : super_type(name)
  {
    _tags.reserve(tags.size());
    for ( auto const& tag : tags ) {
      _tags.emplace_back(tag);
    }
  }
  
  bool FlagGroupProperty::parse(TextView token, MemSpan<std::byte> span) {
    if ("-"_tv == token) { return true; } // marker for no flags.
    memset(span, 0);
    while (token) {
      auto tag = token.take_prefix_at(';');
      unsigned j = 0;
      for ( auto const& key : _tags ) {
        if (0 == strcasecmp(key, tag)) {
          span[j/8] |= (std::byte{1} << (j % 8));
          break;
        }
        ++j;
      }
      if (j > _tags.size()) {
        std::cout << W().print("Tag \"{}\" is not recognized.", tag);
        return false;
      }
    }
    return true;
  }
  
  bool FlagGroupProperty::is_set(Table::Row const&row, unsigned idx) const {
    auto sp = row.span_for(*this);
    return std::byte{0} != ((sp[idx / 8] >> (idx % 8)) & std::byte{1});
  }
  
  size_t FlagGroupProperty::size() const {
    return swoc::Scalar<8>(swoc::round_up(_tags.size())).count();
  }
  
  bool EnumProperty::parse(TextView token, MemSpan<std::byte> span) {
    // Already got one?
    auto spot = std::find_if(_tags.begin(), _tags.end(), [&](TextView const& tag) { return 0 == strcasecmp(token, tag); });
    if (spot == _tags.end()) { // nope, add it to the list.
      _tags.push_back(token);
      spot = std::prev(_tags.end());
    }
    span.rebind<uint8_t>()[0] = spot - _tags.begin();
    return true;
  }
  
  TextView EnumProperty::operator()(Table::Row const& row) const {
    auto idx = row.span_for(*this).rebind<store_type>()[0];
    return _tags[idx];
  }
  
  // ---
  
  TEST_CASE("IPSpace properties", "[libswoc][ip][ex][properties]") {
    Table table;
    auto flag_names = { "prod"_tv, "dmz"_tv, "internal"_tv};
    auto owner = table.add_column(std::make_unique<EnumProperty>("owner"));
    auto colo = table.add_column(std::make_unique<EnumProperty>("colo"));
    auto flags = table.add_column(std::make_unique<FlagGroupProperty>("flags"_tv, flag_names));
    [[maybe_unused]] auto description = table.add_column(std::make_unique<StringProperty>("Description"));
  
    TextView src = R"(10.1.1.0/24,asf,cmi,prod;internal,"ASF core net"
  192.168.28.0/25,asf,ind,prod,"Indy Net"
  192.168.28.128/25,asf,abq,dmz;internal,"Albuquerque zone"
  )";
  
    REQUIRE(true == table.parse(src));
    REQUIRE(3 == table.size());
    auto row = table.find(IPAddr{"10.1.1.56"});
    REQUIRE(nullptr != row);
    CHECK(true == flags->is_set(*row, 0));
    CHECK(false == flags->is_set(*row, 1));
    CHECK(true == flags->is_set(*row, 2));
    CHECK("asf"_tv == (*owner)(*row));
  
    row = table.find(IPAddr{"192.168.28.131"});
    REQUIRE(row != nullptr);
    CHECK("abq"_tv == (*colo)(*row));
  };
  
  