#!perl
#
# memory_map_2300.PL - creates a MemoryMap.pm
#
use strict;
use warnings;

# ExtUtils::MakeMaker invokes us, via make, with the first arg
# pointing to the name of our destination file.
my $mmap = shift || die "Usage: $0 PATH-TO-MEMORY-MAP-PM-FILE\n";
my $tmpfile = "$mmap.tmp.$$";
unlink $tmpfile;
open my $mmap_fh, '>', $tmpfile
    or die "Cannot create $tmpfile: $!\n";

# The _canonical_name() function is defined in the DATA section below.
# We want to eval it, and also need to include it in the .pm source.
my $canonical_name_func = do { local $/; <DATA>; }; close DATA;
eval $canonical_name_func;
die "$0: $@" if $@;

# Read in the memory map...
(my $mapfile = $0) =~ s!^(.*/)?(.*)\.PL$!$2.txt!;
open IN, "<$mapfile"
  or die "Cannot read $mapfile: $!";

my @map;
my %macro;
my $previous_address;
while (my $line = <IN>) {
    chomp $line;
    $line =~ s/\s+$//;			# Remove trailing whitespace

    # E.g. 0019 0   alarm set flags
    if ($line =~ s!^([0-9a-f]{4})\s+[0-9a-f]\s*!!i) {
	my $address = hex($1);

	# This is not expected to trigger: check the sequence
	if (defined $previous_address) {
	    $address == $previous_address+1
	      or die sprintf("$mapfile:$.: Error between %04X and %04X",
			     $previous_address, $address);
	}
	$previous_address = $address;

	# Anything in plain parentheses, at the end, is a comment:
	#    0266 4    | LCD contrast: $BCD+1 (Read Only: ....)
	# strip it off.
	$line =~ s/\s*\([^\(]+\)$//;

	# Is it a definition line?
	if ($line =~ m!^\|\s+([^ 0-9].*?)\s*:\s*(.*)!) {
	    my ($desc, $formula) = ($1, $2);
	    push @map, {
			desc => $desc,
			name => _canonical_name($desc),
			address => $address,
			length => 1,
			   };

	    # FIXME: formula
	    $formula =~ s{<(\S+)>}{
		my $key = $1;
		defined $macro{$key}
		  or die "$mapfile:$.: Undefined macro <$key>";
		$macro{$key};
	    }ge;

	    if ($formula =~ s/\s*\[(.*)\]\s*//) {
		$map[-1]->{units} = $1;
	    }

	    $map[-1]->{formula} = $formula;
	}
	elsif ($line =~ m!^_/!) {
	    my $l = $address - $map[-1]->{address} + 1;
	    if ($l > 10) {
		die "$mapfile:$.: preposterous length";
	    }
	    $map[-1]->{length} = $l;
	}
    }
    elsif ($line =~ /^\s*macro \s+ (\S+) \s+ = \s+ (\S.*\S)/x) {
	$macro{$1} = $2;
    }
    else {
	# FIXME: check for macro definition lines
    }
}
close IN;

#
# ...write out the map file...
#
print { $mmap_fh } <<'END_MMAP_HEADER';
# -*- perl -*-
#
###############################################################################
# This file is autogenerated by $0.  DO NOT EDIT!
###############################################################################
#
package Device::LaCrosse::WS23xx::MemoryMap;

use strict;
use warnings;
use Carp;

my $_memory_map = <<'END_MEMORY_MAP';
END_MMAP_HEADER

for my $entry (@map) {
    my $name = $entry->{name};
    if (my $units = $entry->{units}) {
	$name .= " [$units]";
    }

    printf { $mmap_fh } "%04X:%-2d %-40s %s\n", @{$entry}{"address","length"},
      $name, $entry->{formula};
}

print { $mmap_fh } <<'END_MMAP_REST';
END_MEMORY_MAP

my $Canonical = <<'END_CANONICAL';
Max		Maximum | Maximal
Min		Minimum | Minimal

Indoor		Indoors  | Inside  | In
Outdoor		Outdoors | Outside | Out

Pressure	Press | Air Pressure
Temperature	Temp
Humidity	Hum   | Relative Humidity | Rel Humidity
Windchill	Wind Chill
Wind_Speed	Wind Speed | Windspeed
Dewpoint	Dew Point
Rain		Rainfall  | Rain
END_CANONICAL




sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;

    my $self = {
	fields => {},
    };

    # Read and parse the memory map at top.
    for my $line (split "\n", $_memory_map) {
	next if $line =~ m!^\s*$!;		# Skip blank lines
	next if $line =~ m!^\s*#!;		# Skip comments

	$line =~ /^(\S+):(\S+)\s+(\S+)(\s+\[(.*?)\])?\s+(\S.*\S)/
	  or die "Internal error: Cannot grok '$line'";
	$self->{fields}{lc $3} = {
	    address => hex($1),
	    count   => $2,
	    name  => $3,
	    units => $5 || '?',
	    expr  => $6,
	};
    }

    return bless $self, $class;
}

sub find_field {
    my $self = shift;
    my $field = shift;

    # Canonicalize the requested field name, e.g.
    # 'Indoor Temp Max' => Max_Indoor_Temperature
    my $canonical_field = _canonical_name($field);
    if (! exists $self->{fields}->{lc $canonical_field}) {
	(my $re = lc $field) =~ s/[ _]+/.*/g;
	my @match = grep { /$re/i } keys %{$self->{fields}};
	if (@match == 1) {
	    $canonical_field = $match[0];
	}
    }

    # Get the field info.
    # FIXME: If there's no such field, return undef instead of croaking?
    return $self->{fields}->{lc $canonical_field}
      or croak "No such value, '$field'";
}

