#!/usr/bin/perl
# Filename:	vidb
# Author:	David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License
# Description:	Edits a database like a text file
#		Uses DB_File::Lock style locking
#		  but not between read and write! (so writes can be lost)
use strict;
use DB_File::Lock;
use Fcntl ':flock';

my $PROGNAME = $0;
$PROGNAME =~ s|.*/||;

my $EDITOR = $ENV{EDITOR} || "vi";

my $TMP = "/tmp/$PROGNAME.$$";

##################################################
# Usage
##################################################
sub usage {
  foreach my $msg (@_) { print STDERR "ERROR:  $msg\n"; }
  print STDERR "\n";
  print STDERR "Usage:\t$PROGNAME [-l] [-d] <db>\n";
  print STDERR "\tEdits a database like a text file\n";
  print STDERR "\tUses DB_File::Lock style locking\n";
  print STDERR "\t-l\tJust list the database contents\n";
  print STDERR "\t-n\tCreate db if not existing\n";
  print STDERR "\t-L\tHold lock for db while editing\n";
  print STDERR "\n";
  print STDERR "\t-d\tSet debug mode\n";
  print STDERR "\n";
  exit -1;
}

sub parse_args {
  my ($file,$list,$hold);
  while (my $arg=shift(@ARGV)) {
    if ($arg =~ /^-h$/) { usage(); }
    if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; }
    if ($arg =~ /^-l$/) { $list=1; next; }
    if ($arg =~ /^-L$/) { $hold=1; next; }
    if ($arg =~ /^-n$/) { $MAIN::CREATE=1; next; }
    if ($arg =~ /^-/) { usage("Unknown option: $arg"); }
    usage("Too many databases specified [$arg and $file]") if (defined($file));
    $file=$arg;
  }
  usage("No database specified") if (!defined($file));

  ($file,$list,$hold);
}

sub debug {
  return unless $MAIN::DEBUG;
  foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; }
}

##################################################
# Code
##################################################
sub from_db {
  my ($str) = @_;
  $str =~ s/([%\t\n])/"%".sprintf("%0.2x",ord($1))/eg;
  $str;
}
sub to_db {
  my ($str) = @_;
  $str =~ s/%([0-9a-f]{2})/chr(hex($1))/eig;
  $str;
}

sub list_db {
  my ($file) = @_;
  open(TMP,">$TMP") || die("Couldn't write [$TMP]\n");
  my %db;
  tie %db, 'DB_File::Lock', $file, O_RDONLY, 0666, $DB_HASH, "read"
    or die("Can't read db [$file]");
  my %copy = %db;	# Faster untie,  since we need the whole thing
  untie %db;
  foreach my $k ( sort keys %copy ) {
    print "$k\t-> $copy{$k}\n";
  }
}

my %_db;
sub read_db {
  my ($file,$hold) = @_;
  open(TMP,">$TMP") || die("Couldn't write [$TMP]\n");
  my $flags = $hold ? O_RDWR : O_RDONLY;
  $flags |= O_CREAT if $MAIN::CREATE;
  my $lock = ($MAIN::CREATE || $hold) ? "write" : "read";
  tie %_db, 'DB_File::Lock', $file, $flags, 0666, $DB_HASH, $lock
    or die("Can't $lock db [$file]\n");
  my %copy = %_db;	# Faster untie,  since we need the whole thing
  untie %_db unless $hold;
  foreach my $k ( sort keys %copy ) {
    print TMP from_db($k),"\t",from_db($copy{$k}),"\n";
    debug("Read: $k\t-> $copy{$k}\n");
  }
  close TMP;
}

sub edit_db() {
  my $mod = -M $TMP;
  system("$EDITOR $TMP");
  return -M $TMP != $mod ? 1 : 0;
}

sub write_db {
  my ($file,$hold) = @_;
  open(TMP,"<$TMP") || die("Couldn't read [$TMP]\n");
  unless ($hold) {
    tie %_db, 'DB_File::Lock', $file, O_CREAT|O_RDWR, 0666, $DB_HASH, "write"
      or die("Can't write db [$file]");
  }
  # Clear the hash first
  delete @_db{keys %_db};
  my $errors=0;
  while(<TMP>) {
    chomp;
    if (/^([^\t]+)(\t(.+)?)?$/) {
      my ($k,$v) = (to_db($1),to_db($3));
      $_db{$k} = $v;
      debug("Wrote: $k->$v\n");
    } else {
      print STDERR "Can't parse database line [$.]:\n  $_";
      $errors++;
    }
  }
  untie %_db;
  close TMP;
  while ($errors) {
    print STDERR "\nE)dit it again, F)orget it?  e";
    # Char mode
      my $ttyname=`/usr/bin/tty`;
      system "/bin/stty -icanon -echo min 1 < $ttyname " if (! $?);
    my $ans;
    read(STDIN,$ans,1);
    # Line mode
      `/usr/bin/tty -s`;
      system "/bin/stty icanon echo < $ttyname " if (! $? ); 
    print "$ans\n";
    return if ($ans =~ /F/i);
    return edit_db if ($ans =~ /E/i);
  }
}

##################################################
# Main code
##################################################
sub main {
  my ($file,$list,$hold) = parse_args();

  return list_db($file) if $list;
  read_db($file,$hold);
  write_db($file,$hold) if edit_db;
  unlink($TMP);
}
main();
