#!/usr/bin/perl
# SPDX-License-Identifier: GPL-2.0-only

use strict;
use warnings;

use Getopt::Long;
use JSON::MaybeXS;
use Encode;

my %Options;

my $Translations = {};

my $rem2html_version = '06.02.05';

my($days, $shades, $moons, $classes, $Month, $Year, $Numdays, $Firstwkday, $Mondayfirst, $weeks,
    @Daynames, $Nextmon, $Nextlen, $Prevmon, $Prevlen);

my $TIDY_PROGNAME = $0;
$TIDY_PROGNAME =~ s|^.*/||;

# rem2html -- convert the output of "remind -pp" or "remind -ppp" to HTML

=head1 NAME

rem2html - Convert the output of "remind -pp" or "remind -ppp" to HTML

=head1 SYNOPSIS

    remind -ppp [remind_options] file | rem2html [options]

    remind -pp [remind_options] file | rem2html [options]

You can also use the old interchange format as below, but the -pp
or -ppp versions are preferred.

    remind -p [remind_options] file | rem2html [options]

=head1 OPTIONS

=over 4

=item --help, -h

Print usage information

=item --version

Print version

=item --utf8

Assume standard input is encoded in UTF-8; write UTF-8 data to standard
output.

=item --backurl I<url>

When producing the small calendar for the previous month, make the
month name a link to I<url>.

=item --forwurl I<url>

When producing the small calendar for the next month, make the
month name a link to I<url>.

=item --imgbase I<url>

When creating URLs for the stylesheet or external images, use I<url>
as the base URL.

=item --pngs

Normally, rem2html uses inline "data:" URLs for the moon phase images,
yielding a standalone HTML file.  The C<--pngs> option makes it use
external images named firstquarter.png, fullmoon.png, lastquarter.png
and newmoon.png, which are expected to live in C<--imgbase>.

=item --stylesheet I<url.css>

Use I<url.css> as the stylesheet.  If this option is used,
I<url.css> is interpreted relative to B<imgbase> I<unless> it starts
with a "/".

=item --nostyle

Produce basic HTML that does not use a CSS stylesheet.

=item --tableonly

Output results as a E<lt>tableE<gt> ... E<lt>/tableE<gt> sequence only
without any E<lt>htmlE<gt> or E<lt>bodyE<gt> tags.

=item --title I<title>

Use I<title> as the content between E<lt>titleE<gt> and E<lt>/titleE<gt>
tags.

=item --yaag

