# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
 
import unittest
from datasketches import var_opt_sketch, var_opt_union, PyIntsSerDe, PyStringsSerDe

class VoTest(unittest.TestCase):
  def test_vo_example(self):
    k = 50  # a small value so we can easily fill the sketch
    vo = var_opt_sketch(k)

    # varopt sampling reduces to standard reservoir sampling
    # if the items are all equally weighted, although the
    # algorithm will be significantly slower than an optimized
    # reservoir sampler
    n = 5 * k
    for i in range(0, n):
      vo.update(i)

    # we can also add a heavy item, using a negative value for
    # easy filtering later.  Keep in mind that "heavy" is a
    # relative concept, so using a fixed multiple of n may not
    # be considered a heavy item for larger values of n
    vo.update(-1, 1000 * n)
    self.assertEqual(k, vo.k)
    self.assertEqual(k, vo.num_samples)
    self.assertEqual(n + 1, vo.n)
    self.assertFalse(vo.is_empty())

    # we can easily get the list of items in the sample
    items = list(vo)
    self.assertEqual(len(items), k)

    count = 0
    for tuple in vo:
      sample = tuple[0]
      weight = tuple[1]
      count = count + 1
    self.assertEqual(count, vo.num_samples)

    # we can also apply a predicate to the sketch to get an estimate
    # (with optimally minimal variance) of the subset sum of items
    # matching that predicate among the entire population

    # we'll use a lambda here, but any function operating on a single
    # item which returns a boolean value should work
    summary = vo.estimate_subset_sum(lambda x: x < 0)
    self.assertEqual(summary['estimate'], 1000 * n)
    self.assertEqual(summary['total_sketch_weight'], 1001 * n)

    # a regular function is similarly handled
    def geq_zero(x):
      return x >= 0
    summary = vo.estimate_subset_sum(geq_zero)
    self.assertEqual(summary['estimate'], n)
    self.assertEqual(summary['total_sketch_weight'], 1001 * n)

    # next we'll create a second, smaller sketch with
    # only heavier items relative to the previous sketch,
    # but with the sketch in sampling mode
    k2 = 5
    vo2 = var_opt_sketch(k2)
    # for weight, use the estimate of all items >=0 from before
    wt = summary['estimate']
    for i in range(0, k2 + 1):
      vo2.update((2 * n) + i, wt)

    # now union the sketches, demonstrating how the
    # union's k may not be equal to that of either
    # input value
    union = var_opt_union(k)
    union.update(vo)
    union.update(vo2)

    result = union.get_result()
    self.assertEqual(n + k2 + 2, result.n)
    self.assertFalse(result.is_empty())
    self.assertGreater(result.k, k2)
    self.assertLess(result.k, k)

    # we can compare what information is available from both
    # the union and a sketch.
    print(union)

    # if we want to print the list of items, there must be a
    # __str__() method for each item (which need not be the same
    # type; they're all generic python objects when used from
    # python), otherwise you may trigger an exception.
    # to_string() is provided as a convenience to avoid direct
    # calls to __str__() with parameters.
    print(result.to_string(True))

    # finally, we can serialize the sketch by providing an
    # appropriate serde class.
    expected_size = result.get_serialized_size_bytes(PyIntsSerDe())
    b = result.serialize(PyIntsSerDe())
    self.assertEqual(expected_size, len(b))

    # if we try to deserialize with the wrong serde, things break
    try:
      var_opt_sketch.deserialize(b, PyStringsSerDe())
      self.fail()
    except:
      # expected; do nothing
      self.assertTrue(True)
      
    # using the correct serde gives us back a copy of the original
    rebuilt = var_opt_sketch.deserialize(b, PyIntsSerDe())
    self.assertEqual(result.k, rebuilt.k)
    self.assertEqual(result.num_samples, rebuilt.num_samples)
    self.assertEqual(result.n, rebuilt.n)
    summary1 = result.estimate_subset_sum(geq_zero)
    summary2 = rebuilt.estimate_subset_sum(geq_zero)
    self.assertEqual(summary1['estimate'], summary2['estimate'])
    self.assertEqual(summary1['total_sketch_weight'], summary2['total_sketch_weight'])

    # check that printing works as expected
    self.assertGreater(len(result.to_string(True)), 0)
    self.assertEqual(len(result.__str__()), len(result.to_string()))

    self.assertGreater(len(union.to_string()), 0)
    self.assertEqual(len(union.__str__()), len(union.to_string()))



if __name__ == '__main__':
  unittest.main()
