package CType::Enum;

use 5.6.0;
use strict;
use warnings;

use CType;
use CType::Fundamental;
use CType::Structural;

use CExpr::Integer;
use CDecl::Enumerator;

our @ISA = qw/CType::Structural CType::Fundamental::Enum/;

sub new
  {
    my $this = shift;
    my $class = ref($this) || $this;
    my $members = shift;
    my $attributes = shift;
    my $location = shift;

    my $self = {members => $members,
                attributes => $attributes,
                file => $location->{file},
                line => $location->{line},
                pos => $location->{pos},
                alignment_exprs => [],
               };
    bless $self, $class;

    $self->process_attributes($attributes);

    foreach my $attribute (@$attributes)
      {
        if ($attribute->name eq 'packed')
          {
            $self->{packed} = 1;
          }
      }

    return $self;
  }

sub layout
  {
    my $self = shift;
    my $accept_incomplete = shift;
    my $namespace = shift;

    return if $self->{completed};

    # Stop recursion
    return if $self->{laying_out};
    $self->{laying_out} = 1;

    # Enums must always be complete
    $accept_incomplete = 0;

    my $min_value = undef;
    my $max_value = undef;
    my $last = undef;
    my %index;
    $self->{member_index} = \%index;
    foreach my $member (@{$self->{members}})
      {
        if ($member->{value})
          {
            $member->{value}->layout($accept_incomplete, $namespace);
          }
        else
          {
            if ($last)
              {
                $member->{value} = new CExpr::Integer ($last->compute + 1);
              }
            else
              {
                $member->{value} = new CExpr::Integer 0;
              }
          }
        my $this_value = $member->{value}->compute;
        $min_value = $this_value if not defined $min_value or $min_value > $this_value;
        $max_value = $this_value if not defined $max_value or $max_value < $this_value;
        $last = $member->{value};
        $index{$member->{name}} = $member;

        # And update the namespace with a proper reference to the
        # enumerator. It'll all get horribly confused if we leave the
        # reference to $self in there
        $namespace->set('ordinary', $member->{name}, (new CDecl::Enumerator $self, $member->{name}));
      }

    if ($self->{packed})
      {
        my $class = CType::Fundamental::pick_smallest_type $min_value, $max_value;
        $self->{width} = $class->width;
        $self->{signed} = $class->signed;
        $self->add_alignment(new CExpr::Integer ($class->alignment / 8));
      }

    $self->{alignment} = $self->compute_alignment if scalar $self->alignment_exprs;
    $self->{laying_out} = 0;
    $self->{completed} = 1;
  }

sub describe
  {
    my $self = shift;

    my @members = map {$_->{name} . ' = ' . $_->{value}->dump_c} @{$self->{members}};

    return "enum {" . join(', ', @members) . "}";
  }

sub dump_c
  {
    my $self = shift;
    my $skip_cpp = shift;
    my $tag = shift;

    my $str = "";

    $str .= $self->dump_location($skip_cpp);

    my $qualifiers = $self->dump_c_qualifiers;

    $str .= 'enum';
    if ($qualifiers)
      {
        $str .= ' ';
        $str .= $qualifiers;
      }

    if ($tag)
      {
        $str .= ' ';
        $str .= $tag;
      }
    $str .= "\n";
    $str .= "  {\n";

    my @members = map {$_->{name} . ' = ' . $_->{value}->dump_c} @{$self->{members}};
    $str .= "    $_,\n" foreach @members;

    $str .= "  }\n";

    return $str;
  }

sub get_refs
  {
    my $self = shift;
    return (map {$_->{value}->get_refs} @{$self->{members}});
  }

sub get_value
  {
    my $self = shift;
    my $name = shift;
    return undef unless exists $self->{member_index}{$name};
    return $self->{member_index}{$name}{value};
  }

sub _check_interface
  {
    my $self = shift;
    my $other = shift;

    return 'both' unless $other->isa('CType::Enum');

    my %self_member_name_index = %{$self->{member_index}};
    my %other_member_name_index = %{$other->{member_index}};

    my @ret;

    if ($self->width != $other->width)
      {
        print "ABI mismatch: size of " . $self->width . " versus " . $other->width . "\n";
        push @ret, {abi_forward => 1, abi_backward => 1};
      }

    # First, we take out all the things with matching identifiers
    foreach my $member (sort {$a->{name} cmp $b->{name}} values %self_member_name_index)
      {
        my $other_member = $other_member_name_index{$member->{name}};
        next unless $other_member;
        my $this_value = $member->{value}->compute;
        my $other_value = $other_member->{value}->compute;
        if ($this_value != $other_value)
          {
            print "ABI mismatch between values $this_value and $other_value for enumerator $member->{name}\n";
            push @ret, {abi_forward => 1, abi_backward => 1};
          }
        else
          {
            push @ret, 'ok';
          }
        delete $self_member_name_index{$member->{name}};
        delete $other_member_name_index{$member->{name}};
      }

    # Now we hit all the stuff that's been added or removed
    foreach my $member (sort {$a->{name} cmp $b->{name}} values %other_member_name_index)
      {
        print "API removal: enumerator " . $member->{name} . " is gone\n";
        push @ret, {api_backward => 1};
      }

    foreach my $member (sort {$a->{name} cmp $b->{name}} values %self_member_name_index)
      {
        print "API addition: enumerator " . $member->{name} . " is new\n";
        push @ret, {api_forward => 1};
      }

    return @ret;
  }

1;
