package CType::Struct;

use 5.6.0;
use strict;
use warnings;

use Carp;
use CType;
use CExpr::Integer;
use CType::Structural;

no warnings 'recursion';

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

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);

    return $self;
  }

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

    return if defined $self->{width};

    $_->layout($accept_incomplete, $namespace) foreach @{$self->{members}};
    if ($accept_incomplete and grep {not $_->complete} @{$self->{members}})
      {
        # This type is incomplete and we don't care
        return;
      }

    foreach my $attribute (@{$self->{attributes}})
      {
        # A packed struct is an automatic packed attribute on all the
        # member declarations
        if ($attribute->name eq 'packed')
          {
            foreach (@{$self->{members}})
              {
                if ($_->type->isa('CType::Struct') or $_->type->isa('CType::Enum') or $_->type->isa('CType::Union'))
                  {
                    $_->set_packed(1);
                  }
              }
          }
      }

    my $offset = 0;

    my $have_variable_array = 0;
    my $previous_was_bitfield = 0;

    my @members;

    foreach my $member (@{$self->{members}})
      {
        if ($have_variable_array)
          {
            die "Variable-length array is not the last struct member";
          }

        if ($member->type->isa('CType::BitField'))
          {
            if ($member->type->width == 0)
              {
                # Zero-width bitfields are magic. This magic is
                # inexplicable. I'm just duplicating what gcc does.
                if ($previous_was_bitfield)
                  {
                    # The alignment of the struct is affected by a
                    # zero-width bitfield if and only if the previous
                    # member was a non-zero-width bitfield
                    $self->add_alignment($_) foreach $member->alignment_exprs;
                  }
                $previous_was_bitfield = undef;

                # And the less magic part: a zero-width bitfield
                # advances to the next 8-bit boundary (before
                # considering alignment), such that the previous and
                # next bitfields won't occupy the same byte
                my $x = $offset % 8;
                if ($x != 0)
                  {
                    # This value needs to be aligned, so we pad it
                    $offset += 8 - $x;
                  }
              }
            else
              {
                # The type of bitfields affects the alignment
                $self->add_alignment($_) foreach $member->alignment_exprs;
                $previous_was_bitfield = $member;

                # Declarations without identifiers are padding, not members
                if ($member->identifier)
                  {
                    push @members, $member;
                  }
              }
          }
        elsif ($member->type->isa('CType::Array') and not $member->type->size)
          {
            $have_variable_array = 1;

            # We can't align a variable length array. Just note its position
            $member->set_offset($offset);

            push @members, $member;

            # Now, we should be exiting the loop here, since a
            # variable length array must be the last member; if we
            # don't, then the test at the top will report the error
            next;
          }
        else
          {
            if (not $member->packed)
              {
                # The type of unpacked fields affects the alignment
                $self->add_alignment($_) foreach $member->alignment_exprs;

                # And, of course, the offset
                my $x = $offset % $member->alignment;
                if ($x != 0)
                  {
                    # This value needs to be aligned, so we pad it
                    $offset += $member->alignment - $x;
                  }
              }

            push @members, $member;

            $previous_was_bitfield = undef;
          }

        $member->set_offset($offset);
        $offset += $member->width;
      }

    # And then pad out the struct to its own alignment
    my $alignment = $self->compute_alignment * 8;
    my $x = $offset % $alignment;
    if ($x != 0)
      {
        # This value needs to be aligned, so we pad it
        $offset += $alignment - $x;
      }

    $self->{width} = $offset;
    $self->{alignment} = $alignment;
    $self->{members} = \@members;
  }

sub complete
  {
    my $self = shift;

    return defined $self->{width} ? 1 : 0;
  }

