#!/usr/bin/perl
##########################################################################
#    onis 0.8.2                                               2005-06-07 #
#---=============--------------------------------------------------------#
# Language: Perl                                                         #
# Purpose:  Generating statistics                                        #
# Input:    IRC-Logfiles                                                 #
# Output:   One HTML file                                                #
# Version:  0.8.2 (unstable)                                             #
# License:  GPL                                                          #
# Homepage: http://verplant.org/onis/                                    #
# Authors:  Florian octo Forster <octo@verplant.org>                     #
#           Contributions are listed in THANKS                           #
##########################################################################

BEGIN
{
	unshift (@INC, 'lib');

	# 0x0010   Language (make not-translated lines red/yellow)
	# 0x0020   Parser (dropped lines)
	# 0x0040   Parser (time information)
	# 0x0100   Data::Core (host unsharp)
	# 0x0200   Data::Persistent
	# 0x0400   Data::Core (dump incoming data to stderr)
	# 0x0800   Data::Core (initializing)
	# 0x1000   Onis::Users
	$::DEBUG = 0x0000;
}

use strict;
use warnings;
use vars qw/$VERSION $REVISION/;

use Onis::Config qw/get_config parse_argv read_config/;
use File::Basename qw/dirname/;
use Fcntl qw/:flock/;

=head1 NAME

onis - onis not irs stats

=head1 SYNOPSIS

B<onis> [I<options>] I<logfile>...

=head1 DESCRIPTION

onis is a script that converts IRC logfiles into an HTML statistics page. It
provides information about daily channel usage, user activity, and channel
trivia. It provides a configurable customization and supports Dancer,
dircproxy, eggdrop, irssi, mIRC, and XChat logs. Persistent data (history
files) and automatic log purging make onis applicable for a large number of
logfiles. It also features a powerful translation infrastructure.

=cut

$VERSION = '0.8.2';
$REVISION = '$LastChangedRevision: 121 $';

if (!$VERSION)
{
	$VERSION = $REVISION;
	$VERSION =~ s/^\D*(\d+).*/r$1/;
}

print STDERR $/, __FILE__, ': $Id: onis 121 2005-06-07 19:42:47Z octo $' if ($::DEBUG);

our $FileInfo;
our $PurgeLogs = 0;

parse_argv (@ARGV);
read_config (get_config ('config') ? get_config ('config') : 'onis.conf');
read_config (scalar get_config ('theme')) if (get_config ('theme'));

my $output = get_config ('output');
if (!$output)
{
	$output = "reports/onis.html";
}

foreach ('Core', get_config ('plugin'))
{
	my $module = ucfirst (lc ($_));
	require "Onis/Plugins/$module.pm";
}

if (!get_config ('input'))
{
	print STDERR <<EOF;

Usage: $0 [options] <logfile> [logfile logfile ..]

Options:
	--config		Specify alternate config file
	--output <file>		Defines the file to write the HTML to.
	--overwrite <bool>	Overwrites files without prompting.
	--channel <channel>	Defines the channel's name.
	--logtype <type>	Defines the logfile's type.
				See 'config' for a complete list.
	--user <name>		Define's the generator's name.

For a full list of all options please read the onis(1) manpage.
EOF
	exit (1);
}

if (-e $output)
{
	my $overwrite = 0;
	if (get_config ('overwrite'))
	{
		my $tmp = lc (get_config ('overwrite'));
		if ($tmp eq 'true' or $tmp eq 'yes' or $tmp eq 'on')
		{
			$overwrite = 1;
		}
	}
	
	if (!$overwrite)
	{
		print STDERR <<MESSAGE;

WARNING: The output file ``$output'' already exists

  You can set the ``overwrite'' option in the config
  file to disable this dialog.

MESSAGE
		print STDERR 'Are you sure you want to overwrite it? [Y|n] ';
		my $answer = <STDIN>;
		exit (1) if ($answer =~ m/n/i);
	}
}

my $logtype = 'Eggdrop';
if (get_config ('logtype'))
{
	$logtype = ucfirst (lc (get_config ('logtype')));
}

require "Onis/Parser/$logtype.pm";
require Onis::Parser::Persistent;
require Onis::Data::Persistent;
import Onis::Parser (qw(parse last_date));
import Onis::Parser::Persistent (qw(newfile));
import Onis::Data::Persistent ();

$FileInfo = Onis::Data::Persistent->new ('FileInfo', 'inode', qw(mtime));

if (get_config ('purge_logs'))
{
	my $temp = lc (get_config ('purge_logs'));
	if (($temp eq 'truncate') or ($temp eq 'shorten'))
	{
		$PurgeLogs = 1;
	}
	elsif (($temp eq 'delete') or ($temp eq 'remove')
			or ($temp eq 'del'))
	{
		$PurgeLogs = 2;
	}
}

