#!/usr/bin/env perl
# -*-perl-*-
# Header comments and licensing info {{{1
#
# LiveJournal Client Account Migrator
# (see also "sclj" and "http://www.livejournal.com/")
#
# This version by:
# C. A. "Sapphire Cat" Daelhousen
# http://www.livejournal.com/community/sclj
# $Id: scljconv,v 1.1.1.1 2003/11/21 02:45:07 sapphirecat Exp $
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program, in a file named "LICENSE"; if not, write to:
#    Free Software Foundation, Inc.
#    59 Temple Place, Suite 330
#    Boston, MA  02111-1307  USA
#

# Pragmas (pragmae?) {{{1
use 5.005; # As required by sclj (due to LWP)
#eval "use warnings" if($^V && $^V >= 5.006);
# XXX: Do we need anything else?

# Config, the global configuration singleton {{{1
package SCLJ::Config;
my %config = ( # Defaults {{{2
	'path'        => "$ENV{'HOME'}/.sclj3",
# Hardcoded into earlier clients
	'server'      => 'www.livejournal.com',
	'serverport'  => '80',
	'serverpath'  => '/interface/flat',
# Don't worry about converting v2 until v3 arrives, possibly never.
	'files'       => ["$ENV{'HOME'}/.livejournal/rc", "$ENV{'HOME'}/.lj/rc",
		"$ENV{'HOME'}/.livejournal.rc" ]
);

sub val { # (var, [newval]) - get/set var; return old val on set {{{2
	my $ret = undef;

	if(defined $_[1]) {
		$ret = (exists $config{$_[1]} ? $config{$_[1]} : undef);
		if(defined $_[2]) {
			$config{$_[1]} = $_[2];
		}
	}

	return $ret;
}

sub path_prepend { # (file) - return "path/file" {{{2
	defined $_[1] && defined $config{'path'} ? "$config{'path'}/$_[1]" : undef;
}