sub describe
  {
    my $self = shift;

    my @members = map {$_->describe} @{$self->{members}};

    return "struct {" . join(', ', @members) . "} of width $self->{width} and alignment $self->{alignment}";
  }

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 .= 'struct';
    if ($qualifiers)
      {
        $str .= ' ';
        $str .= $qualifiers;
      }

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

    my $offset = 0;
    foreach my $member (@{$self->{members}})
      {
        if ($member->type->isa('CType::BitField'))
          {
          }
        elsif ($member->type->isa('CType::Array') and not $member->type->size)
          {
            # We can't do much with this, and it's the end of the array
            foreach my $line (split /\n/, $member->dump_c($skip_cpp))
              {
                $str .= "    $line\n"
              }
            last;
          }
        else
          {
            if (not $member->packed)
              {
                my $x = $offset % $member->alignment;
                if ($x != 0)
                  {
                    # This value needs to be aligned, so we pad it
                    $offset += $member->alignment - $x;
                  }
              }
          }

        if ($member->{offset} < $offset)
          {
            # Whoops, it didn't fit
            die;
          }

        if ($member->{offset} > $offset)
          {
            # We need some padding
            my $padding = $member->{offset} - $offset;
            $str .= "    char : $padding;\n";
            $offset += $padding;
          }

        foreach my $line (split /\n/, $member->dump_c($skip_cpp))
          {
            $str .= "    $line\n"
          }

        $offset += $member->width;
      }

    $str .= "  }\n";

    return $str;
  }

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

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

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

    my @ret;

    if ($self->{width} and $other->{width})
      {
        if ($self->{width} != $other->{width})
          {
            print "ABI mismatch: size of $self->{width} versus $other->{width}\n";
            push @ret, {abi_forward => 1, abi_backward => 1};
          }
      }
    elsif ($self->{width})
      {
        print "Can't check type (old version is incomplete)\n";
        return {abi_forward => 1, abi_backward => 1, api_forward => 1, api_backward => 1};
      }
    elsif ($other->{width})
      {
        print "Can't check type (new version is incomplete)\n";
        return {abi_forward => 1, abi_backward => 1, api_forward => 1, api_backward => 1};
      }

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

    # We aren't interested in members without identifiers; padding can
    # change in any way without mattering to us.

    my %other_member_name_index;
    my %other_member_offset_index;
    foreach my $member (@{$other->{members}})
      {
        next unless $member->identifier;
        $other_member_name_index{$member->identifier} = $member;
        $other_member_offset_index{$member->offset} = $member;
      }

    # We make multiple passes over the list of members, because we
    # want name matches to take absolute priority over offset matches
    # - if there is both a name and an offset match, we must always
    # pick the name match. Otherwise things get ugly when a struct
    # member is inserted in the middle.
    my %done;

    # Okay, first we'll try the member with the same name
    foreach my $member (@{$self->{members}})
      {
        next unless $member->identifier;
        if ($other_member_name_index{$member->identifier})
          {
            my $other_member = $other_member_name_index{$member->identifier};
            push @ret, $member->check_interface($other_member);
            delete $other_member_name_index{$other_member->identifier};
            delete $other_member_offset_index{$other_member->offset};
            $done{$member}++;
          }
      }

    # Then the member at the same offset
    foreach my $member (@{$self->{members}})
      {
        next unless $member->identifier;
        next if $done{$member};
        if ($other_member_offset_index{$member->offset})
          {
            my $other_member = $other_member_offset_index{$member->offset};
            push @ret, $member->check_interface($other_member);
            delete $other_member_name_index{$other_member->identifier};
            delete $other_member_offset_index{$other_member->offset};
            $done{$member}++;
          }
      }

    foreach my $member (@{$self->{members}})
      {
        next unless $member->identifier;
        next if $done{$member};
        # A member has been removed. This is a
        # forwards-incompatible change
        print "API and ABI addition: member " . $member->identifier . " is new\n";
        push @ret, {api_forward => 1, abi_forward => 1};
      }

    # We've been removing members from the indices as we check them
    # off. If anything's left, members have been added.
    foreach my $member (sort {$a->identifier cmp $b->identifier} values %other_member_name_index)
      {
        print "API and ABI removal: member " . $member->identifier . " is gone\n";
        push @ret, {api_backward => 1, abi_backward => 1};
      }

    return @ret;
  }

1;
