#!/usr/bin/env perl

# Auto IRC Bot. An advanced, lightweight and powerful IRC bot.
# Copyright (C) 2010-2011 Xelhua Development Team (doc/CREDITS)
# This program is free software; rights to this code are stated in doc/LICENSE.
package Auto;
require 5.10.0;
use strict;
use warnings;
use POSIX;
use locale;
use Mouse;
use IO::Socket;
use Async;
use Class::Unload;
use FindBin qw($Bin);
our $Bin = $Bin;
BEGIN { unshift(@INC, "$Bin/../src"); }
use API::Std qw(conf_get err);
use API::Log qw(println alog dbug);
use DB::Flatfile;
use Parser::Config;
use Parser::Lang;
use Parser::IRC;
#use Sys;

# Set version information.
use constant {
	NAME   => 'Auto IRC Bot',
	VER    => 3,
	SVER   => 0,
	REV    => 0,
	RSTAGE => 'd'
};
our $VERSION = 3.0.0;
$0 = NAME;

# Check for build files.
unless (-e "$Bin/../build/os" and -e "$Bin/../build/perl" and -e "$Bin/../build/time" and -e "$Bin/../build/ver") {
	println "Missing build file(s). Please build Auto before running it." and exit;
}

# Check build OS.
open(my $BFOS, q{<}, "$Bin/../build/os") or println "Cannot start: Broken build." and exit;
my @BFOS = <$BFOS>;
close $BFOS;
if ($BFOS[0] ne $^O."\n") {
	println "Cannot start: Broken build." and exit;
}
undef @BFOS;

# Check build Perl version.
open(my $BFPERL, q{<}, "$Bin/../build/perl") or println "Cannot start: Broken build." and exit;
my @BFPERL = <$BFPERL>;
close $BFPERL;
if ($BFPERL[0] ne $]."\n") {
	println "Cannot start: Broken build." and exit;
}
undef @BFPERL;

# Check build Auto version.
open(my $BFVER, q{<}, "$Bin/../build/ver") or println "Cannot start: Broken build." and exit;
my @BFVER = <$BFVER>;
close $BFVER;
if ($BFVER[0] ne VER.".".SVER.".".REV.RSTAGE."\n") {
	println "Cannot start: Broken build." and exit;
}
undef @BFVER;

# Set signal handlers.
$SIG{TERM} = \&signal_TERM;
$SIG{INT}  = \&signal_INT;
$SIG{HUP}  = \&signal_HUP;
API::Std::event_add("on_sigterm");
API::Std::event_add("on_sigint");
API::Std::event_add("on_sighup");


# Print startup message.
println "* ".NAME." (version ".VER.".".SVER.".".REV.RSTAGE.") is starting up...";

our ($APID, %TIMERS);

# Check for debug mode.
our $DEBUG = 0;
if (defined $ARGV[0]) {
	if ($ARGV[0] eq '-d') {
		$DEBUG = 1;
	}
}

# Parse configuration file.
println "* Parsing configuration file auto.conf...";
our $CONF = Parser::Config->new("auto.conf") or err(1, "Failed to parse configuration file!", 1);
our %SETTINGS = $CONF->parse or err(1, "Failed to parse configuration file!", 1);
println " Success";

# Check for required configuration values.
my @REQCVALS = qw(locale expire_logs server);
foreach my $REQCVAL (@REQCVALS) {
	if (!conf_get($REQCVAL)) {
		my $err = 2;
		if ($REQCVAL eq "expire_logs") {
			$err = 1;
		}
		err($err, "Missing required configuration value: $REQCVAL", 1);
	}
}
undef @REQCVALS;

# Parse translations.
println "* Parsing translation files...";
our $LOCALE = (conf_get("locale"))[0][0];
my @lang = split('_', $LOCALE);
Parser::Lang::parse($lang[0]) or err(2, "Failed to parse translation files!", 1);
undef @lang;
println " Success";

# Expire old logs.
API::Log::expire_logs();

# Load database.
DB::load();

# Successful startup.
our $STARTTIME = time;
println "* Auto successfully started at ".POSIX::strftime("%Y-%m-%d %I:%M:%S %p", localtime).".";
alog "Auto successfully started.";

# Fork into the background if not in debug mode.
unless ($DEBUG) {
    println "*** Becoming a daemon...";
    open(STDIN, q{<}, '/dev/null') or err(2, "Can't read /dev/null: $!", 1);
    open(STDOUT, q{>>}, '/dev/null') or err(2, "Can't write to /dev/null: $!", 1);
    open(STDERR, q{>>}, '/dev/null') or err(2, "Can't write to /dev/null: $!", 1);
    $APID = fork();
    unless ($APID == 0) {
        alog "* Successfully forked into the background. Process ID: ".$APID;
        unless (-e "$Bin/auto.pid") {
			system("touch $Bin/auto.pid");
		}
		open(my $FPID, q{>}, "$Bin/auto.pid") or exit;
		print $FPID "$APID\n" or exit;
		close $FPID or exit;
        exit;
    }
    POSIX::setsid() or err(2, "Can't start a new session: $!", 1);
}
else {
    $APID = $$;
}

