# This test file tests the following set operators:
#   (^)     set symmetric difference (Texas)
#   ⊖       set symmetric difference

proto sub infix:<(^)>(|) is pure {*}
multi sub infix:<(^)>()               { set() }
multi sub infix:<(^)>(QuantHash:D $a) { $a    } # Set/Bag/Mix

multi sub infix:<(^)>(Setty:D $a, Setty:D $b) {
    nqp::if(
      (my \araw := $a.RAW-HASH) && nqp::elems(araw),
      nqp::if(
        (my \braw := $b.RAW-HASH) && nqp::elems(braw),
        nqp::stmts(                            # both are initialized
          nqp::if(
            nqp::islt_i(nqp::elems(araw),nqp::elems(braw)),
            nqp::stmts(                        # a smallest, iterate over it
              (my $iter  := nqp::iterator(araw)),
              (my $elems := nqp::clone(braw))
            ),
            nqp::stmts(                        # b smallest, iterate over that
              ($iter  := nqp::iterator(braw)),
              ($elems := nqp::clone(araw))
            )
          ),
          nqp::while(
            $iter,
            nqp::if(                           # remove if in both
              nqp::existskey($elems,nqp::iterkey_s(nqp::shift($iter))),
              nqp::deletekey($elems,nqp::iterkey_s($iter)),
              nqp::bindkey($elems,nqp::iterkey_s($iter),nqp::iterval($iter))
            )
          ),
          nqp::create($a.WHAT).SET-SELF($elems)
        ),
        $a                                     # b empty, so a
      ),
      nqp::if(                                 # a empty, so b
        nqp::istype($a,Set), $b.Set, $b.SetHash
      )
    )
}
multi sub infix:<(^)>(Setty:D $a, Mixy:D  $b) { $a.Mixy  (^) $b }
multi sub infix:<(^)>(Setty:D $a, Baggy:D $b) { $a.Baggy (^) $b }

multi sub infix:<(^)>(Mixy:D $a, Mixy:D $b) {
    nqp::if(
      (my \araw := $a.RAW-HASH) && nqp::elems(araw),
      nqp::if(
        (my \braw := $b.RAW-HASH) && nqp::elems(braw),
        nqp::stmts(                            # both are initialized
          nqp::if(
            nqp::islt_i(nqp::elems(araw),nqp::elems(braw)),
            nqp::stmts(                        # a smallest, iterate over it
              (my $iter  := nqp::iterator(my $base := araw)),
              (my $elems := nqp::clone(braw))
            ),
            nqp::stmts(                        # b smallest, iterate over that
              ($iter  := nqp::iterator($base := braw)),
              ($elems := nqp::clone(araw))
            )
          ),
          nqp::while(
            $iter,
            nqp::if(
              nqp::existskey($elems,nqp::iterkey_s(nqp::shift($iter))),
              nqp::if(
                (my \diff := nqp::getattr(nqp::iterval($iter),Pair,'$!value')
                  - nqp::getattr(
                      nqp::atkey($elems,nqp::iterkey_s($iter)),
                      Pair,
                      '$!value'
                    )
                ),
                nqp::bindkey(
                  $elems,
                  nqp::iterkey_s($iter),
                  nqp::p6bindattrinvres(
                    nqp::clone(nqp::iterval($iter)),Pair,'$!value',abs(diff)
                  )
                ),
                nqp::deletekey($elems,nqp::iterkey_s($iter))
              ),
              nqp::bindkey(
                $elems,
                nqp::iterkey_s($iter),
                nqp::clone(nqp::iterval($iter))
              )
            )
          ),
          nqp::create($a.WHAT).SET-SELF($elems)
        ),
        nqp::create($a.WHAT).SET-SELF(        # b empty, so a
          Rakudo::QuantHash.MIX-CLONE-ALL-POSITIVE(araw)
        )
      ),
      nqp::if(
        (my \raw := $b.RAW-HASH) && nqp::elems(raw),
        nqp::create($a.WHAT).SET-SELF(        # a empty, so b
          Rakudo::QuantHash.MIX-CLONE-ALL-POSITIVE(raw)
        ),
        $a                                    # a and b empty
      )
    )
}
multi sub infix:<(^)>(Mixy:D $a, Baggy:D $b) { $a (^) $b.Mix }
multi sub infix:<(^)>(Mixy:D $a, Setty:D $b) { $a (^) $b.Mix }