for (get_config ('input'))
{
	my $file = $_;
	my $logfile;
	my $status = 4;
	my $position = 0;
	my $mtime;
	my $size;
	my $inode;

	($inode, $size, $mtime) = (stat ($file))[1,7,9];

	print STDERR $/, $/, __FILE__, " --- New File ``$file'' ---" if ($::DEBUG & 0x200);
	
	if (!defined ($mtime))
	{
		print STDERR $/, __FILE__, ": Unable to stat file ``$file''";
		next;
	}
	else
	{
		my ($old_mtime) = $FileInfo->get ($inode);

		print STDERR $/, __FILE__, ": ``$file'': " if ($::DEBUG & 0x200);

		if (defined ($old_mtime))
		{
			if ($old_mtime == $mtime)
			{
				print STDERR "File did not change. Skipping." if ($::DEBUG & 0x200);
				next;
			}
			elsif ($old_mtime < $mtime)
			{
				print STDERR "File changed. Reading it again." if ($::DEBUG & 0x200);
			}
			else
			{
				print STDERR "File ``$file'' is older than expected. There might be a problem!";
			}
		}
		else
		{
			print STDERR "File appears to be new. Reading it." if ($::DEBUG & 0x200);
		}
		$FileInfo->put ($inode, $mtime);
	}
	
	# truncate
	if ($PurgeLogs == 1)
	{
		unless (open ($logfile, '+< ' . $file))
		{
			print STDERR $/, __FILE__, ": Unable to open file ``$file'': $!";
			next;
		}
	}
	else
	{
		unless (open ($logfile, '< ' . $file))
		{
			print STDERR $/, __FILE__, ": Unable to open file ``$file'': $!";
			next;
		}
	}
	
	if ($PurgeLogs)
	{
		unless (flock ($logfile, LOCK_EX))
		{
			print STDERR $/, __FILE__, ": Unable to get an exclusive lock for file ``$file'': $!";
			close ($logfile);
			next;
		}
	}
	else
	{
		unless (flock ($logfile, LOCK_SH))
		{
			print STDERR $/, __FILE__, ": Unable to get a shared lock for file ``$file'': $!";
			close ($logfile);
			next;
		}
	}
	
	newfile ($inode);
	while (<$logfile>)
	{
		s/\n|\r//g;
		$status = parse ($_);

		# 0 == rewind file
		# 1 == line parsed
		# 2 == unable to parse
		# 3 == line old
		# 4 == don't have date

		if ($status == 0)
		{
			print STDERR $/, __FILE__, ": Rewinding file ``$file''" if ($::DEBUG & 0x200);
			seek ($logfile, 0, 0);
			$position = 0;
		}
		elsif (($status == 1) or ($status == 2)
				or ($status == 3))
		{
			$position = tell ($logfile);
		}
		elsif ($status == 4)
		{
			# void
		}
		else
		{
			print STDERR $/, __FILE__, ": Parser returned unknown status code: ``$status''";
		}
	}

	if ($PurgeLogs and (($status == 1)
				or ($status == 2)
				or ($status == 3)))
	{
		if (($PurgeLogs > 1)
			#and (($position + 1) >= $size)
			)
		{
			# delete file
			print STDERR $/, __FILE__, ": Deleting empty file ``$file''" if ($::DEBUG & 0x200);
			close ($logfile);

			if (-w $file)
			{
				unless (unlink ($file))
				{
					print STDERR $/, __FILE__, ": Unable to delete empty file ``$file'': $!";
				}
				delete ($FileInfo->{$inode});
			}
			else
			{
				print STDERR $/, __FILE__, ": Won't delete ``$file''. Set it to writeable first!";
			}
		}
		else
		{
			seek ($logfile, 0, 0);
			if (truncate ($logfile, 0))
			{
				print $logfile &last_date ();
				print STDERR $/, __FILE__, ": Truncated ``$file''" if ($::DEBUG & 0x200);
			}
			else
			{
				print STDERR $/, __FILE__, ": Couldn't truncate file ``$file'': $!";
			}
			
			close ($logfile);
		}
	}
	else
	{	
		close ($logfile);
	}
}

require Onis::Data::Core;
require Onis::Html;
import Onis::Data::Core qw#print_output#;
import Onis::Html qw#open_file close_file#;

if (open_file ($output))
{
	print_output ();
	close_file ();
}
else
{
	# Fail and make noise! ;)
	print STDERR <<MESSAGE;

ERROR: Unable to open output file

The output file ``$output'' could not be opened. Please make sure to set
the permissions right and try again.

MESSAGE
	exit (1);
}

exit (0);

END
{
	print $/ if ($::DEBUG);
}

=head1 OPTIONS

=head2 Core options

=over 4

=item B<config>: I<file>;

Load the config from this file. B<(command line only)>

