#!/usr/bin/perl

use strict;
use warnings;

use Chemistry::Atom qw( angle_deg dihedral_deg );
use Chemistry::File::SDF;
use Chemistry::Mol;
use Chemistry::OpenSMILES::Stereo qw( mark_all_double_bonds );
use Chemistry::OpenSMILES::Writer qw( write_SMILES );
use File::Basename qw( basename );
use Getopt::Long::Descriptive;
use Graph::Traversal::DFS;
use Graph::Undirected;
use List::Util qw( any min sum );

my $default_planarity_threshold = 1e-6;

my $basename = basename $0;
my( $opt, $usage ) = describe_options( <<"END" . 'OPTIONS',
USAGE
    $basename [<args>] [<files>]

DESCRIPTION
    $basename converts SDF files into SMILES.
END
    [ 'convert-type-8-bonds-to-covalent',
      'convert all type 8 bonds to single covalent bonds' ],
    [],
    [ tetrahedral_chiral_method => hidden => {
        one_of => [
            [ 'ignore-flat-tetrahedra' =>
                'when considering tetrahedral chiral centers, ignore ' .
                'flat tetrahedra (based on volume threshold) [default]' ],
            [ 'require-tetrahedral-angles' =>
                'when considering tetrahedral chiral centers, ignore ' .
                'tetrahedra with average bond angle far from 109.5 degrees' ],
        ],
        default => 'ignore_flat_tetrahedra'
      }
    ],
    [ 'planarity-threshold=f',
      'planarity volume threshold in cubic angstroms; tetrahedra with ' .
      'chiral volume less or equal to the given one will be understood ' .
      "as flat [default: $default_planarity_threshold]",
      { default => $default_planarity_threshold } ],
    [ 'normalise-vectors',
      'normalise vectors used for chiral volume calculation' ],
    [],
    [ 'help', 'print usage message and exit', { shortcircuit => 1 } ],
);

if( $opt->help ) {
    print $usage->text;
    exit;
}

@ARGV = ( '-' ) unless @ARGV;

Chemistry::Mol->register_format( sdf => Chemistry::File::SDF:: );

# Perl Clone module does native cloning, thus it should be able to handle
# larger molecules than the default Storable cloer.
$Chemistry::Mol::clone_backend = 'Clone';

