#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;

use DBI;
use Flickr::API;
use XML::LibXML;

use Compress::Zlib; # boggle

# put in your NSID here
my $user_id = ''; # mine is '48600109393@N01'

# flickr initialisation
my $key    = undef;
my $secret = undef; # your key and secret here 

my $api = new Flickr::API({'key'    => $key, 'secret' => $secret});

my $auth_token = '' || get_auth_token();
my $debug = 0; # TODO argument

# dbi initialisation
my $dbh = DBI->connect("dbi:SQLite:dbname=$ENV{'HOME'}/.flickrexif.db");
db_create($dbh);

# main app
my @labels = ("Aperture", "Exposure", "Exposure Bias", "Flash", 
              "Focal Length", "ISO Speed", "Make", "Model",);

print "Initialised\n";

my @photo_ids = get_all_photo_ids($user_id); 
# if you want to only handle the first page, put in a second argument 
# which is true, like 'only first page'

print "I have ".scalar(@photo_ids)." to process\n";
print join(", ", @photo_ids) if ($debug >= 2);

foreach my $photo_id (@photo_ids) {
  print "Handling photo $photo_id\n" if $debug;

  my $exif = get_exif($photo_id);

  set_machine_tags($photo_id, $exif);

  db_add_data($dbh, $photo_id, $exif) if (scalar keys %{ $exif });
}

### Flickr

sub get_all_photo_ids {
  my $user_id = shift;
  my $return  = shift;

  my @photo_ids;

  my ($photo_ids, $pages) = get_photo_ids($user_id, 1);
  
  push @photo_ids, @{ $photo_ids };
  
  # handy for debugging- use one page only
  return @photo_ids if $return;

  foreach my $page (2..$pages) {
    my ($photo_ids) = get_photo_ids($user_id, $page);
    push @photo_ids, @{ $photo_ids };
  }
  
  return @photo_ids;
}

sub get_photo_ids {
  my $user_id = shift;
  my $page    = shift;

  my $photo_ids = [];
  my $info = get_flickr_data('flickr.photos.search',
                             { 'user_id'  => $user_id,
                               'per_page' => 500,
                               'page'     => $page,
                               'extras'   => 'tags',
                             });

  my $nodes = $info->findnodes('/rsp/photos');
  my $pages = $nodes->[0]->findvalue('@pages');

  print "  Got page $page (of $pages) of photos\n";

  my $photo_nodes = $info->findnodes('//photo');
  foreach my $photo_node (@{ $photo_nodes }) {
    next if ($photo_node->getAttribute('tags') =~ /meta:exif/);
    push @{ $photo_ids }, $photo_node->getAttribute('id');
  }
  
  return ($photo_ids, $pages);
}

sub get_exif {
  my $photo_id = shift;

  my $info = get_flickr_data('flickr.photos.getExif', {'photo_id' => $photo_id});
  my $exif;

  foreach my $label (@labels) { # TODO bad global
    print "    Looking for $label\n" if $debug;
    my $data = $info->findvalue("//exif[\@label='$label']/clean");
    if (!$data) {
      print "    ... looking for raw data\n" if $debug;
      $data = $info->findvalue("//exif[\@label='$label']/raw");
    }
    if ($data->size() > 1) {
        $data = $data->shift();
    }
    $data = $data->to_literal();

    print "    ... got '$data'\n" if $debug;
    $exif->{$label} = $data unless (!defined($data) || $data eq '');
  }

  # special case for filename
  my $name = $info->findvalue("//exif[\@label='Object Name (Title)']/raw");
  $exif->{'Filename'} = $name unless !defined $name;

  return $exif;
}

sub set_machine_tags {
  my ($photo_id, $exif) = @_;

  my @machine_tags;

  foreach my $label (@labels) { # TODO bad global
    if ($exif->{$label}) {
    
      my $predicate = "exif";
      my $value     = $exif->{$label};
      my $name      = lc($label);
      $name =~ s/ /_/g;

      if ($name eq 'make' || $name eq 'model') {
        $predicate = "camera";
      }

      if ($name eq 'make' || $name eq 'model') {
        $predicate = "camera";
      }

      print qq(Got tag "$predicate:$name=$value"\n) if $debug;
      push @machine_tags, '"'.$predicate.':'.$name.'='.$value.'"';
    }
  }

  # special case for filename
  if ($exif->{'Filename'}) {
    push @machine_tags, '"file:name='.$exif->{'Filename'}.'"';
  }
  # TODO extension? find full path ('location') too? use this
  # for lens tags?

  if (scalar @machine_tags) {
    print "  Got ".scalar(@machine_tags)." exif tags for $photo_id.\n";
    push @machine_tags, "meta:exif=".time;
  } else {
    print "  Got no exif tags for $photo_id.\n";
    push @machine_tags, "meta:exif=none";
  }

  my $machine_tags = join(" ", @machine_tags);

  # finally, do tagging
  my $info = get_flickr_data('flickr.photos.addTags', 
                             { 'photo_id' => $photo_id,
                               'tags'     => $machine_tags, });

  print "  Set ".scalar(@machine_tags)." machine tags for $photo_id.\n";
}

