#!/usr/bin/env perl
# -*- perl -*-

#
# Author: Slaven Rezic
#
# Copyright (C) 2020,2022,2023,2024 Slaven Rezic. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# Mail: slaven@rezic.de
# WWW:  http://www.rezic.de/eserte/
#

use strict;
use warnings;
use FindBin;
use lib "$FindBin::RealBin/..";

use File::Temp;
use Getopt::Long;
use Hash::Util qw(lock_keys);
use LWP::UserAgent;
use Text::Wrap;
use Time::Piece;
use XML::LibXML;

use BBBikeUtil qw(is_in_path);

use constant STATUS_URL => 'https://prometheus.openstreetmap.org/d/5rTT87FMk/web-site?orgId=1&refresh=1m&from=now-3h&to=now&viewPanel=16';

my $use_pager = -t STDOUT && is_in_path('less');
my $do_blame;
my $diff_context = 0;

GetOptions(
	   'pager!' => \$use_pager,
	   'blame'  => \$do_blame,
	   'diff-context=i' => \$diff_context,
	  )
    or die "usage: $0 [--no-pager] osmobject/id\n";

my $api_root_url = "https://api.openstreetmap.org/api/0.6";

my $elem = shift
    or die qq{Please specify osm element (e.g. "way/12345")\n};
if ($elem =~ m{((?:way|node|relation)/\d+)}) {
    $elem = $1;
} elsif ($elem =~ m{(way|node|relation)=(\d+)}) {
    # id URLs, e.g. https://www.openstreetmap.org/edit?editor=id&way=140964912#map=20/52.44433/13.56909
    $elem = "$1/$2";
} else {
    die qq{"$elem" cannot be parsed as an osm element\n};
}

my $history_url = "$api_root_url/$elem/history";

my $ua = LWP::UserAgent->new(keep_alive => 1, timeout => 10); # normal response times are below 10ms
my $resp = $ua->get($history_url);
if (!$resp->is_success) {
    dump_failed_resp($resp);
}

my $today = localtime->strftime('%F');

if ($use_pager) {
    my @pager_cmd = ('less', '-S');
    open STDOUT, '|-', @pager_cmd
	or die "Failed to run '@pager_cmd': $!";
}
binmode STDOUT, ':utf8';

my $dom = XML::LibXML->load_xml(string => $resp->decoded_content);
my @nodes = $dom->findnodes('/osm/*');
if ($do_blame) {
    blame(\@nodes);
} else {
    history_log(\@nodes);
}

close STDOUT; # required for pager operation

sub history_log {
    my $nodes = shift;

    for(my $node_i=$#$nodes; $node_i>=0; $node_i--) {
	my $newer_node = $nodes->[$node_i];
	my $older_node = $nodes->[$node_i-1];

	# changeset comment
	my $changeset_id = $newer_node->findvalue('./@changeset');
	my $changeset = get_changeset($changeset_id);
	print "="x70,"\n";
	print "id $changeset->{id}\n";
	print "OSMCha-URL: https://osmcha.org/changesets/$changeset->{id}\n";
	print "achavi-URL: https://overpass-api.de/achavi/?changeset=$changeset->{id}&relations=true\n";
	print "OSM-URL:    https://www.openstreetmap.org/changeset/$changeset->{id}\n";
	print "Author: $changeset->{user}\n";

	print "Date:   $changeset->{closed_at}";
	if ($changeset->{closed_at} =~ /^(\d{4}-\d{2}-\d{2})/) {
	    my $isodate = $1;
	    if ($isodate eq $today) {
		print " (today)";
	    } else {
		my $date = Time::Piece->strptime($isodate, '%Y-%m-%d');
		if ($date) {
		    my $days = int((time - $date->epoch) / 86400);
		    if ($days < 365) {
			print " (about $days day" . ($days != 1 ? 's' : '') . " ago)";
		    } else {
			my $years = int($days/365);
			print " (about $years year" . ($years != 1 ? 's' : '') . " ago)";
		    }
		}
	    }
	}
	print "\n";

	print "\n";
	print Text::Wrap::wrap("    ", "    ", $changeset->{comment}) . "\n\n";

	# diff
	my $newer = File::Temp->new(TEMPLATE => "osmhistory_XXXXXXXX", TMPDIR => 1);
	$newer->print($newer_node->toString . "\n");
	my $new_version = $newer_node->findvalue('./@version');

	my $older;
	my $old_version;
	if ($node_i > 0) {
	    $older = File::Temp->new(TEMPLATE => "osmhistory_XXXXXXXX", TMPDIR => 1);
	    $older->print($older_node->toString . "\n");
	    $old_version = $nodes[$node_i-1]->findvalue('./@version');
	} else {
	    $older = "/dev/null";
	    $old_version = "0";
	}
	open my $fh, '-|', 'diff', "--label=$old_version", "--label=$new_version", "--unified=$diff_context", $older, $newer
	    or die "Error while running diff command: $!";
	while(<$fh>) {
	    next if /^\@\@/;
	    print $_;
	}
	print "\n";
    }
}

