package Mandel::Collection;

=head1 NAME

Mandel::Collection - A collection of Mandel documents

=head1 SYNOPSIS

  my $connection = MyModel->connect("mongodb://localhost/my_db");
  my $persons = $connection->collection("person");

  $persons->count(sub {
    my($persons, $err, $int) = @_;
  });

  # ...

=head1 DESCRIPTION

This class is used to describe a group of mongodb documents.

=cut

use Mojo::Base -base;
use Mandel::Iterator;
use Mango::BSON ':bson';
use Scalar::Util 'blessed';
use Carp 'confess';
use constant DEBUG => $ENV{MANDEL_CURSOR_DEBUG} ? eval 'require Data::Dumper;1' : 0;

=head1 ATTRIBUTES

=head2 connection

An object that inherit from L<Mandel>.

=head2 model

An object that inherit from L<Mandel::Model>.

=cut

has connection => sub { confess "connection required in constructor" };
has model => sub { confess "model required in constructor" };

has _storage_collection => sub {
  my $self = shift;
  $self->connection->_storage_collection($self->model->collection_name);
};

=head1 METHODS

=head2 all

  $self = $self->all(sub { my($self, $err, $docs) = @_; });
  $docs = $self->all;

Retrieves all documents from the database that match the given L</search>
query.

=cut

sub all {
  my($self, $cb) = @_;


  my $c = $self->_new_cursor;
  return [ map { $self->_new_document($_, 1) } @{ $c->all } ] unless $cb;

  $c->all(sub {
    my($cursor, $err, $docs) = @_;
    return $self->$cb($err, [ map { $self->_new_document($_, 1) } @$docs ]);
  });
  return $self;
}

=head2 create

  $document = $self->create;
  $document = $self->create(\%args);

Returns a new object of a given type. This object is NOT inserted into the
mongodb collection. You need to call L<Mandel::Document/save> for that to
happen.

C<%args> is used to set the fields in the new document, NOT the attributes.

=cut

sub create {
  my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
  my $self = shift;

  $self->_new_document(shift || undef, 0);
}

=head2 count

  $self = $self->count(sub { my($self, $err, $int) = @_; });
  $int  = $self->count;

Used to count how many documents the current L</search> query match.

=cut

sub count {
  my($self, $cb) = @_;

  my $c = $self->_new_cursor;
  return $c->count unless $cb;

  $c->count( sub{shift; $self->$cb(@_)} );
  return $self;
}

=head2 distinct

  $self   = $self->distinct("field_name", sub { my($self, $err, $values) = @_; });
  $values = $self->distinct("field_name");

Get all distinct values for key in this collection.

=cut

sub distinct {
  my($self, $field, $cb) = @_;

  my $c = $self->_new_cursor;
  return $c->distinct($field) unless $cb;

  $c->distinct( sub{shift; $self->$cb(@_)} );
  return $self;
}

=head2 iterator

  $iterator = $self->iterator;

Returns a L<Mandel::Iterator> object based on the L</search> performed.

=cut

sub iterator {
  my $self = shift;

  Mandel::Iterator->new(
    cursor => $self->_new_cursor,
    model => $self->model,
  );
}

=head2 patch

  $self = $self->patch(\%changes, sub { my($self, $err, $doc) = @_ });
  $self = $self->patch(\%changes);

This method can be used to add C<%changes> to multiple documents
in the collection. Which documents to update will be decided by the
C<%query> given to L</search>.

C<%extra> arguments default to:

=over 4

=item * upsert: false

=item * multi: true

=back

=cut

sub patch {
  my($self, $changes, $cb) = @_;
  my $extra = $self->{extra};

  warn '[Mandel::Collection::patch] ', Data::Dumper->new([$changes, $self->{query}, $extra])->Indent(1)->Sortkeys(1)->Terse(1)->Maxdepth(3)->Dump if DEBUG;
  $self->_storage_collection->update(
    $self->{query} || {},
    {
      '$set' => $changes
    },
    {
      upsert => $extra->{upsert} // bson_false,
      multi => $extra->{multi} // bson_true,
    },
    $cb ? (sub { $self->$cb($_[1]) }) : (),
  );

  $self;
}