### SQLite database handling

sub db_create {
  my $dbh = shift;  
  
  my $create = <<EOC;
  create table if not exists
               exif (id INTEGER PRIMARY KEY,
                     aperture TEXT,
                     exposure TEXT,
                     exposure_bias TEXT,
                     flash TEXT,
                     focal_length TEXT,
                     iso_speed INTEGER,
                     make TEXT,
                     model TEXT,
                     exposure_no REAL,
                     focal_length_no REAL);
EOC

  # TODO better schema
  # exposure could be a number or time
  #   - trigger
  # iso speed is a number
  #   - trigger
  # focal length could be a number
  
  $dbh->do($create);
}

sub db_add_data {
  my $dbh      = shift;
  my $photo_id = shift;
  my $exif     = shift;
  
  # TODO bad global
  my @fields = @labels;
  @fields = map { s/ /_/g; lc $_ } sort @fields;
  
  my $sql = "INSERT OR REPLACE INTO exif(id, ".join(", ", @fields).") ";
  $sql   .= "VALUES ("."?," x scalar(@fields)."?)";

  my $sth = $dbh->prepare($sql);
  my @insert = ($photo_id);
  foreach my $label (sort @labels) { 
    push @insert, $exif->{$label};
  }
  
  print "  About to insert '".join("', '", @insert)."'\n" if $debug;

  $sth->execute(@insert); # TODO error capture

  print "  Stored local info for '$photo_id'\n";

  return 1;
}

# Flickr - utility wrapper

sub get_flickr_data {
  my ($method, $args) = @_;
  
  $args->{'auth_token'} = $auth_token; # TODO remove global

  my $request = new Flickr::API::Request({
                  'method' => $method,
                  'args'   => $args,
                });
  
  my $response = $api->execute_request($request);
  die "Error calling $method: $response->{error_message}\n"
    unless $response->{'success'};
  my $buffer  = $response->content;
  my $content = Compress::Zlib::memGunzip($buffer);
  if (!$content) {
    $content = $response->content;
  }

  my $parser = XML::LibXML->new();
  my $info = $parser->parse_string( $content );

  return $info;
}

# Flickr - auth token fetching cheekily nicked from flickr_upload

sub get_auth_token {
  # The user wants to authenticate. There's really no nice way to handle this.
  # So we have to spit out a URL, then hang around or something until
  # the user hits enter, then exchange the frob for a token, then tell the user what
  # the token is and hope they care enough to stick it into .flickrrc so they
  # only have to go through this crap once.

  # 1. get a frob
  my $frob = get_frob( $api );

  # 2. get a url for the frob
  my $url = $api->request_auth_url('write', $frob);

  # 3. tell the user what to do with it
  print "1. Enter the following URL into your browser\n\n",
        "$url\n\n",
        "2. Follow the instructions on the web page\n",
      "3. Hit <Enter> when finished.\n\n";
  
  # 4. wait for enter.
  <STDIN>;

  # 5. Get the token from the frob
  my $auth_token = get_token( $api, $frob );
  die "Failed to get authentication token!" unless defined $auth_token;
  
  # 6. Tell the user what they won.
  print "Your authentication token for this application is\n\t\t", $auth_token, "\n";
  print "Edit this program and insert this at line 20\n";
  
  exit 0;
}

sub get_frob {
  my $api = shift;

  my $res = $api->execute_method("flickr.auth.getFrob");
  return undef unless defined $res and $res->{success};

  # FIXME: error checking, please. At least look for the node named 'frob'.
  return $res->{tree}->{children}->[1]->{children}->[0]->{content};
}

sub get_token {
  my $api = shift;
  my $frob = shift;

  my $res = $api->execute_method("flickr.auth.getToken",
    { 'frob' => $frob } );
  return undef unless defined $res and $res->{success};

  # FIXME: error checking, please.
  return $res->{tree}->{children}->[1]->{children}->[1]->{children}->[0]->{content};
}