multi sub infix:<(^)>(Baggy:D $a, Mixy:D  $b) { $a.Mixy (^) $b }
multi sub infix:<(^)>(Baggy:D $a, Baggy:D $b) {
    nqp::if(
      (my \araw := $a.RAW-HASH) && nqp::elems(araw),
      nqp::if(
        (my \braw := $b.RAW-HASH) && nqp::elems(braw),
        nqp::stmts(                            # both are initialized
          nqp::if(
            nqp::islt_i(nqp::elems(araw),nqp::elems(braw)),
            nqp::stmts(                        # a smallest, iterate over it
              (my $iter  := nqp::iterator(my $base := araw)),
              (my $elems := nqp::clone(braw))
            ),
            nqp::stmts(                        # b smallest, iterate over that
              ($iter  := nqp::iterator($base := braw)),
              ($elems := nqp::clone(araw))
            )
          ),
          nqp::while(
            $iter,
            nqp::if(                           # remove if in both
              nqp::existskey($elems,nqp::iterkey_s(nqp::shift($iter))),
              nqp::if(
                (my int $diff = nqp::sub_i(
                  nqp::getattr(nqp::iterval($iter),Pair,'$!value'),
                  nqp::getattr(
                    nqp::atkey($elems,nqp::iterkey_s($iter)),
                    Pair,
                    '$!value'
                  )
                )),
                nqp::bindkey(
                  $elems,
                  nqp::iterkey_s($iter),
                  nqp::p6bindattrinvres(
                    nqp::clone(nqp::iterval($iter)),
                    Pair,
                    '$!value',
                    nqp::abs_i($diff)
                  )
                ),
                nqp::deletekey($elems,nqp::iterkey_s($iter))
              ),
              nqp::bindkey($elems,nqp::iterkey_s($iter),nqp::iterval($iter))
            )
          ),
          nqp::create($a.WHAT).SET-SELF($elems)
        ),
        $a                                     # b empty, so a
      ),
      nqp::if(                                 # a empty, so b
        nqp::istype($a,Bag), $b.Bag, $b.BagHash
      )
    )
}
multi sub infix:<(^)>(Baggy:D $a, Setty:D $b) { $a (^) $b.Bag }

multi sub infix:<(^)>(Map:D \a, Map:D \b) {
    nqp::if(
      nqp::elems((my \elems := Rakudo::QuantHash.COERCE-MAP-TO-SET(a))),
      nqp::if(                                    # $a has elems
        (my \iter :=
          nqp::iterator(nqp::getattr(nqp::decont(b),Map,'$!storage'))),
        nqp::stmts(
          nqp::if(                                # both have elems
            nqp::istype(b,Hash::Object),
            nqp::while(                           # object hash
              iter,
              nqp::if(
                nqp::getattr(nqp::iterval(nqp::shift(iter)),Pair,'$!value'),
                nqp::if(                          # should be checked
                  nqp::existskey(elems,nqp::iterkey_s(iter)),
                  nqp::deletekey(elems,nqp::iterkey_s(iter)),# remove existing
                  nqp::bindkey(                   # add new
                    elems,
                    nqp::iterkey_s(iter),
                    nqp::getattr(nqp::iterval(iter),Pair,'$!key')
                  )
                )
              )
            ),
            nqp::while(                           # ordinary hash
              iter,
              nqp::if(
                nqp::iterval(nqp::shift(iter)),
                nqp::if(                          # should be checked
                  nqp::existskey(
                    elems,
                    (my \which := nqp::iterkey_s(iter).WHICH)
                  ),
                  nqp::deletekey(elems,which),    # remove existing
                  nqp::bindkey(elems,which,nqp::iterkey_s(iter)) # add new
                )
              )
            )
          ),
          nqp::create(Set).SET-SELF(elems)        # done
        ),
        nqp::create(Set).SET-SELF(elems)          # nothing right, so make left
      ),
      b.Set                                       # nothing left, coerce right
    )
}