Instead of making 7-column tables for each month, show each month
as a I<single> table row of 38 columns.  This is the so-called
"year-at-a-glance" format.  You must use the "-ppp" option to
C<remind> or the --yaag option will be ignored.  And this format
works best if you create a calendar for an entire year (so, starting
in January and using "-ppp12" to get 12 months' worth of reminders.)

=item --prologue I<html_text>

Insert I<html_text> right after the E<lt>bodyE<gt> tag.

=item --epilogue I<html_text>

Insert I<html_text> right before the E<lt>/bodyE<gt> tag.

=back

=head1 SPECIALS SUPPORTED

The rem2html back-end supports the following SPECIAL reminders:

=over

=item HTML

Add an HTML reminder to the calendar.  All HTML tags are available.

=item HTMLCLASS

Add a CSS class to the box representing the trigger date.  See
"HIGHLIGHTING TODAY" for an example

=item WEEK, MOON, SHADE, COLOR

The standard SPECIALs supported by all back-ends

=back

=head1 INFO STRINGS SUPPORTED

Rem2html supports one INFO string, namely C<Url>.  Its value
should be a URL.  If the C<Url> INFO string is supplied for
a normal reminder, a COLOR special, a MOON special or a WEEK
special, the corresponding output is turned into a hyperlink.

=head1 HIGHLIGHTING TODAY

Older versions of rem2html used to highlight today's date with a red outline.
The current version does not do that by default.  If you wish to highlight
today's date, add the following reminder to your reminders file:

    REM [realtoday()] SPECIAL HTMLCLASS rem-today

=head1 AUTHOR

rem2html was written by Dianne Skoll with much inspiration from an
earlier version by Don Schwarz.

=head1 HOME PAGE

L<https://dianne.skoll.ca/projects/remind/>

=head1 SEE ALSO

B<remind>, B<rem2ps>, B<rem2pdf>, B<tkremind>
=cut

sub usage
{
    my ($exit_status) = @_;
    if (!defined($exit_status)) {
	    $exit_status = 1;
    }
    print STDERR <<"EOM";
$TIDY_PROGNAME: Produce an HTML calendar from the output of "remind -pp[p]"

Usage: remind -pp [remind_options] file | rem2html [options]
   or: remind -ppp [remind_options] file | rem2html [options]

Options:

--help, -h            Print usage information
--utf8                Assume UTF-8 input and write UTF-8 output
--man                 Show man page (requires "perldoc")
--version             Print version
--backurl url         Make the title on the previous month's small calendar
                      entry a link to <url>
--forwurl url         Same as --backurl, but for the next month's small calendar
--imgbase url         Base URL of images and default stylesheet file
--pngs                Use external .PNG images for moon phases rater than
                      inline data: URLs
--stylesheet url.css  URL of CSS stylesheet.  If specified, imgbase is NOT
                      prepended to url.css
--nostyle             Produce basic HTML that does not use a CSS stylesheet
--tableonly           Output results as a <table> only, no <html>, <body>, etc.
--title string        What to put in <title>...</title> tags
--prologue html_text  Text to insert at the top of the body
--epilogue html_text  Text to insert at the end of the body
--yaag                Output one month per row for year-at-a-glance format
EOM
    exit($exit_status);
}

sub smoosh
{
        my ($first, $second) = @_;
        return $second unless defined ($first);
        return $second if $first eq '';
        return $second if ($second =~ m|^/|);  # Absolute path given for second

        # Squash multiple slashes
        $first =~ s|/+|/|g;

        # Special case
        return "/$second" if ($first eq '/');

        # Delete trailing slash
        $first =~ s|/$||;

        return "$first/$second";
}

sub parse_options
{
    local $SIG{__WARN__} = sub { print STDERR "$TIDY_PROGNAME: $_[0]\n"; };
    if (!GetOptions(\%Options, "help|h",
		    "man",
                    "utf8",
                    "pngs",
                    "yaag",
		    "version",
		    "stylesheet=s",
		    "nostyle",
		    "backurl=s",
		    "forwurl=s",
		    "title=s",
		    "prologue=s",
		    "epilogue=s",
		    "imgbase=s",
		    "tableonly")) {
	usage(1);
    }
    $Options{title} ||= 'HTML Calendar';

    my $stylesheet = $Options{stylesheet};
    if ($stylesheet) {
            $Options{stylesheet} = smoosh($Options{imgbase}, $stylesheet);
    }
    if ($Options{utf8}) {
            binmode(STDIN, ':encoding(UTF-8)');
            binmode(STDOUT, ':encoding(UTF-8)');
    }
}

sub start_output
{
    return if ($Options{tableonly});

    print("<html>\n<head>\n");
    if ($Options{utf8}) {
            print '<meta charset="UTF-8">' . "\n";
    }

    print("<title>" . $Options{title} . "</title>\n");
    if (!$Options{nostyle}) {
	if ($Options{stylesheet}) {
                print('<link rel="stylesheet" type="text/css" href="' .
                      $Options{stylesheet} . '">' . "\n");
	} else {
                print("<style>\n");
                print default_stylesheet();
                print("</style>\n");
        }
    }
    print("</head>\n<body>\n");
    if ($Options{prologue}) {
	print $Options{prologue} . "\n";
    }
}

sub end_output
{
    return if ($Options{tableonly});
    if ($Options{epilogue}) {
	print $Options{epilogue} . "\n";
    }
    print("</body>\n</html>\n");
}

sub slurp_translations
{
    my $line;

    $line = <STDIN>;
    chomp $line;
    eval {
        if ($Options{utf8}) {
            $Translations = decode_json(encode('UTF-8', $line, Encode::FB_DEFAULT));
        } else {
            $Translations = decode_json($line);
        }
    };
    if ($@) {
        $Translations = {};
    }
}

sub t
{
    my ($str) = @_;
    return $Translations->{$str} if exists($Translations->{$str});
    return $str;
}

sub parse_input
{
    undef $days;
    undef $shades;
    undef $moons;
    undef $classes;
    undef $weeks;

    my $found_data = 0;
    while(<STDIN>) {
        chomp;
        if ($_ eq '[') {
            return parse_input_ppp();
        }
        if ($Options{yaag}) {
            $Options{yaag} = 0;
            print STDERR "Ignoring option --yaag: You must use remind -ppp for this option\n";
        }
        if (/# translations/) {
            slurp_translations();
            next;
        }
        last if /^\# rem2ps2? begin$/;
    }

    my $line;
    # Month Year numdays firstday monday_first_flag
    $line = <STDIN>;
    return 0 unless $line;
    chomp($line);
    ($Month, $Year, $Numdays, $Firstwkday, $Mondayfirst) = split(' ', $line);

    $Month =~ s/_/ /g;
    # Day names
    $line = <STDIN>;
    return 0 unless $line;
    chomp($line);
    @Daynames = split(' ', $line);

    for (my $i=0; $i<7; $i++) {
            $Daynames[$i] =~ s/_/ /g;
    }

    # Prevmon prevlen
    $line = <STDIN>;
    return 0 unless $line;
    chomp($line);
    ($Prevmon, $Prevlen) = split(' ', $line);
    $Prevmon =~ s/_/ /g;

    # Nextmon nextlen
    $line = <STDIN>;
    return 0 unless $line;
    chomp($line);
    ($Nextmon, $Nextlen) = split(' ', $line);
    $Nextmon =~ s/_/ /g;

    $found_data = 1;
    my $class;
    if ($Options{nostyle}) {
	$class = '';
    } else {
	$class = ' class="rem-entry"';
    }
    while(<STDIN>) {
	chomp;
	last if /^\# rem2ps2? end$/;
	next if /^\#/;
	my ($y, $m, $d, $special, $tag, $duration, $time, $body, $title);
        my $url_pre = '';
        my $url_post = '';
        my $url;
	if (m/^(\d*).(\d*).(\d*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*)$/) {
		($y, $m, $d, $special, $tag, $duration, $time, $body) =
		    ($1, $2, $3, $4, $5, $6, $7, $8);
                $title = '';
	} elsif (/\{/) {
		my $obj;
                if ($Options{utf8}) {
                        $obj = decode_json(encode('UTF-8', $_, Encode::FB_DEFAULT));
                } else {
                        $obj = decode_json($_);
                }
		next unless ($obj->{date} =~ /^(\d+)-(\d+)-(\d+)$/);
                if ($obj->{info} && $obj->{info}->{url}) {
                        $url = $obj->{info}->{url};
                        $url_pre = "<a href=\"$url\">";
                        $url_post = "</a>";
                }
		$y = $1;
		$m = $2;
		$d = $3;
		$special = $obj->{passthru} || '*';
		$tag = $obj->{tags} || '*';
		$duration = $obj->{duration} || '*';
		$time = $obj->{time} || '*';
		$body = $obj->{body};
                $title = info_to_title($obj->{info});
	} else {
		next;
	}
	my $d1 = $d;
	$d1 =~ s/^0+//;
	$special = uc($special);
	if ($special eq 'HTML') {
	    push(@{$days->[$d]}, $body);
	} elsif ($special eq 'HTMLCLASS') {
	    $classes->[$d] = $body;
	} elsif ($special eq 'WEEK') {
		$body =~ s/^\s+//;
		$body =~ s/\s+$//;
		$weeks->{$d1}->{body} = $body;
                $weeks->{$d1}->{url} = $url;
	} elsif ($special eq 'MOON') {
		if ($body =~ /(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/) {
			my ($phase, $moonsize, $fontsize, $msg) = ($1, $2, $3, $4);
			$moons->[$d]->{'phase'} = $phase;
			$moons->[$d]->{'msg'} = $msg;
		} elsif ($body =~ /(\S+)/) {
			$moons->[$d]->{'phase'} = $1;
			$moons->[$d]->{'msg'} = '';
		}
                $moons->[$d]->{url} = $url;
	} elsif ($special eq 'SHADE') {
		if ($body =~ /(\d+)\s+(\d+)\s+(\d+)/) {
			$shades->[$d] = sprintf("#%02X%02X%02X",
						($1 % 256), ($2 % 256), ($3 % 256));
		} elsif ($body =~ /(\d+)/) {
			$shades->[$d] = sprintf("#%02X%02X%02X",
						($1 % 256), ($1 % 256), ($1 % 256));
		}
	} elsif ($special eq 'COLOR' || $special eq 'COLOUR') {
		if ($body =~ /(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/s) {
			my($r, $g, $b, $text) = ($1, $2, $3, $4);
			my $color = sprintf("style=\"color: #%02X%02X%02X;\"",
					    $r % 256, $g % 256, $b % 256);
			push(@{$days->[$d]}, "<p$class$title $color>" . $url_pre . fix_whitespace(escape_html($text)) . $url_post . '</p>');
		}
	} elsif ($special eq '*') {
	    push(@{$days->[$d]}, "<p$class$title>" . $url_pre . fix_whitespace(escape_html($body)) . $url_post . '</p>');
	}
    }
    return $found_data;
}

sub parse_input_ppp
{
    my $json = "[\n";
    my $curlies = 0;
    my $did_a_calendar = 0;
    while(<STDIN>) {
        $json .= $_;
        $curlies++ if ($_ eq "{\n");
        $curlies-- if ($_ eq "}\n");
        $curlies-- if ($_ eq "},\n");
        if ($_ eq "]\n" && !$curlies) {
            my $array;
            eval {
                if ($Options{utf8}) {
                    $array = decode_json(encode('UTF-8', $json, Encode::FB_DEFAULT));
                } else {
                    $array = decode_json($json);
                }
            };
            if (!$array) {
                print STDERR "Could not decode JSON.\n";
                exit(1);
            }
            if (!$did_a_calendar) {
                start_output();
                $did_a_calendar = 1;
            }
            if (exists($array->[0]{caltype}) &&
                $array->[0]{caltype} eq 'weekly') {
                if ($Options{yaag}) {
                    $Options{yaag} = 0;
                    print STDERR "Ignoring option --yaag for a weekly calendar\n";
                }
                emit_ppp_calendars($array, 'weekly');
            } else {
                emit_ppp_calendars($array, 'monthly');
            }
            $json = '';
        }
    }
    if (!$did_a_calendar) {
        print STDERR "$TIDY_PROGNAME: Could not find any calendar data on STDIN.\n";
        exit(1);
    } else {
        end_output();
        exit(0);
    }
}

sub print_yaag_weekday_row
{
    my ($cal) = @_;
    print "<tr class=\"rem-yaag-weekday-row\">";
    print "<th class=\"rem-yaag-empty-header\">&nbsp;</th>";
    my $ex;
    for (my $i=0; $i<37; $i++) {
        my $day;
        if ($cal->{mondayfirst}) {
            $day = $cal->{daynames}->[($i+1) % 7];
        } else {
            $day = $cal->{daynames}->[$i % 7];
        }
        if (($i % 7) == 6) {
                $ex = " rem-yaag-end-of-week";
        } else {
                $ex = "";
        }
        print "<th class=\"rem-yaag-weekday-header$ex\">$day</th>";
    }
    print "</tr>\n";
}

sub emit_ppp_calendars
{
    my ($array, $type) = @_;
    my $prev_year = 0;
    my $done_one = 0;
    foreach my $cal (@$array) {
        if ($Options{yaag}) {
            if ($cal->{year} != $prev_year) {
                $prev_year = $cal->{year};
                if ($done_one) {
                    print "</table>\n";
                }
                print "<span class=\"year_heading\">$prev_year</span>\n";
                print "<table class=\"rem-yaag\">\n";
                print_yaag_weekday_row($cal);
            }
        }
        emit_one_ppp_calendar($cal, $type);
        $done_one = 1;
    }
    if ($done_one) {
        print "</table>\n";
    }
}

sub info_to_title
{
        my ($info) = @_;
        if (!$info) {
                return '';
        }
        my $done_one = 0;
        my $title = '';
        foreach my $key ('location', 'description') {
                next unless $info->{$key};
                if ($done_one) {
                        $title .= "&#010";
                }
                $done_one = 1;
                my $val = escape_html($info->{$key});
                $val =~ s/\n/&#010/g;
                $val =~ s/"/&#034;/g;
                $val =~ s/</&lt;/g;
                $title .= ucfirst($key) . ': ' . $val;
        }
        return " title=\"$title\"";
}

sub emit_one_ppp_calendar
{
    my ($c, $type) = @_;

    undef $days;
    undef $shades;
    undef $moons;
    undef $classes;
    undef $weeks;

    my $dates_to_day_index;
    my $cols_to_date_info;
    if (exists($c->{translations})) {
        $Translations = $c->{translations};
    }
    if ($type eq 'monthly') {
        $Month = $c->{monthname};
        $Year = $c->{year};
        $Numdays = $c->{daysinmonth};
        $Firstwkday = $c->{firstwkday};
        $Mondayfirst = $c->{mondayfirst};
        @Daynames = @{$c->{daynames}};
        $Prevmon = $c->{prevmonthname};
        $Prevlen = $c->{daysinprevmonth};
        $Nextmon = $c->{nextmonthname};
        $Nextlen = $c->{daysinnextmonth};
    } else {
        my $idx = 0;
        $Numdays = 7;
        foreach my $date (@{$c->{dates}}) {
            $Daynames[$idx] = $date->{dayname};
            $dates_to_day_index->{$date->{date}} = $idx;
            $cols_to_date_info->[$idx] = $date;
            $idx++;
        }
    }

    my $class;
    if ($Options{nostyle}) {
	$class = '';
    } else {
	$class = ' class="rem-entry"';
    }
    foreach my $obj (@{$c->{entries}}) {
        next unless ($obj->{date} =~ /^(\d+)-(\d+)-(\d+)$/);
        my $y = $1;
        my $m = $2;
        my $d = $3;
        my $col;
        if ($type eq 'weekly') {
            $col = $dates_to_day_index->{$obj->{date}};
        } else {
            $col = $d;
            $col =~ s/^0+//;
        }
        my $special = $obj->{passthru} || '*';
        my $tag = $obj->{tags} || '*';
        my $duration = $obj->{duration} || '*';
        my $time = $obj->{time} || '*';
        my $body = $obj->{body};
        my $title = info_to_title($obj->{info});
        my $url_pre = '';
        my $url_post = '';
        my $url = '';
        if ($obj->{info} && $obj->{info}->{url}) {
                $url = $obj->{info}->{url};
                $url_pre = "<a href=\"$url\">";
                $url_post = "</a>";
        }
	$special = uc($special);
	if ($special eq 'HTML') {
	    push(@{$days->[$col]}, $body);
	} elsif ($special eq 'HTMLCLASS') {
	    $classes->[$col] = $body;
	} elsif ($special eq 'WEEK') {
		$body =~ s/^\s+//;
		$body =~ s/\s+$//;
		$weeks->{$col}->{body} = $body;
		$weeks->{$col}->{url} = $url;
	} elsif ($special eq 'MOON') {
		if ($body =~ /(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/) {
			my ($phase, $moonsize, $fontsize, $msg) = ($1, $2, $3, $4);
			$moons->[$col]->{'phase'} = $phase;
			$moons->[$col]->{'msg'} = $msg;
		} elsif ($body =~ /(\S+)/) {
			$moons->[$col]->{'phase'} = $1;
			$moons->[$col]->{'msg'} = '';
		}
                $moons->[$col]->{url} = $url;
	} elsif ($special eq 'SHADE') {
		if ($body =~ /(\d+)\s+(\d+)\s+(\d+)/) {
			$shades->[$col] = sprintf("#%02X%02X%02X",
						($1 % 256), ($2 % 256), ($3 % 256));
		} elsif ($body =~ /(\d+)/) {
			$shades->[$col] = sprintf("#%02X%02X%02X",
						($1 % 256), ($1 % 256), ($1 % 256));
		}
	} elsif ($special eq 'COLOR' || $special eq 'COLOUR') {
		if ($body =~ /(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/s) {
			my($r, $g, $b, $text) = ($1, $2, $3, $4);
			my $color = sprintf("style=\"color: #%02X%02X%02X;\"",
					    $r % 256, $g % 256, $b % 256);
			push(@{$days->[$col]}, "<p$class$title $color>" . $url_pre . fix_whitespace(escape_html($text)) . $url_post . '</p>');
		}
	} elsif ($special eq '*') {
	    push(@{$days->[$col]}, "<p$class$title>" . $url_pre . fix_whitespace(escape_html($body)) . $url_post . '</p>');
	}
    }
    output_calendar($type, $cols_to_date_info);
}

sub fix_whitespace
{
        my ($text) = @_;

        # Collapse multiple spaces/tabs to a single space
        $text =~ s/[ \t]+/ /gs;

        # Remove whitespace before/after newlines
        $text =~ s/\s+\n/\n/gs;
        $text =~ s/\n\s+/\n/gs;
        # Collapse multiple newlines to a single newline
        $text =~ s/\n+/\n/gs;

        # Convert newlines to <br />
        $text =~ s|\n|<br />|g;
        return $text;
}

sub small_calendar
{
	my($month, $monlen, $url, $first_col) = @_;
	if ($Mondayfirst) {
		$first_col--;
		if ($first_col < 0) {
			$first_col = 6;
		}
	}

	if ($Options{nostyle}) {
	    print "<td width=\"14%\">\n";
	    print "<table border=\"0\">\n";
	    print "<caption>";
	} else {
	    print "<td class=\"rem-small-calendar\">\n";
	    print "<table class=\"rem-sc-table\">\n";
	    print "<caption class=\"rem-sc-caption\">";
	}
	print "<a href=\"$url\">" if ($url);
	print $month;
	print "</a>" if ($url);
	print "</caption>\n";

	my $class;
	if ($Options{nostyle}) {
	    print '<tr>';
	    $class = ' align="right"';
	} else {
	    print '<tr class="rem-sc-hdr-row">';
	    $class = ' class="rem-sc-hdr"';
	}
	if (!$Mondayfirst) {
		print "<th$class>" . substr($Daynames[0], 0, 1) . '</th>';
	}
	for (my $i=1; $i<7; $i++) {
		print "<th$class>" . substr($Daynames[$i], 0, 1) . '</th>';
	}
	if ($Mondayfirst) {
		print "<th$class>" . substr($Daynames[0], 0, 1) . '</th>';
	}
	print("</tr>\n");
	my $col = 0;
	for (; $col<$first_col; $col++) {
		if ($col == 0) {
			print("<tr>\n");
		}
		if ($Options{nostyle}) {
		    print("<td align=\"right\" width=\"14%\">&nbsp;</td>");
		} else {
		    print("<td class=\"rem-sc-empty-cell\">&nbsp;</td>");
		}
	}

	for (my $day=1; $day <= $monlen; $day++) {
		if ($col == 0) {
			print("<tr>\n");
		}
		$col++;
		if ($Options{nostyle}) {
		    print("<td align=\"right\" width=\"14%\">$day</td>");
		} else {
		    print("<td class=\"rem-sc-cell\">$day</td>");
		}
		if ($col == 7) {
			print("</tr>\n");
			$col = 0;
		}
	}
	if ($col) {
	    while ($col < 7) {
		if ($Options{nostyle}) {
		    print("<td align=\"right\" width=\"14%\">&nbsp;</td>");
		} else {
		    print("<td class=\"rem-sc-empty-cell\">&nbsp;</td>");
		}
		$col++;
	    }
	    print("</tr>\n");
	}
	print("</table>\n");
	print "</td>\n";
}

sub output_calendar
{
    # Which column is 1st of month in?
    my ($type, $date_info) = @_;

    my ($first_col, $last_col, $number_of_rows);

    if ($type eq 'monthly') {
        $first_col = $Firstwkday;
        if ($Mondayfirst) {
            $first_col--;
            if ($first_col < 0) {
                $first_col = 6;
            }
        }

        # Last column
        $last_col = ($first_col + $Numdays - 1) % 7;

        # Figure out how many rows
        $number_of_rows = int(($first_col + $Numdays ) / 7 + 0.999);

        # Add a row for small calendars if necessary
        if ($first_col == 0 && $last_col == 6) {
            $number_of_rows++;
        }
    } else {
        $first_col = 0;
        $last_col = 6;
        $number_of_rows = 1;
    }

    # Start the table
    my $class;
    if (!$Options{yaag}) {
        if ($Options{nostyle}) {
            print '<table width="100%" border="1" cellspacing="0">';
            if ($type eq 'monthly') {
                print "<caption>$Month $Year </caption>";
            }
            print "\n<tr>";
            $class = ' width="14%"';
        } else {
            if ($type eq 'monthly') {
                print '<table class="rem-cal"><caption class="rem-cal-caption">' .
                    $Month . ' ' . $Year . '</caption>' . "\n";
            } else {
                print '<table class="rem-cal">' . "\n";
            }
            print '<tr class="rem-cal-hdr-row">';
            $class = ' class="rem-cal-hdr"';
        }
        if ($type eq 'monthly') {
            if (!$Mondayfirst) {
                print "<th$class>" . $Daynames[0] . '</th>';
            }
            for (my $i=1; $i<7; $i++) {
                print "<th$class>" . $Daynames[$i] . '</th>';
            }
            if ($Mondayfirst) {
                print "<th$class>" . $Daynames[0] . '</th>';
            }
        } else {
            for (my $i=0; $i<7; $i++) {
                my $inf = $date_info->[$i];
                print "<th$class>" . $inf->{dayname} . "<br>" .
                    $inf->{day} . '&nbsp;' .
                    $inf->{month} . '&nbsp;' .
                    $inf->{year} . '</th>';
            }
        }

        print "</tr>\n";
    } else {
        # For year-at-a-glance calendar, it's just a row
        print '<tr class="rem-yaag-cal-row">';
        print '<th class="rem-yaag-cal-hdr">' . $Month . "</th>\n";

        my $cells_drawn = 0;
        # Print the blank ones
        for (my $i=0; $i<$first_col; $i++) {
            print "<td class=\"rem-yaag-cal-blank\">&nbsp;</td>";
            $cells_drawn++;
        }

        # All the entries
        for (my $day=1; $day <= $Numdays; $day++) {
            if (($cells_drawn % 7) == 6) {
                draw_day_cell($day, 1, $type, ' rem-yaag-end-of-week');
            } else {
                draw_day_cell($day, 1, $type);
            }
            $cells_drawn++;
        }

        # And fill in the remaining ones
        while ($cells_drawn < 37) {
            if (($cells_drawn % 7) == 6) {
                print "<td class=\"rem-yaag-cal-blank rem-yaag-end-of-week\">&nbsp;</td>";
            } else {
                print "<td class=\"rem-yaag-cal-blank\">&nbsp;</td>";
            }
            $cells_drawn++;
        }
        print "</tr>\n";
        return;
    }
    # Start the calendar rows
    my $col = 0;
    if ($Options{nostyle}) {
	print "<tr>\n";
    } else {
	print "<tr class=\"rem-cal-row rem-cal-row-$number_of_rows-rows\">\n";
    }
    if ($type eq 'monthly') {
        if ($first_col > 0) {
            small_calendar($Prevmon, $Prevlen, $Options{backurl},
                           ($Firstwkday - $Prevlen + 35) % 7);
            $col++;
        }

        if ($last_col == 6 && $first_col > 0) {
            small_calendar($Nextmon, $Nextlen, $Options{forwurl},
                           ($Firstwkday + $Numdays) % 7);
            $col++;
        }
    }
    if ($Options{nostyle}) {
	$class = ' width="14%"';
    } else {
	$class = ' class="rem-empty rem-empty-$number_of_rows-rows"';
    }
    while ($col < $first_col) {
	print("<td$class>&nbsp;</td>\n");
	$col++;
    }

    for (my $day=1; $day<=$Numdays; $day++) {
        draw_day_cell($day, $number_of_rows, $type);
	$col++;
	if ($col == 7) {
	    $col = 0;
	    print "</tr>\n";
	    if ($day < $Numdays) {
		if ($Options{nostyle}) {
		    print "<tr>\n";
		} else {
                    print "<tr class=\"rem-cal-row rem-cal-row-$number_of_rows-rows\">\n";
		}
	    }
	}
    }

    if ($type eq 'monthly') {
        if ($col) {
            while ($col < 7) {
                if ($col == 5) {
                    if ($first_col == 0) {
                        small_calendar($Prevmon, $Prevlen, $Options{backurl},
                                       ($Firstwkday - $Prevlen + 35) % 7);
                    } else {
                        print("<td$class>&nbsp;</td>\n");
                    }
                } elsif ($col == 6) {
                    small_calendar($Nextmon, $Nextlen, $Options{forwurl},
                                   ($Firstwkday + $Numdays) % 7);
                } else {
                    print("<td$class>&nbsp;</td>\n");
                }
                $col++;
            }
            print "</tr>\n";
        }

        # Add a row for small calendars if they were not yet done!
        if ($first_col == 0 && $last_col == 6) {
	    if ($Options{nostyle}) {
                print "<tr>\n";
	    } else {
                print "<tr class=\"rem-cal-row rem-cal-row-$number_of_rows-rows\">\n";
	    }
	    small_calendar($Prevmon, $Prevlen, $Options{backurl},
			   ($Firstwkday - $Prevlen + 35) % 7);
	    for (my $i=0; $i<5; $i++) {
		print("<td$class>&nbsp;</td>\n");
	    }
	    small_calendar($Nextmon, $Nextlen, $Options{forwurl},
			   ($Firstwkday + $Numdays) % 7);
	    print("</tr>\n");
        }
    }
    # End the table
    print "</table>\n";
    if ($type eq 'weekly') {
        print "&nbsp;<br />\n";
    }
}

sub draw_day_cell
{
    my($day, $number_of_rows, $type, $extra_class) = @_;
    my $shade = $shades->[$day];
    my $week = '';
    $extra_class = '' unless defined($extra_class);
    if (exists($weeks->{$day})) {
            if ($weeks->{$day}->{url}) {
                    $week = ' <a href="' . $weeks->{$day}->{url} . '">' . escape_html($weeks->{$day}->{body}) . '</a>';
            } else {
                    $week = ' ' . escape_html($weeks->{$day}->{body});
            }
    }
    my $class = $classes->[$day] || '';
    if (!$Options{nostyle}) {
        $class .= ' ' if $class ne '';
        if ($Options{yaag}) {
            $class .= "rem-yaag-cell"
        } else {
            $class .= "rem-cell rem-cell-$number_of_rows-rows";
        }
    }
    if ($shade) {
	    $shade = " style=\"background: $shade;\"";
    } else {
	    $shade = "";
    }
    if ($class ne '' || $extra_class ne '') {
	print "<td class=\"$class$extra_class\"$shade>\n";
    } else {
	print "<td valign=\"top\" $shade>\n";
    }
    if ($moons->[$day]) {
	    my $phase = $moons->[$day]->{'phase'};
	    my $msg = $moons->[$day]->{'msg'};
	    $msg ||= '';
	    if ($msg ne '') {
		    $msg = '&nbsp;' . escape_html($msg);
	    }
	    my $img;
	    my $alt;
	    my $title;
	    if ($phase == 0) {
                    if ($Options{pngs}) {
                            $img = smoosh($Options{imgbase}, 'newmoon.png');
                    } else {
                            $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAC6SURBVDiNpdNNbsIwFATgL0HKolchHKBX6yFaBOEyoPYUabvOIVKJRaCL2JX5TRNGGvnJ8ozGz89cYoElPvET+BX2yivn/1Bggw5HHMKa1h2qcPZC/JEIhvh+brIZIY6sorhMYo9hh3KGFzzfa84NZNjDt9OG/ZcH1BlaPE1IAG0+URhxzNGESKPFaHJs9Q0Ziww7HnvGeXSrJhis0jiFfjwnj3I0WRv+TKtr4hQl3lDrZ6QN9Wt654hfWfGDmBpUwDkAAAAASUVORK5CYII=';
                    }
		    $title = escape_html(t('New Moon'));
		    $alt = 'new';
	    } elsif ($phase == 1) {
                    if ($Options{pngs}) {
                            $img = smoosh($Options{imgbase}, 'firstquarter.png');
                    } else {
                            $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADfSURBVDiNndM9TsNAFATgzy5yjZSAE85JBygETgENUPF3iBCitHAFQkcIhZ/Ryn9gRlrZmp2Z3ef3TBOHOMULPrDBMrhpi/4HI5xjix2+4nmJRbx/Yh7ahvkpRPVV4QDXwT3UQy46zGkAZDgK/iytefvHgCrkJsqZUH6cLnNbABSxd5Jhhf1IbkMXv8Qux7hH1Ic1xvk/jBWy6gavumvtwx7ectwZXkKh7MA95XgObeOtpI2U4zl0kGbpxgiPvwQUcXLrKFchc82f6Ur0PK49azOnmOI4TBu84zm4SV38DeIVYkrYJyNbAAAAAElFTkSuQmCC';
                    }
		    $title = escape_html(t('First Quarter'));
		    $alt = '1st';
	    } elsif ($phase == 2) {
                    if ($Options{pngs}) {
                            $img = smoosh($Options{imgbase}, 'fullmoon.png');
                    } else {
                            $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADlSURBVDiNrdNBUsJAEAXQlyw4hq4hwWPqTixET6ELkZ16CcAq7oFLqXExjaYgQVNlV/Viev7/6XT/4TjGuME7PiLXUatb8N8xwB12SFjiIXIZtU/MAntEfgvQE4YtHxhiHpjXQ5H7uLhEcaLLAleBvd0Xx9Ha/BdyU+Q5OBV5OKmj7a4YBWdSyNPe4aKHAHkzqcQZNj3JgnNexqE8heyIAulffuFF3kTfIVbBVeu/xoXGGsn2TLJJ/mqkafNiINszySYZdbS90GHlvcgsWktY4TFy7ecxTdvIzahxHQLbyFXUqkPwF2ASRNYgB/PXAAAAAElFTkSuQmCC';
                    }
		    $alt = 'full';
		    $title = escape_html(t('Full Moon'));
	    } else {
                    if ($Options{pngs}) {
                            $img = smoosh($Options{imgbase}, 'lastquarter.png');
                    } else {
                            $img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGQAAABkABchkaRQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAADmSURBVDiNndMxTsNAEIXhzy5yCyQ6FAgcE7oQheQWUAAl5BIkREoZrgB0GFNkHBl7bURGsryaee/3jHeXdpxjghU+8InXyI0S+n0MMEeBEi+4jfV3vAvMQtsyL0J0j2GtViaeRRMyj8IlsgY8BSijE2Kur/hy09wHKMJrEolhwtwHKDHOsI4OLnoAXfl1jiNsOkR9keE4P8D4q4scbzg5xIxtjie709f1E7siC+9+Gx/8fxvPKtEsklcJSBdgWhcN8ByFR5z+AWgd5QpyE+OUWOJO+zJNU+Z6jHAdgHe7K73CuD5zFT9nCmRDIssCaAAAAABJRU5ErkJggg==';
                    }
		    $alt = 'last';
		    $title = escape_html(t('Last Quarter'));
	    }
            my $url_pre = '';
            my $url_post = '';
            if ($moons->[$day]->{url}) {
                    $url_pre = '<a href="' . $moons->[$day]->{url} . '">';
                    $url_post = '</a>';
            }
	    if ($Options{nostyle}) {
		print("<div style=\"float: left\">$url_pre<img border=\"0\" width=\"16\" height=\"16\" alt=\"$alt\" title=\"$title\" src=\"$img\">msg$url_post</div>");
	    } else {
		print("<div class=\"rem-moon\">$url_pre<img width=\"16\" height=\"16\" alt=\"$alt\" title=\"$title\" src=\"$img\">$msg$url_post</div>");
	    }
    }

    if ($type eq 'monthly') {
        if ($Options{nostyle}) {
            print "<div style=\"float: right\">$day$week</div>\n";
            print "<p>&nbsp;</p>\n";
        } else {
            if ($Options{yaag}) {
                print "<div class=\"rem-yaag-daynumber\">$day$week</div>\n";
            } else {
                print "<div class=\"rem-daynumber\">$day$week</div>\n";
            }
        }
    } else {
        if ($Options{nostyle}) {
            print "<div style=\"float: right\">$week</div>\n";
            print "<p>&nbsp;</p>\n";
        } else {
            print "<div class=\"rem-daynumber\">$week</div>\n";
        }
    }
    if ($days->[$day]) {
	    print(join("\n", @{$days->[$day]}));
    }

    print "</td>\n";
}

sub escape_html
{
    my($in) = @_;
    $in =~ s/\&/\&amp;/g;
    $in =~ s/\</\&lt;/g;
    $in =~ s/\>/\&gt;/g;
    return $in;
}

parse_options();
if ($Options{help}) {
    usage(0);
    exit(0);
} elsif ($Options{man}) {
    system("perldoc $0");
    exit(0);
} elsif ($Options{version}) {
    print "rem2html version $rem2html_version\n";
    exit(0);
}

if (-t STDIN) { ## no critic
    print STDERR "$TIDY_PROGNAME: Input should not come from a terminal.\n\n";
    usage(1);
}

my $found_something = 0;
while(1) {
    last if (!parse_input());
    start_output() unless $found_something;
    $found_something = 1;
    output_calendar('monthly', undef);
}
if ($found_something) {
    end_output();
    exit(0);
} else {
    print STDERR "$TIDY_PROGNAME: Could not find any calendar data on STDIN.\n";
    exit(1);
}

sub default_stylesheet
{
        return <<'EOF';
table.rem-cal {
  font-family: helvetica, arial, sans-serif;
  font-size: 12pt;
}

span.year_heading {
  font-family: helvetica, arial, sans-serif;
  font-size: 28pt;
}

table.rem-sc-table {
  font-family: helvetica, arial, sans-serif;
  font-size: 10pt;
  width: 95%;
  float: left;
}

caption.rem-cal-caption {
  font-size: 14pt;
  font-weight: bold;
}

th.rem-cal-hdr {
  width: 14%;
  border-style: solid;
  border-width: 1px;
  vertical-align: top;
}
td.rem-empty, td.rem-cell, td.rem-small-calendar {
  width: 14%;
  height: 7em;
  border-style: solid;
  border-width: 1px;
  vertical-align: top;
}

table.rem-cal {
  width: 100%;
  border-collapse: collapse;
}

div.rem-daynumber {
  float: right;
  text-align: right;
  vertical-align: top;
  font-size: 14pt;
}

p.rem-entry {
    clear: both;
}

div.rem-moon {
  float: left;
  text-align: left;
  vertical-align: top;
}

th.rem-sc-hdr {
  text-align: right;
}

td.rem-sc-empty-cell, td.rem-sc-cell {
  text-align: right;
  width: 14%;
}

caption.rem-sc-caption {
  font-size: 12pt;
}

table.rem-yaag {
  font-family: helvetica, arial, sans-serif;
  font-size: 8pt;
  table-layout: fixed;
  cellspacing: 0;
  border-collapse: collapse;
}

td.rem-yaag-cell {
  height: 7em;
  border-style: solid;
  border-color: #adadad;
  border-width: 1px;
  vertical-align: top;
}

th.rem-yaag-weekday-header {
  font-weight: 100;
  font-size: 9pt;
  padding: 6px 0 0 6px;
  text-align:left;
  border-style: solid;
  border-color: #adadad;
  border-width: 1px;
  vertical-align: top;
}

td.rem-yaag-cal-blank {
  height: 7em;
  border-style: solid;
  border-width: 1px;
  border-color: #adadad;
  background-color: #d5d6d5;
  vertical-align: top;
}

td.rem-yaag-end-of-week {
  border-right-width: 3;
}

th.rem-yaag-end-of-week {
  border-right-width: 3;
}

th.rem-yaag-cal-hdr {
  text-align: right;
  padding-top: 8px;
  padding-right: 5px;
  font-weight: 100;
  font-size: 9pt;
  border-style: solid;
  border-color: #adadad;
  border-width: 1px;
  vertical-align: top;
}

div.rem-yaag-daynumber {
  float: right;
  text-align: right;
  vertical-align: top;
  font-size: 10pt;
}

tr.rem-yaag-cal-row:nth-child(3n+1) {
  border-bottom: 3px solid #000;
}

.rem-yaag-weekday-row .rem-yaag-empty-header {
  height: 20pt;
  width: 70px !important;
}

td.rem-today {
  border-style: solid;
  border-width: 2px;
  border-color: #EE3333;
}

EOF
}
