package System::Introspector::Report::Updater;
use Moo;

my $_flatten_id = sub {
  my $value = $_[0];
  return ref($value) ? join("\0", @$value) : $value;
};

sub merge {
  my ($self, $original, $update) = @_;
  my $old = $self->_map_reports_by_id($original);
  my $new = $self->_map_reports_by_id($update);
  my %add = map { ($_ => $new->{$_}) } grep { not($old->{$_}) } keys %$new;
  return $self->_weave_additional([ map {
    (ref($_) eq 'HASH') ? do {
      my $report = $_;
      my $id = $report->{id}->$_flatten_id;
      $new->{$id}
        ? $self->_merge_reports($report, $new->{$id})
        : $report;
    } : $_;
  } @$original ], \%add, $self->_calc_sequence($update));
}

sub _weave_additional {
  my ($self, $stream, $additional, $sequence) = @_;
  my @items = @$stream;
  if (my $begin = $sequence->{''}) {
    if (my $item = delete $additional->{$begin}) {
      unshift @items, $item;
    }
  }
  my @done;
  while (defined( my $item = shift @items )) {
    if (ref($item) eq 'HASH') {
      my $item_id = $item->{id}->$_flatten_id;
      if (my $after = $sequence->{$item_id}) {
        if (my $insert = $additional->{$after}) {
          unshift @items, $insert;
        }
      }
    }
    push @done, $item;
  }
  return \@done;
}

sub _calc_sequence {
  my ($self, $stream) = @_;
  my @ids = map {
    $_->{id}->$_flatten_id;
  } grep {
    ref($_) eq 'HASH';
  } @$stream;
  my %sequence;
  for my $index (0 .. $#ids) {
    $sequence{ $index ? $ids[$index - 1] : '' } = $ids[$index];
  }
  return \%sequence;
}

sub _merge_reports {
  my ($self, $original, $update) = @_;
  my $rowid = $update->{rowid};
  my $ident = sub {
    my $row = shift;
    join "\0", map $row->{$_}, @$rowid;
  };
  my %current;
  $current{$_->$ident} = $_
    for @{$original->{rows}};
  return {
    title => $update->{title},
    id => $update->{id},
    rowid => $update->{rowid},
    meta => $update->{meta} || {},
    columns => [
      @{$update->{columns}},
      (grep {
        $_->{key} =~ m{^__};
      } @{$original->{columns}}),
    ],
    rows => [ map {
      my $row = $_;
      my $id = $row->$ident;
      +{ %{$current{$id}||{}}, %$row };
    } @{$update->{rows}} ],
  };
}

sub _map_reports_by_id {
  my ($self, $stream) = @_;
  return {
    map {
      ($_->{id}->$_flatten_id, $_);
    } grep {
      ref($_) eq 'HASH';
    } @$stream,
  };
}

1;
