package DBIx::Class::Sims::Runner;

use 5.010_001;

use strictures 2;

use DDP;

use Data::Compare qw( Compare );
use Try::Tiny;

use DBIx::Class::Sims::Item;
use DBIx::Class::Sims::Source;

sub new {
  my $class = shift;
  my $self = bless {@_}, $class;
  $self->initialize;
  return $self;
}

sub initialize {
  my $self = shift;

  $self->{sources} = {};
  foreach my $name ( $self->schema->sources ) {
    $self->{sources}{$name} = DBIx::Class::Sims::Source->new(
      name   => $name,
      runner => $self,
      constraints => $self->{constraints}{$name},
    );
  }

  $self->{created}    = {};
  $self->{duplicates} = {};

  $self->{create_stack} = [];
  $self->{child_requested} = [];

  return;
}

sub has_item {
  my $self = shift;
  my ($item) = @_;

  foreach my $comp (@{$self->{create_stack}}) {
    next unless $item->source_name eq $comp->[0];
    next unless Compare($item->spec, $comp->[1]);
    return 1;
  }
  return;
}
sub add_item {
  my $self = shift;
  my ($item) = @_;
  push @{$self->{create_stack}}, [
    $item->source_name, MyCloner::clone($item->spec),
  ];
}
sub remove_item {
  my $self = shift;
  my ($item) = @_;
  pop @{$self->{create_stack}};
}

sub parent { shift->{parent} }
sub schema { shift->{schema} }
sub driver { shift->schema->storage->dbh->{Driver}{Name} }
sub is_oracle { shift->driver eq 'Oracle' }
sub datetime_parser { shift->schema->storage->datetime_parser }

sub are_columns_equal {
  my $self = shift;
  my ($source, $row, $compare) = @_;
  foreach my $c ($source->columns) {
    next if $c->is_in_fk;
    my $col = $c->name;

    next if !exists($row->{$col}) && !exists($compare->{$col});
    return unless exists($row->{$col}) && exists($compare->{$col});
    return if $compare->{$col} ne $row->{$col};
  }
  return 1;
}
{
  my %added_by;
  sub add_child {
    my $self = shift;
    my ($opts) = @_;
    my ($source, $fkcol, $child, $adder, $trace) = @{$opts}{qw(
      source fkcol child adder trace
    )};

    # If $child has the same keys (other than parent columns) as another row
    # added by a different parent table, then set the foreign key for this
    # parent in the existing row.
    foreach my $compare (@{$self->{spec}{$source->name}}) {
      # Don't mess with the specs we were provided.
      # next if ref($compare) eq 'HASH';

      # Handle the case of a child added with its trace.
      if ( ref($compare) eq 'ARRAY' ) {
        $compare = $compare->[0];
      }

      next if exists $added_by{$adder} && exists $added_by{$adder}{$compare};
      next if exists $compare->{$fkcol};
      if ($self->are_columns_equal($source, $child, $compare)) {
        $compare->{$fkcol} = $child->{$fkcol};
        return;
      }
    }

    push @{$self->{spec}{$source->name}}, [ $child, $trace ];
    $added_by{$adder} //= {};
    $added_by{$adder}{$child} = !!1;
    $self->add_pending($source->name);
  }
}

{
  # The "pending" structure exists because of t/parent_child_parent.t - q.v. the
  # comments on the toposort->add_dependencies element.
  my %pending;
  sub add_pending { $pending{$_[1]} = undef; }
  sub has_pending { keys %pending != 0; }
  sub delete_pending { delete $pending{$_[1]}; }
  sub clear_pending { %pending = (); }
}

sub backref_name {
  my $self = shift;
  my ($item, $colname) = @_;
  return $item->source->name . '->' . $colname;
}

sub convert_backreference {
  my $self = shift;
  my ($backref_name, $proto, $default_method) = @_;

  my ($table, $idx, $methods) = ($proto =~ /(.+)\[(\d+)\](?:\.(.+))?$/);
  unless ($table && defined $idx) {
    die "Unsure what to do about $backref_name => $proto\n";
  }
  unless (exists $self->{rows}{$table}) {
    die "No rows in $table to reference\n";
  }
  unless (exists $self->{rows}{$table}[$idx]) {
    die "Not enough ($idx) rows in $table to reference\n";
  }

  if ($methods) {
    my @chain = split '\.', $methods;
    my $obj = $self->{rows}{$table}[$idx];
    $obj = $obj->$_ for @chain;
    return $obj;
  }
  elsif ($default_method) {
    return $self->{rows}{$table}[$idx]->$default_method;
  }
  else {
    die "No method to call at $backref_name => $proto\n";
  }
}