END_MMAP_REST

print { $mmap_fh } $canonical_name_func;

print { $mmap_fh } <<'END_MMAP_REST';

=head1 NAME

Device::LaCrosse::WS23xx::MemoryMap - Weather station address meanings

=head1 SYNOPSIS

    use Device::LaCrosse::WS23xx::MemoryMap;

    my $map = Device::LaCrosse::WS23xx::MemoryMap->new();

This is NOT intended as a user-visible module.  It is
used internally by Device::LaCrosse::WS23xx.  This interface
is subject to change without notice.

=head1  DESCRIPTION

=head1  CONSTRUCTOR

=over 4

=item B<new>()

Parses the data table contained in the module itself.

=back

=head1  METHODS

=over 4

=item   B<find_field>( FIELD )

Canonicalizes B<FIELD> and looks it up.  If found, returns a
hashref with the following elements:

=over 8

=item	name

Canonical field name.

=item	units

Units of the measurement.  See Units below.

=item	address

Starting address of this field in the WS-23xx memory map

=item	count

Length, in nybbles, of the field.

=item	expr

Perl expression used to convert data nybbles to a useful form.

=back

If FIELD is not found, returns undef.

=back

=head2	Known Fields

The known data fields -- i.e., what you can use as an
argument to Device::LaCrosse::WS23xx->get() -- are:

END_MMAP_REST

for my $entry (@map) {
    my $name = sprintf("%-40s", $entry->{name});

    if (my $units = $entry->{units}) {
	$name .= $units;
    }
    $name =~ s/\s+$//;

    print  { $mmap_fh } "   ", $name, "\n";
}


print { $mmap_fh } <<'END_MMAP_FINAL';

Where applicable, units are displayed to the right of each field.

=head2	Units

The WS-23xx devices return data in the following units:

=over 8

=item	B<C>

Degrees Centigrade (temperature)

=item	B<%>

Percent (humidity)

=item	B<hPa>

hectoPascals (pressure)

=item	B<m/s>

Meters per Second (wind speed)

=item	B<mm>

Millimeters (rainfall)

=item	B<degrees>

Compass degrees, 0-359, (wind direction)

=item	B<minutes>

Minutes.

=item	B<seconds>

Seconds.

=item	B<time_t>

Seconds since the Epoch; you probably want to use it as
an argument to B<localtime>().

=back


=head1  AUTHOR

Ed Santiago <esm@cpan.org>

=head1	SEE ALSO

L<Device::LaCrosse::WS23xx>

=cut


1;
END_MMAP_FINAL

close $mmap_fh
    or die "$0: Error writing $mmap: $!\n";
chmod 0444 => $mmap;
rename $tmpfile => $mmap
    or die "$0: could not rename $tmpfile: $!\n";

exit 0;


__DATA__

################
#
# canonical_name
#
sub _canonical_name {
    my $desc = shift;
    my $canonical_name = '';

    $desc =~ s/_/ /g;

    # Min or Max?
    if ($desc =~ s/\bmin(imum)?\b/ /i) {
	$canonical_name .= 'Min_';
    }
    elsif ($desc =~ s/\bmax(imum)?\b/ /i) {
	$canonical_name .= 'Max_';
    }
    elsif ($desc =~ s/\b(High|Low)\s*Alarm\b/ /i) {
	$canonical_name .= ucfirst(lc($1)) . '_Alarm_';
    }
    elsif ($desc =~ s/\bCurrent\b/ /i) {
	# do nothing
    }

    # Where?
    if ($desc =~ s/\b(in|out)(doors?)?(\b|$)/ /i) {
	$canonical_name .= ucfirst(lc($1) . 'door') . '_';
    }

    # What: Temperature, Windchill, Pressure, ...
    if ($desc =~ s/\btemp(erature)?\b/ /i) {
	$canonical_name .= 'Temperature';
    }
    elsif ($desc =~ s/\bPress(ure)?\b/ /i) {
	$desc =~ s/\bair\b/ /i;

	if ($desc =~ s/\bAbs(olute)?\b/ /i) {
	    $canonical_name .= 'Absolute_';
	}
	elsif ($desc =~ s/\bRel(ative)?\b/ /i) {
	    $canonical_name .= 'Relative_';
	}
	$canonical_name .= 'Pressure';
	if ($desc =~ s/\bCorrection\b/ /i) {
	    $canonical_name .= '_Correction';
	}
    }
    elsif ($desc =~ s/\b(Humidity|Windchill|Dewpoint)\b/ /i) {
	$canonical_name .= ucfirst(lc($1));
	$desc =~ s/\bRel(ative)?\b/ /i;
    }
    elsif ($desc =~ s/\b(Rain)\b//i) {
	$canonical_name .= "Rain";
	if ($desc =~ s/\b(1|24)(\s*h(ou)?r?)?\b//i) {
	    $canonical_name .= "_$1hour";
	}
	elsif ($desc =~ s/\btotal\b//i) {
	    $canonical_name .= "_Total";
	}
    }
    else {
	(my $tmp = $desc) =~ s/\s+/_/g;
	$canonical_name .= $tmp;
	# FIXME: warn?
    }

    # Is this a date/time field?
    if ($desc =~ s!\bDate/Time\b! !i) {
	$canonical_name .= '_datetime';
    }

    if ($desc =~ /\S/) {
#	warn "leftover: $desc\n";
    }

    $canonical_name =~ s/_$//;

    return $canonical_name;
}

# END   canonical_name
