my class X::Enum::NoValue {...};
my class X::Constructor::BadType {...}
# Method that we have on enumeration types.
my role Enumeration {
    has $.key;
    has $.value;
    has int $!index;

    method new { X::Constructor::BadType.new(type => self.WHAT).throw }

    method enums() { self.^enum_values.Map }

    multi method kv(::?CLASS:D:) { ($!key, $!value) }
    method pair(::?CLASS:D:) { $!key => $!value }

    multi method gist(::?CLASS:D:) { $!key                     }
    multi method raku(::?CLASS:D:) { self.^name ~ '::' ~ $!key }

    multi method pick(::?CLASS:U:)       { self.^enum_value_list.pick     }
    multi method pick(::?CLASS:U: \n)    { self.^enum_value_list.pick(n)  }
    multi method pick(::?CLASS:D: *@pos) { self xx +?( @pos[0] // 1 )     }
    multi method roll(::?CLASS:U:)       { self.^enum_value_list.roll     }
    multi method roll(::?CLASS:U: \n)    { self.^enum_value_list.roll(n)  }
    multi method roll(::?CLASS:D: *@pos) { self xx +?( @pos[0] // 1 )     }

    multi method Numeric(::?CLASS:D:) { $!value.Numeric }
    multi method Int(::?CLASS:D:)     { $!value.Int }
    multi method Real(::?CLASS:D:)    { $!value.Real }

    multi method WHICH(::?CLASS:D: --> ValueObjAt:D) {
        nqp::box_s(
          nqp::concat(self.^name,nqp::concat("|",$!index)),
          ValueObjAt
        )
    }

    multi method ACCEPTS(::?CLASS:D: ::?CLASS:D $v) { self === $v }

    method !FROM-VALUE(Mu \val) {
        my $res := Nil;
        my $dcval := nqp::decont(val);
        # If value is a mixin of enum try to pull out the mixed in value first
        if $dcval.^is_mixin {
            my $attr_name := '$!' ~ self.^name;
            if $dcval.^has_attribute($attr_name) {
                my $mixin_value := nqp::getattr($dcval, $dcval.WHAT, $attr_name);
                return $mixin_value if nqp::istype($mixin_value, ::?CLASS);
            }
        }
        if nqp::istype($dcval, ::?CLASS) {
            $res := $dcval;
        }
        elsif nqp::isconcrete($dcval) {
            $res := self.^enum_from_value($dcval);
        }
        $res // X::Enum::NoValue.new(:type(self.WHAT), :value($dcval)).Failure
    }

    proto method CALL-ME(Mu) {*}
    multi method CALL-ME(Mu \val) { self!FROM-VALUE(val) }

    proto method COERCE(Mu) {*}
    multi method COERCE(Mu \val) { self!FROM-VALUE(val) }

    method pred(::?CLASS:D:) {
        nqp::getattr_i(self,::?CLASS,'$!index')
          ?? nqp::atpos(
               nqp::getattr(self.^enum_value_list,List,'$!reified'),
               nqp::sub_i(nqp::getattr_i(self,::?CLASS,'$!index'),1)
             )
          !! self
    }
    method succ(::?CLASS:D:) {
        my $values := nqp::getattr(self.^enum_value_list,List,'$!reified');
        nqp::islt_i(
          nqp::getattr_i(self,::?CLASS,'$!index'),
          nqp::sub_i(nqp::elems($values),1)
        ) ?? nqp::atpos(
               $values,
               nqp::add_i(nqp::getattr_i(self,::?CLASS,'$!index'),1)
             )
          !! self
    }
}

# Methods that we also have if the base type of an enumeration is
# Numeric.
my role NumericEnumeration {
    multi method Str(::?CLASS:D:) {
        self.key
    }
}
my role StringyEnumeration {
    multi method Str(::?CLASS:D:) {
        self.value
    }
}
my role NumericStringyEnumeration {
    multi method Str(::?CLASS:D:) {
        self.key
    }
}

sub ENUM_VALUES(*@args --> Map:D) is implementation-detail {
    my Mu $prev = -1;
    my $res := nqp::hash;

    nqp::istype($_,Pair)
      ?? nqp::bindkey($res, .key, nqp::decont($prev = .value))
      !! nqp::bindkey($res,   $_, nqp::decont($prev = $prev.succ))
      for @args;

    nqp::p6bindattrinvres(nqp::create(Map),Map,'$!storage',$res)
}

Metamodel::EnumHOW.set_composalizer(-> $type, $name, @enum_values {
    my Mu $r := Metamodel::ParametricRoleHOW.new_type(:name($name));
    $r.^add_attribute(Attribute.new(
        :name('$!' ~ $name), :type(nqp::decont($type)),
        :has_accessor(1), :package($r)));
    for @enum_values {
        my $key   = $_.key;
        my $value = $_.value;
        my $meth = method () { self."$name"() == $value }
        $meth.set_name($key);
        $r.^add_method($key, $meth);
    }
    $r.^set_body_block( -> |c {nqp::list($r,nqp::hash('$?CLASS',c<$?CLASS>))});
    $r.^compose;
    $r
});

# We use this one because, for example, Int:D === Int:D, has an optimization
# that simply unboxes the values. That's no good for us, since two different
# Enumeration:Ds could have the same Int:D value.
multi infix:<===> (Enumeration:D $a, Enumeration:D $b --> Bool:D) {
    nqp::hllbool(nqp::eqaddr($a,$b))
}

# vim: expandtab shiftwidth=4
