package Sidef::Types::Set::Set {

    use utf8;
    use 5.016;

    use parent qw(
      Sidef::Types::Hash::Hash
    );

    use overload
      q{bool} => sub { scalar(CORE::keys(%{$_[0]})) },
      q{0+}   => sub { scalar(CORE::keys(%{$_[0]})) },
      q{@{}}  => sub { [CORE::values(%{$_[0]})] },
      q{""}   => \&_dump;

    use Sidef::Types::Block::Block;
    use Sidef::Types::Bool::Bool;
    use Sidef::Types::Number::Number;

    my $serialize = sub {
        my ($obj) = @_;
        my $key = ref($obj) ? (UNIVERSAL::can($obj, 'dump') ? $obj->dump : $obj) : ($obj // 'nil');
        "$key";
    };

    sub new {
        my (undef, @objects) = @_;
        bless {map { $serialize->($_) => $_ } @objects};
    }

    *call = \&new;

    sub get_value {
        my %addr;

        my $sub = sub {
            my ($obj) = @_;

            my $refaddr = Scalar::Util::refaddr($obj);

            exists($addr{$refaddr})
              && return $addr{$refaddr};

            my @set;
            $addr{$refaddr} = \@set;

            foreach my $v (CORE::values(%$obj)) {
                CORE::push(@set, (index(ref($v), 'Sidef::') == 0) ? $v->get_value : $v);
            }

            $addr{$refaddr};
        };

        no warnings 'redefine';
        local *Sidef::Types::Set::Set::get_value = $sub;
        $sub->($_[0]);
    }

    sub concat {
        my ($A, $B) = @_;

        ref($A) eq ref($B)
          ? bless({%$A, %$B}, ref($A))
          : bless({%$A, $serialize->($B) => $B}, ref($A));
    }

    sub union {
        my ($A, $B) = @_;

        if (ref($B) ne __PACKAGE__) {
            $B = $B->to_set;
        }

        $A->SUPER::union($B);
    }

    *or = \&union;

    sub intersection {
        my ($A, $B) = @_;

        if (ref($B) ne __PACKAGE__) {
            $B = $B->to_set;
        }

        $A->SUPER::intersection($B);
    }

    *and = \&intersection;

    sub difference {
        my ($A, $B) = @_;

        if (ref($B) ne __PACKAGE__) {
            $B = $B->to_set;
        }

        $A->SUPER::difference($B);
    }

    *sub  = \&difference;
    *diff = \&difference;

    sub symmetric_difference {
        my ($A, $B) = @_;

        if (ref($B) ne __PACKAGE__) {
            $B = $B->to_set;
        }

        $A->SUPER::symmetric_difference($B);
    }

    *xor     = \&symmetric_difference;
    *symdiff = \&symmetric_difference;

    sub append {
        my ($self, @objects) = @_;

        foreach my $obj (@objects) {
            my $key = $serialize->($obj);
            $self->{$key} = $obj;
        }

        $self;
    }

    *add  = \&append;
    *push = \&append;

    sub pop {
        my ($self) = @_;
        CORE::delete(@{$self}{(CORE::keys(%$self))[-1]});
    }

    sub shift {
        my ($self) = @_;
        CORE::delete(@{$self}{(CORE::keys(%$self))[0]});
    }

    sub delete {
        my ($self, @objects) = @_;
        CORE::delete(@{$self}{map { $serialize->($_) } @objects});
    }

    *remove  = \&delete;
    *discard = \&delete;

    sub map {
        my ($self, $block) = @_;

        $block //= Sidef::Types::Block::Block::IDENTITY;

        my %new;
        foreach my $key (CORE::keys(%$self)) {
            foreach my $value ($block->run($self->{$key})) {
                $new{$serialize->($value)} = $value;
            }
        }

        bless \%new, ref($self);
    }

    sub map_2d {
        my ($self, $block) = @_;

        $block //= Sidef::Types::Block::Block::ARRAY_IDENTITY;

        my %new;
        foreach my $key (CORE::keys(%$self)) {
            foreach my $value ($block->run(@{$self->{$key}})) {
                $new{$serialize->($value)} = $value;
            }
        }

        bless \%new, ref($self);
    }

    sub collect {
        my ($self, $block) = @_;

        my @array;
        foreach my $value (CORE::values(%$self)) {
            CORE::push(@array, $block->run($value));
        }

        Sidef::Types::Array::Array->new(\@array);
    }

    sub grep {
        my ($self, $block) = @_;

        $block //= Sidef::Types::Block::Block::IDENTITY;

        my %new;
        foreach my $key (CORE::keys(%$self)) {
            my $value = $self->{$key};
            if ($block->run($value)) {
                $new{$key} = $value;
            }
        }

        bless \%new, ref($self);
    }

    *select = \&grep;

    sub grep_2d {
        my ($self, $block) = @_;

        my %new;
        foreach my $key (CORE::keys(%$self)) {
            my $value = $self->{$key};
            if ($block->run(@$value)) {
                $new{$key} = $value;
            }
        }

        bless \%new, ref($self);
    }

    sub count_by {
        my ($self, $block) = @_;

        my $count = 0;
        foreach my $value (CORE::values(%$self)) {
            if ($block->run($value)) {
                ++$count;
            }
        }

        Sidef::Types::Number::Number::_set_int($count);
    }

    sub count {
        my ($self, $obj) = @_;

        if (ref($obj) eq 'Sidef::Types::Block::Block') {
            goto &count_by;
        }

        my $key = $serialize->($obj);
        if (CORE::exists($self->{$key})) {
            return Sidef::Types::Number::Number::ONE;
        }

        return Sidef::Types::Number::Number::ZERO;
    }

    sub delete_if {
        my ($self, $block) = @_;

        foreach my $key (CORE::keys(%$self)) {
            if ($block->run($self->{$key})) {
                CORE::delete($self->{$key});
            }
        }

        $self;
    }

    sub delete_first_if {
        my ($self, $block) = @_;

        foreach my $key (CORE::keys(%$self)) {
            if ($block->run($self->{$key})) {
                CORE::delete($self->{$key});
                last;
            }
        }

        $self;
    }

    sub iter {
        my ($self) = @_;

        my $i      = 0;
        my @values = CORE::values(%$self);
        Sidef::Types::Block::Block->new(
            code => sub {
                $values[$i++];
            }
        );
    }

    sub each {
        my ($self, $block) = @_;

        foreach my $value (CORE::values(%$self)) {
            $block->run($value);
        }

        $self;
    }

    sub each_2d {
        my ($self, $block) = @_;

        foreach my $value (CORE::values(%$self)) {
            $block->run(@$value);
        }

        $self;
    }

    sub sort_by {
        my ($self, $block) = @_;
        $self->values->sort_by($block);
    }

    sub sort {
        my ($self, $block) = @_;
        $self->values->sort(defined($block) ? $block : ());
    }

    sub min {
        my ($self) = @_;
        $self->values->min;
    }

    sub max {
        my ($self) = @_;
        $self->values->max;
    }

    sub max_by {
        my ($self, $block) = @_;
        $self->values->max_by($block);
    }

    sub min_by {
        my ($self, $block) = @_;
        $self->values->min_by($block);
    }

    sub sum {
        my ($self, $block) = @_;
        $self->to_a->sum($block);
    }

    *sum_by = \&sum;

    sub prod {
        my ($self, $block) = @_;
        $self->to_a->prod($block);
    }

    *prod_by = \&prod;

    sub sum_2d {
        my ($self, $block) = @_;
        $self->to_a->sum_2d($block);
    }

    sub prod_2d {
        my ($self, $block) = @_;
        $self->to_a->prod_2d($block);
    }

    sub has {
        my ($self, $obj) = @_;
        my $key = $serialize->($obj);
        CORE::exists($self->{$key})
          ? (Sidef::Types::Bool::Bool::TRUE)
          : (Sidef::Types::Bool::Bool::FALSE);
    }

    *haskey   = \&has;
    *has_key  = \&has;
    *exists   = \&has;
    *include  = \&has;
    *includes = \&has;
    *contain  = \&has;
    *contains = \&has;

    sub is_subset {
        my ($A, $B) = @_;

        if (ref($B) ne __PACKAGE__) {
            $B = $B->to_set;
        }

        foreach my $key (CORE::keys(%$A)) {
            if (!CORE::exists($B->{$key})) {
                return Sidef::Types::Bool::Bool::FALSE;
            }
        }

        return Sidef::Types::Bool::Bool::TRUE;
    }

    sub is_superset {
        my ($A, $B) = @_;

        if (ref($B) ne __PACKAGE__) {
            $B = $B->to_set;
        }

        foreach my $key (CORE::keys(%$B)) {
            if (!CORE::exists($A->{$key})) {
                return Sidef::Types::Bool::Bool::FALSE;
            }
        }

        return Sidef::Types::Bool::Bool::TRUE;
    }

    sub contains_all {
        my ($self, @objects) = @_;

        foreach my $obj (@objects) {
            if (!CORE::exists($self->{$serialize->($obj)})) {
                return Sidef::Types::Bool::Bool::FALSE;
            }
        }

        return Sidef::Types::Bool::Bool::TRUE;
    }

    sub join {
        my ($self, @rest) = @_;
        $self->to_a->join(@rest);
    }

    sub all {
        my ($self, $block) = @_;

        $block //= Sidef::Types::Block::Block::IDENTITY;

        foreach my $key (CORE::keys(%$self)) {
            $block->run($self->{$key})
              || return Sidef::Types::Bool::Bool::FALSE;
        }

        Sidef::Types::Bool::Bool::TRUE;
    }

    sub any {
        my ($self, $block) = @_;

        $block //= Sidef::Types::Block::Block::IDENTITY;

        foreach my $key (CORE::keys(%$self)) {
            $block->run($self->{$key})
              && return Sidef::Types::Bool::Bool::TRUE;
        }

        Sidef::Types::Bool::Bool::FALSE;
    }

    sub none {
        my ($self, $block) = @_;

        $block //= Sidef::Types::Block::Block::IDENTITY;

        foreach my $key (CORE::keys(%$self)) {
            $block->run($self->{$key})
              && return Sidef::Types::Bool::Bool::FALSE;
        }

        Sidef::Types::Bool::Bool::TRUE;
    }

    sub _dump {
        my %addr;    # keeps track of dumped objects

        my $sub = sub {
            my ($obj) = @_;

            my $refaddr = Scalar::Util::refaddr($obj);

            exists($addr{$refaddr})
              and return $addr{$refaddr};

            my @values = CORE::values(%$obj);

            $addr{$refaddr} = "Set(#`($refaddr)...)";

            my $s;
            "Set(" . CORE::join(', ', map { (ref($_) && ($s = UNIVERSAL::can($_, 'dump'))) ? $s->($_) : ($_ // 'nil') } @values) . ')';
        };

        no warnings 'redefine';
        local *Sidef::Types::Set::Set::dump = $sub;
        $sub->($_[0]);
    }

    sub dump {
        Sidef::Types::String::String->new($_[0]->_dump);
    }

    sub to_a {
        my ($self) = @_;
        Sidef::Types::Array::Array->new(ref($self) ? [CORE::values(%$self)] : $self);
    }

    *values   = \&to_a;
    *to_array = \&to_a;

    sub to_list {
        my ($self) = @_;
        CORE::values(%$self);
    }

    sub to_bag {
        my ($self) = @_;
        Sidef::Types::Set::Bag->new(CORE::values(%$self));
    }

    sub to_set {
        $_[0];
    }

    {
        no strict 'refs';

        *{__PACKAGE__ . '::' . '+'}   = \&concat;
        *{__PACKAGE__ . '::' . '<<'}  = \&append;
        *{__PACKAGE__ . '::' . '∪'}   = \&union;
        *{__PACKAGE__ . '::' . '|'}   = \&union;
        *{__PACKAGE__ . '::' . '&'}   = \&intersection;
        *{__PACKAGE__ . '::' . '∩'}   = \&intersection;
        *{__PACKAGE__ . '::' . '-'}   = \&difference;
        *{__PACKAGE__ . '::' . '∖'}   = \&difference;
        *{__PACKAGE__ . '::' . '^'}   = \&symmetric_difference;
        *{__PACKAGE__ . '::' . '<='}  = \&is_subset;
        *{__PACKAGE__ . '::' . '≤'}   = \&is_subset;
        *{__PACKAGE__ . '::' . '>='}  = \&is_superset;
        *{__PACKAGE__ . '::' . '≥'}   = \&is_superset;
        *{__PACKAGE__ . '::' . '⊆'}   = \&is_subset;
        *{__PACKAGE__ . '::' . '⊇'}   = \&is_superset;
        *{__PACKAGE__ . '::' . '...'} = \&to_list;
        *{__PACKAGE__ . '::' . '∋'}   = \&contains;
        *{__PACKAGE__ . '::' . '∌'}   = sub { $_[0]->contains($_[1])->not };
        *{__PACKAGE__ . '::' . '≡'}   = \&Sidef::Types::Hash::Hash::eq;
    }
};

1