=item B<users_config>: I<file>;

Sets the file from which to read the user configuration.

=item B<language_file>: I<file>;

Sets the language file/translation to use.

=item B<plugin>: I<string>;

Sets the plugins to load. The plugin B<Core> will always be loaded.

=item B<input>: I<file>;

Read and parse this file(s). B<(config file only)>

=item B<logtype>: I<string>;

Sets the parser to use for parsing the input file.

=item B<output>: I<file>;

Write the generated output to this file.

=item B<overwrite>: I<bool>;

Sets wether or not to overwrite the output-file if it exists.

=item B<purge_logs>: "I<false>" | "I<truncate>" | "I<delete>";

Sets wether logs should be truncated or even removes after they have been
parsed.

=item B<user>: I<string>;

Sets the user that created the page. Defaults to the environment variable
B<USER> or "onis", if it is not set.

=item B<channel>: I<string>;

Sets the name of the channel being parsed. Normally this is auto-detected.

=item B<unsharp>: "I<none>" | "I<light>" | "I<medium>" | "I<hard>";

Sets how to do unsharping. What each setting actually does is described in the
readme and in L<Onis::Data::Core>.

=back

=head2 Appearance

=over 4

=item B<theme>: I<file>;

Theme file to load.

=item B<stylesheet>: I<file>;

Sets the stylesheet to use. This is included in the HTML-file as-is, so you
have to take care of absolute/relative paths yourself..

=item B<color_codes>: I<bool>;

Wether or not to print the color codes (introduced by mIRC, used by idiots and
ignored by the rest) in the generated HTML-file. Of course this defaults to not
print the codes..

=item B<display_images>: I<bool>;

Sets if user-images should be displayed.

=item B<default_image>: I<file>;

Sets the default image to use if no user-defined image is available.

=item B<display_lines>: "I<none>" | "I<number>" | "I<bar>" | "I<both>";

=item B<display_words>: "I<none>" | "I<number>" | "I<bar>" | "I<both>";

=item B<display_chars>: "I<none>" | "I<number>" | "I<bar>" | "I<both>";

Sets if and how lines, words and/or characters should be displayed.

=item B<sort_by>: "I<lines>" | "I<words>" | "I<chars>";

Sets wether to sort by lines, words or characters written.

=item B<display_times>: I<bool>;

Wether or not to display a fixed width bar that shows when a user is most
active.

=item B<horizontal_images>: I<file>, I<file>, I<file>, I<file>;

=item B<vertical_images>:   I<file>, I<file>, I<file>, I<file>;

Sets the images to use for horizontal and vertical bars. This should be used in
the theme-file.

=item B<encoding>: I<string>;

Sets the encoding to include in the HTML-file. If you don't know what this is,
don't change it..

=item B<public_page>: I<bool>;

Wether or not this is a public page. Public pages may be linked on the onis
homepage at some point in the fututre..

=back

=head2 Storage / Persistency

=over 4

=item B<storage_module>: I<string>;

Sets the storage-module to use.

=item B<storage_dir>: I<directory>;

Sets the directory to store persistency information under.

=item B<storage_file>: I<file>;

Sets the file to write persistency data to, if applicable by the
storage-module.

=back

=head2 Plugins

=over 4

=item B<min_word_length>: I<number>;

Substring containing only word-characters needs to be this long to be
considered a word.

=item B<plugin_max>: I<number>;

Sets the number of "most referenced nicks", "most used words" and the like to
be displayed. This option will be removed in the future.

=item B<longlines>: I<number>;

=item B<shortlines>: I<number>;

The number of lines in the big and the small table. While in the big table one
line is dedicated for one person the small table displays six persons per line.

=item B<quote_cache_size>: I<number>;

Sets how many quotes are to be cached for each nick. At the end of the run one
of the quotes in the cache will be chosen at random and displayed.

=item B<quote_min>: I<number>;

=item B<quote_max>: I<number>;

Sets the minimum and maximum length of a quote. Too short quotes may be not
very typical for a person, too long quotes may clutter the layout.

=item B<conversations_number>: I<number>;

=item B<userdetails_conversations_number>: I<number>;

Number of conversations partners to include in the output (or in the
conversations section of the userdetails plugin).

=item B<soliloquies_count>: I<number>;

Sets how many lines without interruption are considered a soliloquy.

=item B<longterm_days>: I<number>;

=item B<userdetails_longterm_days>: I<number>;

Sets the number of days shown in the longterm-plugin (or the longter-section of
the userdetails-plugin).

=item B<ignore_words>: I<number>;

The Words-Plugin will ignore words with less than this characters.

=item B<userdetails_number>: I<number>;

The number of nicks to print userdetails for.

=back

=head1 AUTHOR

Florian Forster E<lt>octo at verplant.orgE<gt>

=cut