sub call_hook {
  my $self = shift;
  my $phase = shift;

  return unless exists $self->{hooks}{$phase};
  return $self->{hooks}{$phase}->(@_);
}

sub ensure_children {
  my $self = shift;
  my ($parent, $rel, $count) = @_;

  push @{$self->{child_requested}}, [ $parent, $rel, $count ];
}

sub run {
  my $self = shift;

  try {
    return $self->schema->txn_do(sub {
      # DateTime objects print too big in SIMS_DEBUG mode, so provide a
      # good way for DDP to print them nicely.
      no strict 'refs';
      local *{'DateTime::_data_printer'} = sub { shift->iso8601 }
        unless DateTime->can('_data_printer');

      $self->{traces} = [];
      $self->{ids} = {
        find => 1,
        made => 1,
        seen => 1,
      };

      $self->{rows} = {};
      my %still_to_use = map { $_ => 1 } keys %{$self->{spec}};
      while (1) {
        foreach my $name ( @{$self->{toposort}} ) {
          next unless $self->{spec}{$name};
          delete $still_to_use{$name};

          while ( my $proto = shift @{$self->{spec}{$name}} ) {
            # This is the case of a child added via add_child()
            if ( ref($proto) eq 'ARRAY' ) {
              ($proto, my $trace) = @{$proto};
              push @{$self->{traces}}, $trace;
            }
            else {
              push @{$self->{traces}}, {
                table => $name,
                spec => MyCloner::clone($proto),
                seen => $self->{ids}{seen}++,
                parent => 0,
              };
            }

            $proto->{__META__} //= {};
            $proto->{__META__}{create} = 1;

            my $item = DBIx::Class::Sims::Item->new(
              runner => $self,
              source => $self->{sources}{$name},
              spec   => $proto,
              trace  => $self->{traces}[-1],
            );

            if ($self->{allow_pk_set_value}) {
              $item->set_allow_pk_to(1);
            }

            my $row = $item->create;

            if ($self->{initial_spec}{$name}{$item->spec}) {
              push @{$self->{rows}{$name} //= []}, $row;
            }
          }

          $self->delete_pending($name);
        }

        foreach my $item ( @{$self->{child_requested}} ) {
          my ($parent, $rel, $count) = @{$item};
          # Look at $parent's children via $rel and compare to $count.
          # If we're short, then $self->add_child().
          my $name = $rel->name;

          # TODO: If the rel is a single accessor, then the constraints should
          # be capped to 1 and an error thrown otherwise.
          my $num_children = $rel->is_single_accessor
            ? ($parent->row->$name ? 1 : 0)
            : $parent->row->$name->count;
          while ( $count > $num_children ) {
            my $child = {};
            ($child->{__META__} //= {})->{allow_pk_set_value} = 1;

            my @inverse = $parent->source->find_inverse_relationships(
              $rel->target, $rel->foreign_fk_col,
            );
            $child->{$rel->foreign_fk_col} = @inverse == 0
              ? $parent->row->get_column($rel->self_fk_col)
              : $parent->row;

            $self->add_child({
              adder  => $parent->source_name,
              source => $rel->target,
              fkcol  => $rel->foreign_fk_col,
              child  => $child,
              trace  => {
                table  => $rel->target->name,
                spec   => MyCloner::clone($child),
                seen   => $parent->{runner}{ids}{seen}++,
                parent => $parent->{trace}{seen},
                via    => 'add_child',
              },
            });

            $count--;
          }
        }
        $self->{child_requested} = [];

        last unless $self->has_pending();
        $self->clear_pending();
      }

      # Things were passed in, but don't exist in the schema.
      if (!$self->{ignore_unknown_tables} && %still_to_use) {
        my $msg = "The following names are in the spec, but not the schema:\n";
        $msg .= join ',', sort keys %still_to_use;
        $msg .= "\n";
        die $msg;
      }

      if ( $self->{object_trace} ) {
        use JSON::MaybeXS qw( encode_json );
        open my $fh, '>', $self->{object_trace};
        print $fh encode_json({
          objects => $self->{traces},
        });
        close $fh;
      }

      return $self->{rows};
    });
  } catch {
    my $e = $_;

    if ( $self->{object_trace} ) {
      open my $fh, '>', $self->{object_trace};

      # Try our hardest to write out in JSON. If that doesn't work, write it
      # out in DDP.
      try {
        use JSON::MaybeXS qw( encode_json );
        print $fh encode_json({
          objects => $self->{traces},
        });
      } catch {
        my $e2 = $_;

        use DDP;
        my %x = ( objects => $self->{traces} );
        print $fh np(%x);

        warn "Couldn't write out in JSON ($e2). Using DDP instead\n";
      };

      close $fh;
    }

    die $e;
  };
}

1;
__END__