package HackaMol;
$HackaMol::VERSION = '0.037';
#ABSTRACT: HackaMol: Object-Oriented Library for Molecular Hacking
use 5.008;
use Moose;
use HackaMol::AtomGroup;
use HackaMol::Molecule;
use HackaMol::Atom;
use HackaMol::Bond;
use HackaMol::Angle;
use HackaMol::Dihedral;
use namespace::autoclean;
use MooseX::StrictConstructor;
use Scalar::Util qw(refaddr);
use Carp;

with 'HackaMol::Roles::NameRole', 
     'HackaMol::Roles::MolReadRole', 
     'HackaMol::Roles::PathRole',
     'HackaMol::Roles::ExeRole', 
     'HackaMol::Roles::FileFetchRole';

sub pdbid_mol {
  my $self  = shift;
  my $pdbid = shift || croak "Croak on passing pdbid, e.g. 2cba";
  my ($file,$rc) = $self->getstore_pdbid($pdbid);
  return($self->read_file_mol($file));
}

sub read_file_push_coords_mol{
    my $self = shift;
    my $file = shift;
    my $mol  = shift or croak "must pass molecule to add coords to";

    my @atoms = $self->read_file_atoms($file);
    my @matoms= $mol->all_atoms;

    if (scalar(@matoms) != scalar(@atoms) ){
      croak "file_push_coords_mol> number of atoms not same";
    }
    foreach my $i (0 .. $#atoms) {
      if ($matoms[$i]->Z != $atoms[$i]->Z){
        croak "file_push_coords_mol> atom mismatch";
      }
      $matoms[$i]->push_coords($_) foreach ($atoms[$i]->all_coords);
    }
}

sub read_file_mol{
    my $self = shift;
    my $file = shift;

    my @atoms = $self->read_file_atoms($file);
    my $name = $file . ".mol";
    return (HackaMol::Molecule->new(name=>$name, atoms=>[@atoms]));
}

sub read_string_mol{
    my $self = shift;
    my $string = shift;
    my $format = shift or croak "must pass format: xyz, pdb, pdbqt, zmat, yaml";

    my @atoms = $self->read_string_atoms($string,$format);
    my $name = $format. ".mol";
    return (HackaMol::Molecule->new(name=>$name, atoms=>[@atoms]));
}

sub build_bonds {

    #take a list of n, atoms; walk down list and generate bonds
    my $self  = shift;
    my @atoms = @_;
    croak "<2 atoms passed to build_bonds" unless ( @atoms > 1 );
    my @bonds;
    # build the bonds
    my $k = 0;
    while ( $k + 1 <= $#atoms ) {
        my $name =
              join( "_", map { _name_resid($_,'B')} @atoms[ $k .. $k + 1 ] );
        push @bonds,
          HackaMol::Bond->new(
            name  => $name,
            atoms => [ @atoms[ $k, $k + 1 ] ]
          );
        $k++;
    }
    return (@bonds);
}

sub build_angles {

    #take a list of n, atoms; walk down list and generate angles
    my $self  = shift;
    my @atoms = @_;
    croak "<3 atoms passed to build_angles" unless ( @atoms > 2 );
    my @angles;

    # build the angles
    my $k = 0;

    while ( $k + 2 <= $#atoms ) {
        my $name =
              join( "_", map { _name_resid($_,'A')} @atoms[ $k .. $k + 2 ] );
        push @angles,
          HackaMol::Angle->new(
            name  => $name,
            atoms => [ @atoms[ $k .. $k + 2 ] ]
          );
        $k++;
    }
    return (@angles);
}

sub _name_resid {
  my $atom    = shift;
  my $default = shift;
  return ($default    . $atom->resid) unless $atom->has_name; # this comes up when undefined atoms are passed 
  return ($atom->name . $atom->resid);
}