## Create sockets.
alog "* Connecting to servers...";
dbug "* Connecting to servers...";
# Get servers from config.
my %cservers = conf_get("server");
# Set the hashes and processes hashes.
my (%SOCKET, %SOCKPROC);
my $it = 0;
# Iterate through each configured server.
foreach my $cskey (keys %cservers) {
	# Create the socket.
	$SOCKET{$cskey} = IO::Socket::INET->new(
		Proto => "tcp",
		LocalAddr => $cservers{$cskey}{'bind'}[0],
		PeerAddr  => $cservers{$cskey}{'host'}[0],
		PeerPort  => $cservers{$cskey}{'port'}[0]
	) or err(2, "Failed to connect to server: ".$cskey." [".$cservers{$cskey}{'host'}[0].":".$cservers{$cskey}{'port'}[0]."]", 0) and next; # Or error.
	# Send PASS if we have one.
	if (defined $cservers{$cskey}{'pass'}[0]) {
		socksnd($cskey, "PASS :".$cservers{$cskey}{'pass'}[0]) or                 
			err(2, "Failed to connect to server: ".$cskey." [".$cservers{$cskey}{'host'}[0].":".$cservers{$cskey}{'port'}[0]."]", 0) 
			and next;
	}
	# Send USER and NICK.
	socksnd($cskey, "USER ".$cservers{$cskey}{'ident'}[0]." * * :".$cservers{$cskey}{'realname'}[0]) or
		err(2, "Failed to connect to server: ".$cskey." [".$cservers{$cskey}{'host'}[0].":".$cservers{$cskey}{'port'}[0]."]", 0) 
		and next;
	API::IRC::nick($cskey, $cservers{$cskey}{'nick'}[0]);
	# Create the process.
	$SOCKPROC{$cskey} = Async->new(sub {
		# Infinite loop.
		while (1) {
			# Read data from socket.
			my $data = readline($SOCKET{$cskey});
			# Lost connection: kill process.
			unless (defined $data) {
				exit;
			}
			# Get rid of the newlines.
			chomp $data;
			# Debug.
			dbug "$cskey -> You: $data";
			
			# Parse data.
			Parser::IRC::ircparse($cskey, $data);
		}
	}) or err(2, "Failed to create process for ".$cskey, 0) and next;
	# Success!
	alog "** Successfully connected to server: ".$cskey;
	dbug "** Successfully connected to server: ".$cskey;
	$it = 1;
}

# Success!
if ($it) {
	alog "** Success: Connected to server(s).";
	dbug "** Success: Connected to server(s).";
}
else {
	err(2, "No server connections.", 1);
}
undef $it;

# Check timers.
while (1) { 
	foreach my $tk (keys %TIMERS) {
		if ($TIMERS{$tk}{time} <= time) {
			&{ $TIMERS{$tk}{sub} }();
			if ($TIMERS{$tk}{type} == 1) {
				# If it's type 1, delete from memory.
				delete $TIMERS{$tk};
			}
			elsif ($TIMERS{$tk}{type} == 2) {
				# If it's type 2, reset timer.
				$TIMERS{$tk}{time} = time + $TIMERS{$tk}{secs};
			}
			else {
				# This should never happen.
				delete $TIMERS{$tk};
			}
		}
	}
	sleep 1; 	
}

###############
# Subroutines #
###############

# Send data to socket.
sub socksnd 
{
	my ($svr, $data) = @_;
	
	if (defined $SOCKET{$svr}) {
		send($SOCKET{$svr}, $data."\n", 0);
		dbug "You -> $svr: $data";
		return 1;
	}
	else {
		return 0;
	}
}

# Load a module.
sub mod_load {
    my ($module) = @_;
    
    if (-e "$Bin/../modules/".lc($module).".pm") {
        do "$Bin/../modules/".lc($module).".pm" and return 1 or print $@ and return 0;
    }
    else {
        return 0;
    }
}


## Signal handlers

# SIGTERM
sub signal_TERM
{
	API::Std::event_run("on_sigterm");
	API::IRC::quit($_, "Caught SIGTERM") foreach (keys %SOCKET);
	DB::flush();
	dbug "!!! Caught SIGTERM; terminating...";
	alog "!!! Caught SIGTERM; terminating...";
	if (-e "$Bin/auto.pid") {
		system("rm $Bin/auto.pid");
	}
	sleep 1;
	exit;
}

# SIGINT
sub signal_INT
{
	API::Std::event_run("on_sigint");
	API::IRC::quit($_, "Caught SIGINT") foreach (keys %SOCKET);
	DB::flush();
	dbug "!!! Caught SIGINT; terminating...";
	alog "!!! Caught SIGINT; terminating...";
	if (-e "$Bin/auto.pid") {
		system("rm $Bin/auto.pid");
	}
	sleep 1;
	exit;
}

# SIGHUP
sub signal_HUP
{
	API::Std::event_run("on_sighup");
	dbug "!!! Caught SIGHUP but rehash is unavailable; ignoring";
	alog "!!! Caught SIGHUP but rehash is unavailable; ignoring";
	return 1;
}