sub parse_opts { # (options) - parse command-line options {{{2
	my ($class, $opts) = @_;
	my ($i, $opt, $letter, $optok, $what, @expected, @ignored);

	$optok = 1;
	@ignored = ();
	for $i (0..$#{$opts}) {
		$opt = $opts->[$i];

		if($optok && $opt =~ /^-.+/) {
			# We don't have anything that needs this yet.
			# do { $optok = 0; next } if $opt eq '--';
			$opt =~ s/^-//;
			for $letter (split(//, $opt)) {
				if($letter =~ /-/) { # Catch '--help' gracefully
					$class->usage("Long options aren't supported.");
				} elsif($letter =~ /d/) {
					push(@expected, 'path');
				} else {
					push(@ignored, "-$letter");
				}
			}
		} elsif($optok && $opt =~ /^\+.+/) {
			$opt =~ s/^\+//;
			for $letter (split(//, $opt)) {
				push(@ignored, "+$letter");
			}
		} elsif($optok && $#expected >= 0) {
			$what = shift(@expected);
			$class->val($what, $opt);
		# We don't support any non-options yet, so we don't need to care about
		# !$optok. So just ignore it for now.
		#} else {
			#$class->usage("Foo");
		}

		# Didn't find something expected. This is why we don't just do
		# for(@{$opts}).
		if($i == $#{$opts} && $#expected >= 0) {
			$class->usage("Not enough arguments found to go with the options.");
		}
	}

	if($#ignored >= 0) {
		print "Warning: unrecognized options ignored: " .
			join(' ', @ignored) . "\n";
	}
}

sub usage { # ([msg]) - print usage because of msg {{{2
	my ($class, $msg) = @_;
	my ($base);

	$base = $0;
	$base =~ s#^.*/##;
	
	if(defined $msg) {
		print "$msg\n\n";
	}

	print <<ENDUSAGE;
Usage: $base [-d <directory>]

Options:
  -d\tWork with <directory> instead of \$HOME/.sclj3

Note: "-dfoo" is equivalent to "-d -f -o -o", not "-d foo".
ENDUSAGE

	exit 1;
}

# File, and the data within it {{{1
package SCLJ::File;

sub new { # ([path], name) - Constructor {{{2
	my ($class, $path, $name) = @_;
	my $self = {};

	return undef if ref $class;

	# Argument munging
	return undef unless defined $path;
	do { $name = $path, $path = $class->default_path } unless defined $name;

	# There's nothing stopping $name from containing path components; $path is
	# just the path to the base dir, then things like accounts/foo can appear
	# beneath it.
	$self->{'path'} = $path;
	$self->{'name'} = $name;
	$self->{'err'} = undef;

	bless $self, $class;
}

# Accessors {{{2
sub default_path { SCLJ::Config->val('path') } # () {{{3

sub filename { # () {{{3
	return undef unless ref $_[0];
	"$_[0]->{'path'}/$_[0]->{'name'}";
}

sub get { # (value) {{{3
	return undef unless ref $_[0];
	(exists $_[0]->{'data'}->{$_[1]} ? $_[0]->{'data'}->{$_[1]} : undef);
}

sub error { # () - Return error message {{{3
	return unless ref $_[0];
	$_[0]->{'err'};
}

sub save { # () - Save values from a hash to a file (unsorted.) {{{3
	my ($class) = @_;

	return unless ref $class && !defined $class->error;

	if(!open(FILE, '>' . $class->filename)) {
		$class->{'err'} = $!;
		return undef;
	}

	print FILE "# sclj data file v2\n".
		"# quotes required; \\\\ and \\\" recognized.\n\n";
	$class = $class->__SaveHash($class->__Escape("root"), $class->{'data'}, { },
		\*FILE);

	close FILE;

	return $class;
}

# Save a single hash to a file {{{4
sub __SaveHash {
	my ($class, $name, $hash, $saved, $file) = @_;
	my (@keys, %refs);

	@keys = keys %{$hash};
	%refs = ();

	print $file "HASH \"$name\"\n";

	for my $key (@keys) {
		my $val = $hash->{$key};

		my $ekey = $class->__Escape($key);
		print $file "\tKEY \"$ekey\" ";

		if(!defined $val) {
			print $file "UNDEFINED\n";
		} elsif(!ref $val) {
			# Scalars can be saved directly
			print $file "SCALAR \"" . $class->__Escape($val) . "\"\n";
		} elsif(ref $val eq 'HASH') {
			if(exists $saved->{$val}) {
				# It's already saved; make the reference point to that
				print $file "HASHREF \"$saved->{$val}\"\n";
			} else {
				# It's not saved; name it and make note that it needs to be
				print $file "HASHREF \"${name}_${ekey}\"\n";
				$refs{$val} = [$val, "${name}_${ekey}"];
			}
		} elsif(ref $val eq 'ARRAY') {
			if(exists $saved->{$val}) {
				print $file "LISTREF \"$saved->{$val}\"\n";
			} else {
				print $file "LISTREF \"${name}_${ekey}\"\n";
				$refs{$val} = [$val, "${name}_${ekey}"];
			}
		}
	}

	# Complete writing this hash before attempting to save any more
	print $file "END \"$name\"\n\n";
	$saved->{$hash} = $name;

	# Check for new references that appeared in this hash
	for my $key (keys %refs) {
		my $obj = $refs{$key}->[0];
		my $objname = $refs{$key}->[1];

		# We already know $saved->{$obj} does not exist, because things only
		# appear in %refs if that is the case.
		if(ref $obj eq 'HASH') {
			$class->__SaveHash($objname, $obj, $saved, $file);
		} elsif(ref $obj eq 'ARRAY') {
			$class->__SaveList($objname, $obj, $saved, $file);
		}
		# else: nothing can get here, because $obj is only in %refs if its type
		# was valid above.
	}

	return $class;
}

# Save a single list to a file (which must be select()'ed) {{{4
sub __SaveList {
	my ($class, $name, $list, $saved, $file) = @_;
	my ($eid, %refs);

	%refs = ();

	print $file "LIST \"$name\"\n";

	$eid = 0;
	for my $elt (@{$list}) {
		print $file "\t";
		if(!defined $elt) {
			print $file "UNDEFINED\n";
		} elsif(!ref $elt) {
			# Scalars can be saved directly
			print $file "SCALAR \"" . $class->__Escape($elt) . "\"\n";
		} elsif(ref $elt eq 'HASH') {
			if(exists $saved->{$elt}) {
				# It's already saved; make the reference point to that
				print $file "HASHREF \"$saved->{$elt}\"\n";
			} else {
				# It's not saved; name it and make note that it needs to be
				print $file "HASHREF \"${name}_${eid}\"\n";
				$refs{$elt} = [$elt, "${name}_${eid}"];
			}
		} elsif(ref $elt eq 'ARRAY') {
			if(exists $saved->{$elt}) {
				print $file "LISTREF \"$saved->{$elt}\"\n";
			} else {
				print $file "LISTREF \"${name}_${eid}\"\n";
				$refs{$elt} = [$elt, "${name}_${eid}"];
			}
		}

		$eid++;
	}

	# Complete writing this list before attempting to save any more
	print $file "END \"$name\"\n\n";
	$saved->{$list} = $name;

	# Check for new references that appeared in this list
	for my $key (keys %refs) {
		my $obj = $refs{$key}->[0];
		my $objname = $refs{$key}->[1];

		# We already know $saved->{$obj} does not exist, because things only
		# appear in %refs if that is the case.
		if(ref $obj eq 'HASH') {
			$class->__SaveHash($objname, $obj, $saved, $file);
		} elsif(ref $obj eq 'ARRAY') {
			$class->__SaveList($objname, $obj, $saved, $file);
		}
		# else: nothing can get here, because $obj is only in %refs if its type
		# was valid above.
	}

	return $class;
}

# Backslash-escape a string to be written to a file. {{{4
sub __Escape {
	my ($class, $str) = @_;
	$str =~ s/([\\"])/\\$1/g;
	return $str;
}

# Mutators {{{2
sub set { # (value, to) - Set value to to {{{3
	return unless ref $_[0] && defined $_[1] && !ref $_[1];
	$_[0]->{'data'}->{$_[1]} = $_[2];
	return $_[0];
}

sub unset { # (value) - Forget about value {{{3
	return undef unless ref $_[0] && exists $_[0]->{'data'}->{$_[1]};
	delete $_[0]->{'data'}->{$_[1]};
	return $_[0];
}

sub clearerr { # () - Remove error status {{{3
	return unless ref $_[0];
	$_[0]->{'err'} = undef;
	return $_[0];
}

sub rename { # (newname) - Change filename {{{3
	return unless ref $_[0];
	$_[0]->{'name'} = $_[1] if defined $_[1] && !ref $_[1];
	return $_[0];
}

sub load { # () - Load a hash with values from a file {{{3
	my ($class) = @_;
	my ($quoteparse, $structname, $struct, $nextloc, %structs, %waiters, $in,
		$which);

	return undef unless ref $class && !defined $class->error;

	if(!-e $class->filename) {
		if(!open(FILE, '>' . $class->filename)) {
			$class->{'err'} = $!;
			return undef;
		}
		close FILE;
		$class->{'data'} = {};
		return $class;
	} elsif(!open(FILE, '<' . $class->filename)) {
		$class->{'err'} = $!;
		$class->{'data'} = {};
		return undef;
	}

	# TMTOWTDI, I'm sure.
	# Match any number of: pairs of backslashes, backslash-quote pairs, and any
	# character that has nothing to do with backslashes or quotes.
	$quoteparse = qr{"((?:\\\\|\\"|[^\\"])*?)"\s*};

	$structs{'LIST'} = { };
	$structs{'HASH'} = { };
	$waiters{'LIST'} = { };
	$waiters{'HASH'} = { };

	$structname = '';

LINE:
	for (<FILE>) {
		chomp;
		next LINE if /^\s*(#.*)?$/gc; # Blank or comment
		/^\s*/gc; # Skip leading whitespace

		if(!defined $struct) { # outside structure
			if(/\G(HASH|LIST)\s+$quoteparse/gc) {
				$structname = $class->__UnEscape($2);

				$in = $1;
				if($in eq 'HASH') {
					$struct = { };
				} else { # $which eq 'LIST' because of the pattern match
					$struct = [ ];
				}
			}
		} else { # inside structure
			if(/\GEND\s+$quoteparse/gc) { # Leaving structure
				#my $ending = $class->__UnEscape($1);
				# We assume END is always well-formed in here...

				$which = $in;

				if(exists $waiters{$which}->{$structname}) {
					# Something was waiting for this struct: fill those locations
					for my $locref (@{$waiters{$which}->{$structname}}) {
						$$locref = $struct;
					}

					delete $waiters{$which}->{$structname};
				}

				# Now store the struct for future references
				$structs{$which}->{$structname} = $struct;

				$struct = undef;
				next LINE;
			} # END keyword

			if($in eq 'HASH') { # Hash key
				if(/\GKEY\s+$quoteparse/gc) {
					my $key = $class->__UnEscape($1);
					$struct->{$key} = undef;
					$nextloc = \$struct->{$key};
				} else {
					next LINE;
				}
			} elsif($in eq 'LIST') { # List item
				my $index = $#{$struct};
				$struct->[$index+1] = undef;
				$nextloc = \$struct->[$index+1];
			} else { # In neither hash nor list!
				die "Hash loading detected impossible state";
			}

			# Figure out what the item is
			if(/\GUNDEFINED/gc) {
				# Undefined. We can always store it now.
				$$nextloc = undef;
			} elsif(/\GSCALAR\s+$quoteparse/gc) {
				# Only a scalar. We can always store it now.
				my $val = $class->__UnEscape($1);
				$$nextloc = $val;
			} elsif(/\G(LIST|HASH)REF\s+$quoteparse/gc) {
				# Some sort of structure...
				my $name = $class->__UnEscape($2);

				$which = $1;
				if(exists $structs{$which}->{$name}) {
					# We already have the structure; store a reference to it now.
					$$nextloc = $structs{$which}->{$name};
				} elsif(exists $waiters{$which}->{$name}) {
					# Add to the existing list of locations.
					push(@{$waiters{$which}->{$name}}, $nextloc);
				} else { # We are the first waiter
					# Create a new key to store locations where the structure will
					# go once its END keyword is read from the file.
					$waiters{$which}->{$name} = [ $nextloc ];
					$$nextloc = undef; # In case it's never defined
				}
			}
		} # if(outside structure)
	} # LINE

	close FILE;

	$class->{'data'} = $structs{'HASH'}->{'root'};
	return $class;
}

# Un-backslash-escape a string read from a file. {{{4
sub __UnEscape {
	my ($class, $str) = @_;
	$str =~ s/\\(.)/$1/g;
	return $str;
}

# Converter, which understands versions {{{1
package SCLJ::Converter;
@ISA = qw(SCLJ::File);
# Version-specific key mappings {{{2
# Anything undefined causes a call to _convert_FOO_key(file, key), where FOO
# is the version being converted.
my %ver_map = (
	'v1' => { # {{{3
		'account' => {
			'user' => 'user',
			'password' => undef,
			'hpassword' => undef,
			'paranoid' => 'paranoid'
		},
		'server' => {
			'host' => 'host',
			'port' => 'port',
			'path' => 'path'
		},
		'rc' => {
			'proxy_host' => 'proxy_host',
			'proxy_port' => 'proxy_port',
			'editor' => 'editor'
		}
	},
	'jlj' => { # {{{3
		'account' => {
			'user' => 'user',
			'password' => undef
		},
		'server' => {
			'server' => 'host',
			'postcgi' => 'path'
		},
		'rc'   => {
			'proxy' => undef,
			'editor' => 'editor'
		},
		'post defaults' => {
			'backdate' => undef,
			'allowcomments' => undef,
			'format' => undef,
			'security' => undef
		}
	}
);

sub new { # (filename) {{{2
	my ($class, $filename) = @_;
	my ($self, $version);

	# absolute pathname on filename required
	return if ref $class || $filename !~ m#^/#;

	$version = $class->guess_ver($filename);

	my (@filehack) = split(m#(/)#, $filename);
	my ($file) = pop(@filehack);
	pop(@filehack) if $#filehack > 0; # strip / for /foo/bar and not /foo
	my $path = join('', @filehack);

	$self = $class->SUPER::new($path, $file); # Blessing happens here
	return unless defined $self;

	if(defined $version) {
		$self->{'ver'} = $version;
		$self->{'_ver_map'} = \%ver_map;
	} else {
		$self->{'err'} = "Couldn't determine file type";
	}

	return $self;
}

sub guess_ver { # (filename) - guess version/filetype {{{2
	my $class = shift;
	my $filename = shift;
	my $index;
	my %magic = (
		'any' => {
			# A non-exhaustive listing of JLJ feature names that differ from v1
			'jlj' => qr{(?:server|postcgi|community|backup|autolink):},
			'v2' => qr{HASH\s+\"root\"}
		},
		'all' => {
			'jlj' => qr{(?:^\s*$|:|#)},
			'v1' => qr{(?:^\s*$|:)}
		},
		'line_1' => {
			'v2' => qr{sclj data file v2}
		}
	);
	my $guess = 'v1'; # Default

	return if -B $filename; # rc files should be mostly ASCII
	# Currently, we don't recognize anything that builds a subdirectory.
	do { closedir DIR; return } if(opendir(DIR, $filename));
	return unless open(FILE, '<' . $filename);

	$index = 1;
SCAN_LOOP:
	for my $line (<FILE>) {
		# Finding something on a specific line decides
		my $lkey = "line_$index";
		if(exists $magic{$lkey}) {
			for my $ver (keys %{$magic{$lkey}}) {
				if($line =~ $magic{$lkey}->{$ver}) {
					$guess = $ver;
					last SCAN_LOOP;
				} # if line matches (specific)
			} # for versions on this line
		} # if any versions have entries for this line

		# Finding an any decides
		for my $ver (keys %{$magic{'any'}}) {
			if($line =~ $magic{'any'}->{$ver}) {
				$guess = $ver;
				last SCAN_LOOP;
			} # if line matches (any)
		} # for all versions with 'any' entries

		# Not finding an all eliminates
		for my $ver (keys %{$magic{'all'}}) {
			if($line !~ $magic{'all'}->{$ver}) {
				for my $type (keys %magic) {
					delete $magic{$type}->{$ver};
				} # for all types
			} # if line doesn't match
		} # for all versions with 'all' requirements
	} # SCAN_LOOP

	close FILE;
	return $guess;
}

sub ver { # () - get version {{{2
	return (ref $_[0] ? $_[0]->{'ver'} : undef);
}

sub alias { # () - return saved alias {{{2
	return (ref $_[0] ? $_[0]->{'data'}->{'user'} : "undef");
}

sub convert { # (...) - convert a file {{{2
	my $self = shift;
	return unless ref $self;
	return if defined $self->error;
	my $vers = $self->ver;
	my $ret;

	my @subrs = ("_load_$vers", "_convert_$vers");

	for my $subr (@subrs) {
		if($self->can($subr)) {
			$ret = $self->$subr(@_);
		} else {
			$subr =~ s/^_//; # Hack "_load_v1" -> "load v1"
			$subr =~ s/_/ /g;
			$self->{'err'} = "Can't $subr format";
			return undef;
		}
	}

	return $ret;
}

# generic file support {{{2
sub _invalid { # () - Bad file format, set error {{{3
	$_[0]->{'err'} = "Doesn't look like a $_[0]->{'ver'} file";
	return undef;
}

sub _savefail { # (file) - Failed to save, set error {{{3
	$_[0]->{'err'} = "Can't save $_[1]: " . ($_[2] || "not known why");
	return undef;
}

sub _new_current_file { # (role) - make a current-version file {{{3
	# The keys for which an obvious 1:1 mapping exists will be copied across
	# automatically.
	my ($self, $role) = @_;
	my ($file);

	my %extras; # Ugh.

	if($role eq 'account') {
		%extras = (
			'serva' => ($self->get('host') || SCLJ::Config->val('server')),
			'alias' => $self->get('user'),
			'fast' => 0,
			'pics' => [],
			'journals' => [],
			'frgrps' => {}
		);
		$file = "accounts/" . $extras{'alias'};
	} elsif($role eq 'server') {
		%extras = (
			'host' => SCLJ::Config->val('server'),
			'port' => SCLJ::Config->val('serverport'),
			'path' => SCLJ::Config->val('serverpath'),
			# Mood cache lost on upgrade. Too bad; it's too hard to fix. You have to
			# "login save" anyway to get the pics/journals/frgrps.
			'maxmood' => 0,
			'moods' => {}
		);
		$extras{'alias'} = $extras{'host'};
		$file = "servers/" . $extras{'alias'};
	} elsif($role eq 'rc') {
		$file = "rc";
		%extras = (
			'default_acct' => $self->get('user')
		);
	} else {
		$file = "post_defaults";
		%extras = ();
	}

	$file = SCLJ::File->new($file);

	# Copy the keys we need across, then stomp them if they're in us, too.
	for my $key (keys %extras) {
		$file->set($key, $extras{$key});
	}

	my $mapping = $self->{'_ver_map'}->{$self->ver}->{$role};
	for my $key (keys %{$mapping}) {
		if(defined $mapping->{$key}) { # 1:1 translation
			if(defined $self->get($key)) { # copy only what they defined
				$file->set($mapping->{$key}, $self->get($key));
			}
		} else { # !defined $mapping->{$key}; not 1:1
			my $subr = "_convert_".$self->ver."_key";
			$self->$subr($file, $key);
		}
	}

	return $file;
}

sub _v1compat_pass_trans { # (file) - Convert v1 et. al. passwords {{{3
	my ($self, $file) = @_;

	$paranoid = $self->get('paranoid');
	# Assume clients that didn't use paranoid, aren't.
	$paranoid = 0 unless defined $paranoid;

	if(defined ($pass = $self->get('password'))) {
		$crypt_pass = 0;
	} elsif(defined ($pass = $self->get('hpassword'))) {
		$crypt_pass = 1;
	} else { # It ain't there, so they must be paranoid.
		$paranoid = 1;
	}

	if($paranoid == 0) {
		$file->set('pass', $pass);
		$file->set('crypt_pass', $crypt_pass);
	}

	$file->set('paranoid', $paranoid);

	return $self;
}

sub _flat_config_save { # (...) - Save a flat config space (v1, jlj) {{{3
	my $self = shift;
	my @roles = keys %{$self->{'_ver_map'}->{$self->ver}};

	for my $role (@roles) {
		my $file = $self->_new_current_file($role, @_);
		return $self->_savefail($role, $file->error) unless defined
			$file->save;
	}

	return $self;
}

# v1 file core {{{2
sub _load_v1 { # () - Load a file from the old sclj 2.x format {{{3
	my $self = shift;
	my @tmp;

	if(!open(FILE, '<' . $self->filename)) {
		$self->{'err'} = $!;
		return;
	}

	chomp(@tmp = split(/\s*:\s*/, join(':', <FILE>)));
	close FILE;

	if(($#tmp % 2) != 1) { # Ensure even number of keys to create hash
		return $self->_invalid;
	}

	%{$self->{'data'}} = @tmp;

	if(!exists $self->{'data'}->{'user'}) { # Sanity check
		return $self->_invalid;
	} else {
		return $self;
	}
}

sub _convert_v1 { # (...) - Convert a v1 file to the current {{{3
	my $self = shift;
	return $self->_flat_config_save(@_);
}

sub _convert_v1_key { # (file, key) - Convert a v1 key {{{3
	my $self = shift;
	my $file = shift;
	my $key = shift;

	if($key eq 'password' || $key eq 'hpassword') {
		$self->_v1compat_pass_trans($file);
	} elsif($key eq 'paranoid') {
		my $paranoid = $self->get('paranoid');
		$paranoid = 0 unless defined $paranoid;
		$file->set('paranoid', $paranoid);
	}
}

# jlj file core {{{2
sub _load_jlj { # () - Load a file in JLJ's format {{{3
	# Shamelessly stolen from JLJ 2.5, more or less.
	my $self = shift;
	my ( $line, $key, $node );

	if(!open(FILE, '<' . $self->filename)) {
		$self->{'err'} = $!;
		return;
	}

	foreach $line (<FILE>)
	{
		chomp $line;
		($key, $node) = split ":", $line;
		next if (!defined $key || !defined $node || $key eq "" || $node eq "");
		$key =~ s/\s//g;
		#$node =~ s/\s//g;
		$key = lc $key;
		$self->{'data'}->{$key} = (split " ", $node)[0];
	}
	close FILE;

	if(!exists $self->{'data'}->{'user'}) {
		return $self->_invalid();
	} else {
		return $self;
	}
}

sub _convert_jlj { # (...) - Convert a JLJ file {{{3
	my $self = shift;
	return $self->_flat_config_save(@_);
}

sub _convert_jlj_key { # (file, key) - Convert a single key {{{3
	my ($self, $file, $key) = @_;

	if($key eq 'password') {
		$self->_v1compat_pass_trans($file);
	} elsif($key eq 'proxy') {
		my $proxy = $self->get('proxy');
		return if !defined $proxy || $proxy eq 'no';

		$file->set('proxy_host', $self->get('proxyhost'));
		$file->set('proxy_port', $self->get('proxyport'));
	} elsif($key eq 'backdate') {
		my $backdate = $self->get('backdateentry');
		return unless defined $backdate;

		if($backdate eq 'yes') {
			$file->set('backdate', 1);
		} else {
			$file->set('backdate', 0);
		}
	} elsif($key eq 'allowcomments') {
		my $comments = $self->get('allowcomments');

		if($comments eq 'no') {
			$file->set('comments', 0);
		} else {
			$file->set('comments', 1);
		}
	} elsif($key eq 'format') {
		my %format_map = (
			'preformatted' => 's',
			'none'         => 'h',
			'jerry'        => 'p' # closest match for sclj
		);
		my $fmt = $self->get('format');
		return unless defined $fmt;

		$file->set('format', $format_map{$fmt});
	} elsif($key eq 'security') {
		my %secure_map = (
			'everyone' => 'public',
			'prompt' => 'public'
		);
		my $sec = $self->get('security');
		return unless defined $sec;

		if(exists $secure_map{$sec}) {
			$self->set('security', $secure_map{$sec});
		} else { # friends, private
			$self->set('security', $sec);
		}
	}
}

# Main program {{{1
package main;

SCLJ::Config->parse_opts(\@ARGV);
my @paths = (SCLJ::Config->val('path'),
	SCLJ::Config->path_prepend('accounts'),
	SCLJ::Config->path_prepend('servers')
);
for my $path (@paths) {
	if((!-e $path) && (!mkdir($path, 0700))) {
		print "Can't create required directory $path:\n$!\n";
		exit 1;
	}
}

my ($conversions, $exit);
$conversions = 0;
$exit = 0;
for my $filename (@{SCLJ::Config->val('files')}) {
	if(-e $filename) {
		$conversions++;
		print "Converting $filename...";
		my $conv = SCLJ::Converter->new($filename);
		if($conv && $conv->convert()) {
			print "created account " . $conv->alias . "\n";
		} else {
			print "failed:\n" . ($conv->error || "(no error message)") . "\n";
			$exit++;
		}
	}
}

if($conversions == 0) {
	print "Nothing could be found to convert.\n";
}

exit $exit;

# Vim editor settings {{{1
# vim:ts=2:sw=2:tw=78:ai:si:sm:nocin:noet
# vim600:fdm=marker