sub build_dihedrals {

    #take a list of n, atoms; walk down list and generate dihedrals
    my $self  = shift;
    my @atoms = @_;
    croak "<4 atoms passed to build_dihedrals" unless ( @atoms > 3 );
    my @dihedrals;

    # build the dihedrals
    my $k = 0;
    while ( $k + 3 <= $#atoms ) {
        my $name =
              join( "_", map { _name_resid($_,'D')} @atoms[ $k .. $k + 3 ] );
        push @dihedrals,
          HackaMol::Dihedral->new(
            name  => $name,
            atoms => [ @atoms[ $k .. $k + 3 ] ]
          );
        $k++;
    }
    return (@dihedrals);
}

sub group_by_atom_attr {

    # group atoms by attribute
    # Z, name, bond_count, etc.
    my $self  = shift;
    my $attr  = shift;
    my @atoms = @_;

    my %group;
    foreach my $atom (@atoms) {
        push @{ $group{ $atom->$attr } }, $atom;
    }

    my @atomgroups =
      map { HackaMol::AtomGroup->new( atoms => $group{$_} ) } sort
      keys(%group);

    return (@atomgroups);

}

sub find_disulfide_bonds {
    my $self  = shift;
    my @sulf  = grep {$_->Z == 16} @_;
    my @ss = $self->find_bonds_brute(
                                     bond_atoms => [@sulf],
                                     candidates => [@sulf],
                                     fudge      => 0.45,
                                     max_bonds  => 1,
                                    );
    return @ss;
}

sub find_bonds_brute {
    my $self       = shift;
    my %args       = @_;
    my @bond_atoms = @{ $args{bond_atoms} };
    my @atoms      = @{ $args{candidates} };

    my $fudge = 0.45;
    my $max_bonds = 99;

    $fudge     = $args{fudge}     if ( exists( $args{fudge} ) );
    $max_bonds = $args{max_bonds} if ( exists( $args{max_bonds} ) );

    my @init_bond_counts = map {$_->bond_count} (@bond_atoms,@atoms);

    my @bonds;
    my %name;

    foreach my $at_i (@bond_atoms) {
        next if ($at_i->bond_count >= $max_bonds);
        my $cov_i = $at_i->covalent_radius;
        my $xyz_i = $at_i->xyz;

        foreach my $at_j (@atoms) {
            next if ( refaddr($at_i) == refaddr($at_j) );
            next if ($at_j->bond_count >= $max_bonds);
            my $cov_j = $at_j->covalent_radius;
            my $dist  = $at_j->distance($at_i);

            if ( $dist <= $cov_i + $cov_j + $fudge ) {
                my $nm = $at_i->symbol . "-" . $at_j->symbol;
                $name{$nm}++;
                push @bonds,
                  HackaMol::Bond->new(
                    name  => "$nm\_" . $name{$nm},
                    atoms => [ $at_i, $at_j ],
                  );
                $at_i->inc_bond_count;
                $at_j->inc_bond_count;
            }

        }
    }
  
    my $i = 0;
    foreach my $at (@bond_atoms,@atoms){
      $at->reset_bond_count;
      $at->inc_bond_count($init_bond_counts[$i]);
      $i++;
    }
    return (@bonds);

}

sub group_rot {
  # no pod yet
  # this method walks out from the two atoms in a bond and returns a group
  my $self  = shift;
  my $mol   = shift;
  my $bond  = shift;
  my $iba = $bond->get_atoms(0)->iatom;
  my $ibb = $bond->get_atoms(1)->iatom;

  my $init = { 
              $iba => 1, 
              $ibb => 1, 
  };
  my $root  = $ibb; 
  my $rotation_indices =  _qrotatable($mol->atoms,$ibb, $init);
  delete( $rotation_indices->{ $iba } );
  delete( $rotation_indices->{ $ibb } );

  return  (
            HackaMol::AtomGroup->new( 
                      atoms => [
                                map {$mol->get_atoms($_)} 
                                    keys %{$rotation_indices} 
                      ] 
            )
  );

}