=head2 remove

  $self = $self->remove(sub { my($self, $err) = @_; });
  $self = $self->remove;

Remove the documents that query given to L</search>.

=cut

sub remove {
  my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
  my $self = shift;
  my $c = $self->_storage_collection;
  my @args = $self->{query};

  warn '[Mandel::Collection::remove] ', Data::Dumper->new([$self->{query}])->Indent(1)->Sortkeys(1)->Terse(1)->Maxdepth(3)->Dump if DEBUG;
  push @args, sub{$self->$cb($_[1])} if $cb;

  $c->remove(@args);
  $self;
}

=head2 save

  $self = $self->save(\%document, sub { my($self, $err, $doc) = @_; );
  $doc  = $self->save(\%document);

Used to save a document. The callback receives a L<Mandel::Document>.

=cut

sub save {
  my($self, $raw, $cb) = @_;
  my $c = $self->_storage_collection;

  $raw->{_id} ||= bson_oid;

  warn '[Mandel::Collection::save] ', Data::Dumper->new([$raw])->Indent(1)->Sortkeys(1)->Terse(1)->Maxdepth(3)->Dump if DEBUG;

  unless ($cb) {
    $c->save($raw);
    return $self->_new_document($raw, 1);
  }

  $c->save($raw, sub {
    my($collection, $err, $doc) = @_;
    $self->$cb($err, $self->_new_document($raw, 1));
  });

  return $self;
}

=head2 search

  $clone = $self->search(\%query, \%extra);

Return a clone of the given collection, but with different C<%search> and
C<%extra> parameters. You can chain these calls to make the query more
precise.

C<%extra> will be used to set extra parameters on the L<Mango::Cursor>, where
all the keys need to match the L<Mango::Cursor/ATTRIBUTES>.

=cut

sub search {
  my($self, $query, $extra) = @_;
  my $class = blessed $self;
  my $clone = $class->new(%$self);

  @{ $clone->{extra} }{keys %$extra} = values %$extra if $extra;
  @{ $clone->{query} }{keys %$query} = values %$query if $query;
  $clone;
}

=head2 single

  $self = $self->single(sub { my($self, $err, $doc) = @_; });
  $doc  = $self->single;

Will return the first object found in the callback, matching the given
C<%search> query.

=cut

sub single {
  my($self, $cb) = @_;

  my $c = $self->_new_cursor->limit(-1);
  unless ($cb) {
    my $doc = $c->next or return;
    return $self->_new_document($doc, 1);
  }

  $c->next(sub {
    my($cursor, $err, $doc) = @_;
    $self->$cb($err, $doc ? $self->_new_document($doc, 1) : undef);
  });
  return $self;
}

sub _new_cursor {
  my $self = shift;
  my $extra = $self->{extra} || {};
  my $cursor = $self->_storage_collection->find;

  $cursor->query($self->{query}) if $self->{query};
  $cursor->$_($extra->{$_}) for keys %$extra;
  warn '[', +(caller 1)[3], '] ', Data::Dumper->new([$cursor])->Indent(1)->Sortkeys(1)->Terse(1)->Maxdepth(3)->Dump if DEBUG;
  $cursor;
};

sub _new_document {
  my($self, $doc, $from_storage) = @_;
  my $model = $self->model;
  my @extra;

  if($doc) {
    push @extra, data => $doc;
    push @extra, dirty => { map { $_, 1 } keys %$doc };
  }
  if(my $connection = $self->{connection}) {
    push @extra, connection => $connection,
  }

  $model->document_class->new(
    model => $model,
    in_storage => $from_storage,
    @extra,
  );
}

=head1 SEE ALSO

L<Mojolicious>, L<Mango>, L<Mandel>

=head1 AUTHOR

Jan Henning Thorsen - C<jhthorsen@cpan.org>

=cut

1;