multi sub infix:<(^)>(Failure:D $a, Any) { $a.throw }
multi sub infix:<(^)>(Any, Failure:D $b) { $b.throw }
multi sub infix:<(^)>(Any \a, Any \b) {
    nqp::istype(a,Mixy)
      ?? a (^) b.Mix
      !! nqp::istype(b,Mixy)
        ?? a.Mix (^) b
        !! nqp::istype(a,Baggy)
          ?? a (^) b.Bag
          !! nqp::istype(b,Baggy)
            ?? a.Bag (^) b
            !! a.Set (^) b.Set
}

multi sub infix:<(^)>(+@p) {   # also Any

    # positions / size in minmax info
    my constant COUNT   = 0;
    my constant LOWEST  = 1;
    my constant HIGHEST = 2;
    my constant SIZE    = 3;

    # basic minmax for new keys
    my $init-minmax := nqp::setelems(nqp::create(IterationBuffer),SIZE);
    nqp::bindpos($init-minmax,COUNT,1);

    # handle key that has been seen before for given value
    sub handle-existing(Mu \elems, Mu \iter, \value --> Nil) {
        my \minmax := nqp::getattr(
          nqp::atkey(elems,nqp::iterkey_s(iter)),Pair,'$!value'
        );
        nqp::bindpos(minmax,COUNT,nqp::add_i(nqp::atpos(minmax,COUNT),1));

        nqp::if(
          value > nqp::atpos(minmax,HIGHEST),
          nqp::stmts(
            nqp::bindpos(minmax,LOWEST,nqp::atpos(minmax,HIGHEST)),
            nqp::bindpos(minmax,HIGHEST,value)
          ),
          nqp::if(
            nqp::not_i(nqp::defined(nqp::atpos(minmax,LOWEST)))
              || value > nqp::atpos(minmax,LOWEST),
            nqp::bindpos(minmax,LOWEST,value)
          )
        );
    }

    # handle key that has not yet been seen
    sub handle-new(Mu \elems, Mu \iter, \pair, \value) {
        my \minmax := nqp::clone($init-minmax);
        nqp::bindpos(minmax,HIGHEST,value);
        nqp::bindkey(
          elems,
          nqp::iterkey_s(iter),
          nqp::p6bindattrinvres(pair,Pair,'$!value',minmax)
        )
    }

    nqp::if(
      (my $params := @p.iterator).is-lazy,
      Any.fail-iterator-cannot-be-lazy('symmetric diff',''), # bye bye

      nqp::stmts(                                # fixed list of things to diff
        (my \elems := nqp::create(Rakudo::Internals::IterationSet)),
        (my $type  := Set),
        (my int $pseen = 0),

        nqp::until(
          nqp::eqaddr((my \p := $params.pull-one),IterationEnd),

          nqp::stmts(                            # not done parsing
            nqp::unless(
              $pseen,
              (my $mutable :=
                   nqp::eqaddr(p.WHAT,MixHash)
                || nqp::eqaddr(p.WHAT,BagHash)
                || nqp::eqaddr(p.WHAT,SetHash)
              )
            ),
            ++$pseen,
            nqp::if(
              nqp::istype(p,Baggy),

              nqp::stmts(                        # Mixy/Baggy semantics apply
                nqp::unless(
                  nqp::istype($type,Mix),
                  ($type := nqp::if(nqp::istype(p,Mixy),Mix,Bag))
                ),
                nqp::if(
                  (my $raw := p.RAW-HASH) && (my $iter := nqp::iterator($raw)),
                  nqp::stmts(                    # something to process
                    nqp::while(
                      $iter,
                      nqp::if(
                        nqp::existskey(
                          elems,
                          nqp::iterkey_s(nqp::shift($iter))
                        ),
                        handle-existing(         # seen this element before
                          elems,
                          $iter,
                          nqp::getattr(nqp::iterval($iter),Pair,'$!value')
                        ),
                        handle-new(              # new element
                          elems,
                          $iter,
                          nqp::clone(nqp::iterval($iter)),
                          nqp::getattr(nqp::iterval($iter),Pair,'$!value')
                        )
                      )
                    )
                  )
                )
              ),

              nqp::stmts(                        # not a Baggy/Mixy, assume Set
                ($raw := nqp::if(nqp::istype(p,Setty),p,p.Set).RAW-HASH)
                  && ($iter := nqp::iterator($raw)),
                nqp::while(                      # something to process
                  $iter,
                  nqp::if(
                    nqp::existskey(elems,nqp::iterkey_s(nqp::shift($iter))),
                    handle-existing(             # seen this element before
                      elems,
                      $iter,
                      1
                    ),
                    handle-new(                  # new element
                      elems,
                      $iter,
                      nqp::p6bindattrinvres(
                        nqp::create(Pair),Pair,'$!key',nqp::iterval($iter)),
                      1
                    )
                  )
                )
              )
            )
          )
        ),

        ($iter := nqp::iterator(elems)),         # start post-processing
        nqp::if(
          nqp::istype($type,Set),
          nqp::while(                            # need to create a Set
            $iter,
            nqp::if(
              nqp::ifnull(
                nqp::atpos(
                  nqp::getattr(nqp::iterval(nqp::shift($iter)),Pair,'$!value'),
                  LOWEST
                ),
                0
              ) == 1,
              nqp::deletekey(elems,nqp::iterkey_s($iter)),     # seen > 1
              nqp::bindkey(                                    # only once
                elems,                                         # convert to
                nqp::iterkey_s($iter),                         # Setty format
                nqp::getattr(nqp::iterval($iter),Pair,'$!key')
              )
            )
          ),
          nqp::if(
            nqp::istype($type,Mix),
            nqp::while(                          # convert to Mixy semantics
              $iter,
              nqp::stmts(
                (my $minmax :=
                  nqp::getattr(nqp::iterval(nqp::shift($iter)),Pair,'$!value')),
                nqp::if(
                  nqp::islt_i(nqp::atpos($minmax,COUNT),$pseen),
                  handle-existing(elems,$iter,0)  # absentee == value 0 seen
                ),
                nqp::if(
                  nqp::ifnull(nqp::atpos($minmax,LOWEST),0)
                   == nqp::atpos($minmax,HIGHEST),
                  nqp::deletekey(elems,nqp::iterkey_s($iter)),   # top 2 same
                  nqp::bindattr(                                 # there's a
                    nqp::iterval($iter),                         # difference
                    Pair,                                        # so convert
                    '$!value',
                    nqp::atpos($minmax,HIGHEST)
                      - nqp::ifnull(nqp::atpos($minmax,LOWEST),0)
                  )
                )
              )
            ),
            nqp::while(                          # convert to Baggy semantics
              $iter,
              nqp::if(
                nqp::ifnull(
                  nqp::atpos(
                    ($minmax := nqp::getattr(
                      nqp::iterval(nqp::shift($iter)),Pair,'$!value')),
                    LOWEST
                  ),
                  0
                ) == nqp::atpos($minmax,HIGHEST),
                nqp::deletekey(elems,nqp::iterkey_s($iter)),     # top 2 same
                nqp::bindattr(                                   # there's a
                  nqp::iterval($iter),                           # difference
                  Pair,                                          # so convert
                  '$!value',
                  nqp::atpos($minmax,HIGHEST)
                    - nqp::ifnull(nqp::atpos($minmax,LOWEST),0)
                )
              )
            )
          )
        ),
        nqp::if(                                 # set to mutable if so started
          $mutable,
          $type := nqp::if(
            nqp::eqaddr($type,Mix),
            MixHash,
            nqp::if(nqp::eqaddr($type,Bag),BagHash,SetHash)
          )
        ),
        nqp::create($type).SET-SELF(elems)
      )
    )
}

# U+2296 CIRCLED MINUS
my constant &infix:<⊖> := &infix:<(^)>;

# vim: expandtab shiftwidth=4