sub _qrotatable {
    my $atoms   = shift;
    my $iroot   = shift;
    my $visited = shift;

    $visited->{$iroot}++;
    
    if (scalar (keys %$visited) > 80){
      carp "search too deep. exiting recursion";
      return;
    }

    my @cands;
    foreach my $at (@$atoms) {
        push @cands, $at unless ( grep { $at->iatom == $_ } keys %{$visited} );
    }

    #not calling in object context, hence the dummy 'self' 
    my @bonds = find_bonds_brute('self',
        bond_atoms => [ $atoms->[$iroot] ],
        candidates => [@cands],
        fudge      => 0.45,
    );

    foreach my $cand ( map { $_->get_atoms(1) } @bonds ) {
        next if $visited->{ $cand->iatom };
        my $visited = _qrotatable( $atoms, $cand->iatom, $visited );
    }
    return ($visited);
}

__PACKAGE__->meta->make_immutable;

1;

__END__

=pod

=head1 NAME

HackaMol - HackaMol: Object-Oriented Library for Molecular Hacking

=head1 VERSION

version 0.037

=head1 DESCRIPTION

The L<HackaMol publication|http://pubs.acs.org/doi/abs/10.1021/ci500359e> has
a more complete description of the library (L<pdf available from researchgate|http://www.researchgate.net/profile/Demian_Riccardi/publication/273778191_HackaMol_an_object-oriented_Modern_Perl_library_for_molecular_hacking_on_multiple_scales/links/550ebec60cf27526109e6ade.pdf>). 

Citation: J. Chem. Inf. Model., 2015, 55, 721 

Loading the HackaMol library in a script with 

       use HackaMol;

provides attributes and methods of a builder class. It also loads all the 
classes provided by the core so including them is not necessary, e.g.:

       use HackaMol::Atom;
       use HackaMol::Bond;
       use HackaMol::Angle;
       use HackaMol::Dihedral;
       use HackaMol::AtomGroup;
       use HackaMol::Molecule;

The methods, described below, facilitate the creation of objects from files and
other objects.

=head1 METHODS

=head2 pdbid_mol

one argument: pdbid

This method will download the pdb, unless it exists, and load it into a 
HackaMol::Molecule object. For example,

      my $mol = HackaMol->new->pdbid_mol('2cba');

=head2 read_file_atoms

one argument: filename.

This method parses the file (e.g. file.xyz, file.pdb) and returns an 
array of HackaMol::Atom objects.

=head2 read_file_mol

one argument: filename.

This method parses the file (e.g. file.xyz, file.pdb) and returns a 
HackaMol::Molecule object.

=head2 read_file_push_coords_mol

two arguments: filename and a HackaMol::Molecule object. 

This method reads the coordinates from a file and pushes them into the atoms 
contained in the molecule. Thus, the atoms in the molecule and the atoms in 
the file must be the same.

=head2 build_bonds

takes a list of atoms and returns a list of bonds.  The bonds are generated for
"list neighbors" by simply stepping through the atom list one at a time. e.g.

  my @bonds = $hack->build_bonds(@atoms[1,3,5]);

will return two bonds: B13 and B35 

=head2 build_angles

takes a list of atoms and returns a list of angles. The angles are generated 
analagously to build_bonds, e.g.

  my @angles = $hack->build_angles(@atoms[1,3,5]);

will return one angle: A135

=head2 build_dihedrals

takes a list of atoms and returns a list of dihedrals. The dihedrals are generated 
analagously to build_bonds, e.g.

  my @dihedral = $hack->build_dihedrals(@atoms[1,3,5]);

will croak!  you need atleast four atoms.

  my @dihedral = $hack->build_dihedrals(@atoms[1,3,5,6,9]);

will return two dihedrals: D1356 and D3569

=head2 group_by_atom_attr

arguments are an atom attribute and then a list of atoms. 

This method builds AtomGroup objects that are grouped by attribute.

=head2 find_bonds_brute 

The arguments are key_value pairs of bonding criteria (see example below). 

This method returns bonds between bond_atoms and the candidates using the 
criteria (many of wich have defaults).

  my @oxy_bonds = $hack->find_bonds_brute(
                                    bond_atoms => [$hg],
                                    candidates => [$mol->all_atoms],
                                    fudge      => 0.45,
                                    max_bonds  => 6,
  );

fudge is optional with Default is 0.45 (open babel uses same default); 
max_bonds is optional with default of 99. max_bonds is compared against
the atom bond count, which are incremented during the search. Before returning
the bonds, the bond_count are returned the values before the search.  For now,
molecules are responsible for setting the number of bonds in atoms. 
find_bonds_brute uses a bruteforce algorithm that tests the interatomic 
separation against the sum of the covalent radii + fudge. It will not test
for bond between atoms if either atom has >= max_bonds. It does not return 
a self bond for an atom (C< next if refaddr($ati) == refaddr($atj) >).

=head2 find_disulfide_bonds

the argument is a list of atoms, e.g. '($mol->all_atoms)'. 

this method returns disulfide bonds as bond objects.

=head1 ATTRIBUTES

=head2 name 

name is a rw str provided by HackaMol::NameRole.

=head1 SYNOPSIS 

       # simple example: load pdb file and extract the disulfide bonds

       use HackaMol;

       my $bldr = HackaMol->new( name => 'builder');
       my $mol  = $bldr->pdbid_mol('1kni');

       my @disulfide_bonds = $bldr->find_disulfide_bonds( $mol->all_atoms );

       print $_->dump foreach @disulfide_bonds;

See the above executed in this L<linked notebook|https://github.com/demianriccardi/p5-IPerl-Notebooks/blob/master/HackaMol/HackaMol-Synopsis.ipynb>

=head1 SEE ALSO

=over 4

=item *

L<HackaMol::Atom>

=item *

L<HackaMol::Bond>

=item *

L<HackaMol::Angle>

=item *

L<HackaMol::Dihedral>

=item *

L<HackaMol::AtomGroup>

=item *

L<HackaMol::Molecule>

=item *

L<HackaMol::X::Calculator>

=item *

L<HackaMol::X::Vina>

=item *

L<Protein Data Bank|http://pdb.org>

=item *

L<VMD|http://www.ks.uiuc.edu/Research/vmd/>

=back

=head1 EXTENDS

=over 4

=item * L<Moose::Object>

=back

=head1 CONSUMES

=over 4

=item * L<HackaMol::Roles::ExeRole>

=item * L<HackaMol::Roles::FileFetchRole>

=item * L<HackaMol::Roles::MolReadRole>

=item * L<HackaMol::Roles::NERFRole>

=item * L<HackaMol::Roles::NameRole>

=item * L<HackaMol::Roles::NameRole|HackaMol::Roles::MolReadRole|HackaMol::Roles::PathRole|HackaMol::Roles::ExeRole|HackaMol::Roles::FileFetchRole>

=item * L<HackaMol::Roles::PathRole>

=item * L<HackaMol::Roles::ReadPdbRole>

=item * L<HackaMol::Roles::ReadPdbqtRole>

=item * L<HackaMol::Roles::ReadXyzRole>

=item * L<HackaMol::Roles::ReadYAMLRole>

=item * L<HackaMol::Roles::ReadYAMLRole|HackaMol::Roles::ReadZmatRole|HackaMol::Roles::ReadPdbRole|HackaMol::Roles::ReadPdbqtRole|HackaMol::Roles::ReadXyzRole>

=item * L<HackaMol::Roles::ReadZmatRole>

=back

=head1 AUTHOR

Demian Riccardi <demianriccardi@gmail.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by Demian Riccardi.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