foreach my $filename (@ARGV) {
    local $SIG{__WARN__} = sub {
        print STDERR "$basename: $filename: $_[0]"
    };

    eval {
        my $reader = Chemistry::Mol->file( $filename, format => 'sdf' );
        $reader->open( '<' );
        while( my $molecule = $reader->read_mol( $reader->fh ) ) {
            # Resolving the issue with bonds of order 8
            for my $bond ($molecule->bonds) {
                next unless $bond->type == 8;

                if( $opt->convert_type_8_bonds_to_covalent ) {
                    $bond->type(1);
                }

                if( grep { !$_->formal_charge } $bond->atoms ) {
                    $bond->type(1) if !$opt->convert_type_8_bonds_to_covalent;
                    next;
                }
                
                my @atoms = sort { $a->formal_charge <=>
                                   $b->formal_charge } $bond->atoms;

                if( $opt->convert_type_8_bonds_to_covalent ) {
                    # When bond 8 is converted into plain covalent bond,
                    # charges for both atoms have to be adjusted
                    next unless $atoms[0]->formal_charge < 0 &&
                                $atoms[1]->formal_charge > 0;
                    my $min_abs_charge = min -$atoms[0]->formal_charge,
                                              $atoms[1]->formal_charge;
                    $atoms[0]->formal_charge( $atoms[0]->formal_charge +
                                              $min_abs_charge );
                    $atoms[1]->formal_charge( $atoms[1]->formal_charge -
                                              $min_abs_charge );
                } else {
                    # Replace bond 8 with a single bond if both of the atoms
                    # have opposite charges and decrease the charges by 1.
                    # Further increase the bond order if both of the atoms
                    # still have opposing charges.
                    while( $atoms[0]->formal_charge < 0 &&
                           $atoms[1]->formal_charge > 0 ) {
                        if(      $bond->type == 3 ) {
                            last; # TODO: Not sure what to do with higher-orders
                        } elsif( $bond->type == 8 ) {
                            $bond->type(0);
                        }
                        $bond->type($bond->type+1);
                        $atoms[0]->formal_charge($atoms[0]->formal_charge+1);
                        $atoms[1]->formal_charge($atoms[1]->formal_charge-1);
                    }
                    # TODO: What to do when both charges are of the same sign?
                }
            }

            my $name = $molecule->name;
            my @graphs;
            for my $moiety ($molecule->separate) {
                my %atoms;
                my %atom_by_vertex;
                my $graph = Graph::Undirected->new( refvertexed => 1 );
                my @atoms = $moiety->atoms;

                # First pass through atoms to add them to molecular graph 
                for my $i (0..$#atoms) {
                    my $atom = $atoms[$i];
                    my $vertex = {
                        charge => $atom->formal_charge,
                        number => $i,
                        symbol => $atom->symbol,
                    };
                    my $most_common_isotope =
                        Chemistry::Atom->new( symbol => $atom->symbol );
                    if( $most_common_isotope->mass != $atom->mass ) {
                        $vertex->{isotope} = sprintf( '%.0f', $atom->mass ) + 0;
                    }
                    $graph->add_vertex( $vertex );
                    $atoms{$atom} = $vertex;
                    $atom_by_vertex{$vertex} = $atom;
                }

                # Second pass through atoms to set chirality
                for my $i (0..$#atoms) {
                    my $atom   = $atoms[$i];
                    my $vertex = $atoms{$atom};
                    my @neighbours = $atom->neighbors;
                    my $hydrogens  = grep { $_->symbol eq 'H' &&
                                            scalar $_->neighbors == 1 }
                                          @neighbours;
                    next unless @neighbours == 4 && $hydrogens <= 1;
                    # Calculate the chiral volume for tetrahedral chiral
                    # centers
                    my( $a, $b, $c ) =
                        map { $_->{coords} - $neighbours[0]->{coords} }
                            @neighbours[1..3];
                    if( $opt->normalise_vectors ) {
                        ( $a, $b, $c ) = map { $_->norm } ( $a, $b, $c );
                    }
                    my $volume = $a . ( $b x $c );
                    if( $opt->tetrahedral_chiral_method eq 'ignore_flat_tetrahedra' ) {
                        next if abs( $volume ) <= $opt->planarity_threshold;
                    } else {
                        my $angle = sum angle_deg( $neighbours[0], $atom, $neighbours[1] ),
                                        angle_deg( $neighbours[0], $atom, $neighbours[2] ),
                                        angle_deg( $neighbours[0], $atom, $neighbours[3] ),
                                        angle_deg( $neighbours[1], $atom, $neighbours[2] ),
                                        angle_deg( $neighbours[1], $atom, $neighbours[3] ),
                                        angle_deg( $neighbours[2], $atom, $neighbours[3] );
                        next if abs( $angle / 6 - 109.5 ) > 5;
                    }
                    $vertex->{chirality} = $volume < 0 ? '@' : '@@';
                    $vertex->{chirality_neighbours} =
                        [ map { $atoms{$_} } @neighbours ];
                }               

                # First pass through bonds to set them as given in SDF
                for my $bond ($moiety->bonds) {
                    my $type = bond_type( $bond->type );
                    if( $type ) {
                        $graph->set_edge_attribute(
                            map( { $atoms{$_} } $bond->atoms ),
                            'bond',
                            $type );
                    } else {
                        $graph->add_edge(
                            map { $atoms{$_} } $bond->atoms );
                    }
                }

                # Derive cis/trans settings for double bonds
                mark_all_double_bonds( $graph,
                                       sub {
                                            my $angle = dihedral_deg( map { $atom_by_vertex{$_} } @_ );
                                            return abs $angle < 90 ? 'cis' : 'trans';
                                       } );

                push @graphs, $graph;
            }
            print join( '.', map { write_SMILES( $_ ) } @graphs ),
                  "\t",
                  $name,
                  "\n";
        }
    };
    if( $@ ) {
        $@ =~ s/\n$//;
        print STDERR "$basename: $filename: $@\n";
    }
}

sub bond_type
{
    my( $type ) = @_;

    return '=' if $type == 2;
    return '#' if $type == 3;
    return ':' if $type == 4;

    return undef; # TODO: discuss if this is correct
}