sub blame {
    my $nodes = shift;

    my %open_kv;
    my %closed_kv;
    my $last_changeset_id = $nodes->[-1]->getAttribute('changeset');
    for my $tag_node ($nodes->[-1]->findnodes('./tag')) {
	my $k = $tag_node->getAttribute('k');
	my $v = $tag_node->getAttribute('v');
	$open_kv{$k} = { v => $v, changeset_id => $last_changeset_id };
    }

    for(my $node_i=$#$nodes; $node_i>=0; $node_i--) {
	my $changeset_id = $nodes->[$node_i]->getAttribute('changeset');
	my %unchanged_kv;
	for my $tag_node ($nodes->[$node_i]->findnodes('./tag')) {
	    my $k = $tag_node->getAttribute('k');
	    next if $closed_kv{$k};
	    if (exists $open_kv{$k}) {
		my $v = $tag_node->getAttribute('v');
		if ($v ne $open_kv{$k}->{v}) {
		    $closed_kv{$k} = delete $open_kv{$k};
		} else {
		    $unchanged_kv{$k} = 1;
		}
	    }
	}
	for my $k (keys %open_kv) {
	    if ($unchanged_kv{$k}) {
		$open_kv{$k}->{changeset_id} = $changeset_id;
	    } elsif ($node_i == 0) {
		$closed_kv{$k} = delete $open_kv{$k};
		$closed_kv{$k}->{changeset_id} = $changeset_id;
	    } else {
		$closed_kv{$k} = delete $open_kv{$k};
	    }
	}
	last if !%open_kv;
    }

    for my $k (sort {
	my $cmp = $closed_kv{$b}->{changeset_id} <=> $closed_kv{$a}->{changeset_id};
	if ($cmp == 0) {
	    $a cmp $b;
	} else {
	    $cmp;
	}
    } keys %closed_kv) {
	my $v = $closed_kv{$k}->{v};
	my $changeset_id = $closed_kv{$k}->{changeset_id};
	if (!defined $changeset_id) {
	    warn "Unexpected: No changeset id for '$k'";
	}
	my $changeset_info = get_cached_changeset($changeset_id);
	printf "%s: %s\n", $k, $v;
	printf "  %s %s %s\n", $changeset_info->{user}, $changeset_info->{closed_at}, $changeset_info->{comment};
    }
}

sub get_changeset {
    my($changeset_id) = @_;

    my %changeset;

    $changeset{id} = $changeset_id;
    $changeset{url} = "$api_root_url/changeset/$changeset_id";
    my $changeset_resp = $ua->get($changeset{url});
    if (!$changeset_resp->is_success) {
	dump_failed_resp($changeset_resp);
    }
    my $changeset_dom = XML::LibXML->load_xml(string => $changeset_resp->decoded_content);
    my($changeset_elem) = $changeset_dom->findnodes('/osm/changeset');
    $changeset{user}      = $changeset_elem->getAttribute('user');
    $changeset{closed_at} = $changeset_elem->getAttribute('closed_at');
    $changeset{comment}   = $changeset_elem->findvalue('./tag[@k="comment"]/@v');

    lock_keys %changeset;
    \%changeset;
}

{
    my %cached_changeset;
    sub get_cached_changeset {
	my($changeset_id) = @_;
	if (!exists $cached_changeset{$changeset_id}) {
	    $cached_changeset{$changeset_id} = get_changeset($changeset_id);
	}
	$cached_changeset{$changeset_id};
    }
}

sub dump_failed_resp {
    my $resp = shift;
    my $msg = "Failed to fetch ";
    if ($resp->request) {
	$msg .= "'" . $resp->request->url . "'";
    } else {
	$msg .= "resource";
    }
    $msg .= ":\n" . $resp->dump . "\n\nMore information possibly at:\n\n    " . STATUS_URL . "\n\n";
    die $msg;
}

__END__

=head1 NAME

osmhistory - format OSM element history like "git log -p"

=head1 SYNOPSIS

    osmhistory [--no-pager] way/123456

=cut
