#!/usr/bin/perl -w
#nonforker - server who multiplexes without forking

use POSIX;
use IO::Socket;
use IO::Select;
use Socket;
use Getopt::Std;
use Fcntl;
use Tie::RefHash;

use Log::Log4perl "get_logger";
use Data::Dumper;
use SKM::DB;
use NextCAM::ELClient;
use NextCAM::CRC qw(crc);

# CONS
my $APL = $ENV{APL};
my $APL_VAR = $ENV{APL_VAR};
my $LTSD_CONF = "$APL_VAR/lts/ltsd.conf";
my $CMD_MAP = "$APL_VAR/lts/cmd.map"; # Mapping: cmd => eventtype,event msg
my $TASS_MODELID = "TASS";
my $SOCK_TMT = 5;
my $EVENT_SOURCE = 3; # source=sensor
my $DEV_SCAN_TMT = 30; # Rescan device list twice a minute
my $EVENT_TYPE_ALERT = 1;
my $EVENT_TYPE_INFO = 0;

my $MSG_START = "\x{01}\x{02}"; # <soh> <stx> <so> <bel> <etx> <\0> <ht>
#my $MSG_END = "\n";
my $MSG_END = "\x{03}\x{04}";

my $TASS_PREFIX = "\x{01}\x{02}\x{0E}"; # <soh> <stx> <so> <bel> <etx> <\0> <ht>
my $TASS_SUFFIX = "\x{11}";
#my $TASS_SUFFIX = "\x{11}\x{03}\x{04}";
my $TASS_ROOT_LEN = 13;

my $TASS_WAIT  = "\x{01}\x{02}\x{41}\x{4E}\x{4E}\x{3F}\x{17}";
#my $TASS_WAIT  = "\x{01}\x{02}\x{57}\x{41}\x{4E}\x{4E}\x{13}\x{03}\x{04}";
#my $TASS_YWAIT = "\x{01}\x{02}\x{59}\x{41}\x{4E}\x{4E}\x{D7}";
my $TASS_YWAIT = "\x{01}\x{02}\x{59}\x{41}\x{4E}\x{4E}\x{D7}\x{03}\x{04}";
my $TASS_RDY   = "\x{01}\x{02}\x{52}\x{44}\x{59}\x{75}";
#my $TASS_RDY   = "\x{01}\x{02}\x{52}\x{44}\x{59}\x{75}\x{03}\x{04}";

# VARS
my $Debug = 0;
my $ConfPath;
my %Opts;
my %Conf;
my %CmdMap;
my %Devs;
my $dbm;
my @EventQueue;   # Events to send to ELog

Log::Log4perl::init_and_watch("$APL/lts/etc/logger_ltsd.conf", 60);
my $log = get_logger('LTSD');
my $ELog;
my $Sock;
my $LocalSock;
my $Last_Scan_TS = 0;
my $SigHup = 0;        # SIGHUP flag
my $Buffer = '';

# SIG
$SIG{TERM} = sub { $log->logdie("Got SIGTERM. Exiting") };
$SIG{HUP}  = sub { $log->logdie("Got SIGHUP. Exiting") };

# SUBS
#sub $log->debug { warn(gmtime()." DEBUG: @_\n") if $Debug; $log->debug(@_); }
#sub $log->info  { warn(gmtime()." INFO: @_\n")  if $Debug; $log->info(@_);  }
#sub log_warn  { warn(gmtime()." WARN: @_\n")  if $Debug; $log->warn(@_);  }
#sub log_error { warn(gmtime()." ERROR: @_\n") if $Debug; $log->error(@_); }

sub decdump
{
  my $bytes = shift;
  my @dec = unpack("H*", $bytes);
  return join("", map {"{".$_."}"} @dec);
}

sub db_master
{
  eval { $dbm->disconnect() if $dbm; $dbm=''; }; # disconnect if defined

  $dbm = eval {  DBMaster({RaiseError => 1, PrintError => 0}) };
  $log->logdie("DB_MASTER: $@") if $@;
}
sub hash_diff
{
  my ($h1, $h2) = @_;

  my ($q1, $q2) = (scalar keys %$h1, scalar keys %$h2);
  return 1 if $q1 != $q2;
  foreach my $k1 (keys %$h1) {
    return 1 if not exists $h2->{$k1};
  }
  foreach my $k2 (keys %$h2) {
    return 1 if not exists $h1->{$k2};
  }
  return 0;
}

sub scan_devices
{
  my %devs = ();
  eval {
    my $ra = $dbm->selectall_arrayref(
    "SELECT o.obj,a.attr,a.val FROM _objs o INNER JOIN _obj_attr a ON o.obj=a.obj
    WHERE o.deleted=0 AND o.otype='D' and o.subtype='S'"
    );
    foreach my $row (@$ra) {
      my ($obj,$attr,$val) = @$row;
      $devs{$obj}{$attr} = $val;
    }
  };
  $log->logdie("DB fetch devices failed: $@") if $@;

  foreach $obj (keys %devs) {
    delete $devs{$obj}, next if $devs{$obj}{"MODELID"} ne $TASS_MODELID;
    delete $devs{$obj}, next if $devs{$obj}{"ARCHSTATE"} ne "on";
  }

  if (hash_diff \%Devs, \%devs) {
    my $i = 1;
    my $total = keys %devs;
    $log->debug("Rescan list of TASS sensors ($total total):");
    foreach my $lts (values %devs) {
      $log->debug("$i. OBJID=$lts->{OBJID} HW_ID=$lts->{HW_ID}");
      $i++;
    }
  }
  %Devs = %devs;

  $Last_Scan_TS = time;
}

sub submit_events
{
  while (my $evt = shift @EventQueue) {
    $ELog->createEvent($evt, 1); # Async action
    $log->debug("Submit event: OBJID=$evt->{objid} EVENTTYPE=$evt->{eventtype} MSG=$evt->{msg}");
  }
}

sub gw_connect
{
  $log->debug("Trying to connect to $Conf{IP} : $Conf{PORT}");
  $Sock = IO::Socket::INET->new(
  PeerAddr  => $Conf{IP},
  PeerPort  => $Conf{PORT},
  Proto     => 'tcp',
  Timeout   => $SOCK_TMT,
  KeepALive => 1,
  Blocking  => 1,
  );
  die "Connection to gateway failed: $!" unless $Sock;

  # Tune up socket
  $Sock->autoflush(1);

  $select->add($Sock);
}

sub local_connect
{
  $log->debug("Trying to listen to localhost : $Conf{LOCALPORT}");
  $server = IO::Socket::INET->new(
	LocalPort => $Conf{LOCALPORT},
  	Listen    => 10,
	Proto	  => 'tcp',
	Reuse	  => 1,
  )
  or die "Can't make server socket: $@\n";
  nonblock($server);

  $select->add($server);
}

sub configure
{
  getopts("d", \%Opts);
  $Debug = $Opts{d};
  $ConfPath = shift @ARGV;
  $ConfPath = $LTSD_CONF if not $ConfPath and -f $LTSD_CONF;

  die "Must specify path to config file!" unless $ConfPath;
  die "Invalid config file path!" unless -f $ConfPath;
  die "Command map file is missing!" unless -f $CMD_MAP;

  $log->info("------ Starting Legacy TASS Sensors Daemon (PID=$$)");
  $log->info("Config file: $ConfPath");

  # Read and parse ltsd.conf
  open FH, $ConfPath or die "Cannot read config file: $!";
  %Conf = map {/(\w+)=(.*)/} grep {/^\w+=/} <FH>;
  close FH;

  die "IP must be specified in ltsd.conf" unless $Conf{IP};
  die "PORT must be specified in ltsd.conf" unless $Conf{PORT};

  # Read and parse cmd.map
  open FH, $CMD_MAP or die "Cannot read command map file: $!";
  %CmdMap = map
  {
    /^(\d+)=(info|alert|ignore)(,(.*))?$/;
    ($1 => { eventtype => $2, msg => $4 })
  }
  grep { /^\d+=/ } <FH>;

  close FH;

  # Init ELog client
  $ELog = new ELClient;
  die "ELog client init failed" unless $ELog;

  # Fetch sensors configuration
  scan_devices;

  #
  $select = IO::Select->new();
}

# begin with empty buffers
my %inbuffer  = ();
my %outbuffer = ();
my %ready     = ();

tie %ready, 'Tie::RefHash';


sub event_loop
{
  # Main loop: check reads/accepts, check writes, check ready to process
#  $select->add($Sock);
#  $select->add($server);
#  $log->debug(Dumper($select));
  while (1) {
    my $client;
    my $rv;
    my $data;

    # check for new information on the connections we have

    # anything to read or accept?
    foreach $client ($select->can_read(1)) {
#	$log->debug("connection");
      if ($client == $server) {
        # accept a new connection
#	$log->debug("cmd sender connection");
        $client = $server->accept();
        $select->add($client);
        nonblock($client);
      } else {
        # read data
        $data = '';
        $rv   = $client->recv($data, POSIX::BUFSIZ, 0);
	#$log->debug("Data recieved (", length $data,") - ", decdump($data)) if $data;
        unless (defined($rv) && length $data) {
          # This would be the end of file, so close the client
          delete $inbuffer{$client};
          delete $outbuffer{$client};
          delete $ready{$client};

          $select->remove($client);
          close $client;
          next;
        }

        $inbuffer{$client} .= $data;

        while ($inbuffer{$client} =~ s/(.*$MSG_END)//) {
          push( @{$ready{$client}}, $1 );
        }
      }
    }

    # Any complete requests to process?
    foreach $client (keys %ready) {
      handle($client);
    }

    # Buffers to flush?
    foreach $client (keys %outbuffer) {
      # Skip this client if we have nothing to say
      next unless exists $outbuffer{$client};
      $log->debug("Sending message: ", decdump($outbuffer{$client}));

      $rv = $Sock->send($outbuffer{$client}, 0);
      unless (defined $rv) {
        # Whine, but move on.
        warn "I was told I could write, but I can't.\n";
        next;
      }
      if ($rv == length $outbuffer{$client} ||
      		$! == POSIX::EWOULDBLOCK) {
#        substr($outbuffer{$client}, 0, $rv) = '';
        delete $outbuffer{$client};
      } else {
        # Couldn't write all the data, and it wasn't because
        # it would have blocked.  Shutdown and move on.
        delete $inbuffer{$client};
        delete $outbuffer{$client};
        delete $ready{$client};

        $select->remove($client);
        close($client);
        next;
      }
    }

    # Out of band data?
    foreach $client ($select->has_exception(0)) {
    # Deal with out-of-band data here
    }
  }
}

# handle($socket) deals with all pending requests for $client
sub handle {
  # requests are in $ready{$client}
  # send output to $outbuffer{$client}
  my $client = shift;
  my $request;

  foreach $request (@{$ready{$client}}) {
    # $request is the text of the request
    # put text of reply into $outbuffer{$client}
#    $log->debug("REQUEST: ", decdump($request));

    my $i_start = index($request, $MSG_START);
    my $i_end = rindex($request, $MSG_END);

    my $buf = substr($request, 0, $i_end + 2, '');
    # Cut off incomplete message at the beginning if any
    $buf = substr($buf, $i_start, length($buf) - $i_start, '');

    my @msg = split(/$MSG_END/, $buf);
    foreach my $msg (@msg) {
      if ($msg =~ /^($TASS_PREFIX.+$TASS_SUFFIX)/) {
        $log->debug("new tass message".decdump($1));
        handle_tass($1,$client);
      } elsif ($msg eq $TASS_WAIT) {
        $log->debug("cmc waiting for announciator, rx WANN");
        handle_handshake($TASS_WAIT, $Sock);
      } elsif ($msg eq $TASS_RDY) {
        $log->debug("recieved ready from TASS");
      } else {
        $log->debug("unknown message format: ".decdump($msg));
        $log->debug("   wait message format: ".decdump($TASS_WAIT));
      }
    }

  } # end foreach $request
  delete $ready{$client};
}

sub handle_handshake {
  my ($root, $client) = shift;
  $log->debug("ACK cmc wait, tx YANN: ", decdump($TASS_YWAIT));
  $outbuffer{$client} = $TASS_YWAIT;
}

sub handle_tass {
  my ($root, $client) = @_;

  if (length($root) != $TASS_ROOT_LEN) {
    $log->debug("Invalid message root length: ".decdump($root));
    #$root = substr($root, 0, $TASS_ROOT_LEN);
  }

  my $bin_string = unpack("B*", $root);
  my @chars = unpack("C".$TASS_ROOT_LEN, $root);

  if (oct(sprintf("0b%s",substr ($bin_string, 66, 1 ))) != 0) {
    # message to be sent
    $log->debug("Found message to send ", decdump($root)," sending...");
    $outbuffer{$client} = $root;
  }

  my $ctx = NextCAM::CRC->new(type=>"crc8", poly=>0x107);
  # slice of data used to calc CRC
  foreach (@chars[3..10]) { $ctx->add(pack("C*",$_)) };
  my $crc = hex($ctx->hexdigest);
  my $msgCrc = oct(sprintf("0b%s",substr ($bin_string, 88, 8 )));

  if ($crc != $msgCrc) {
    $log->debug("Invalid message, crc does not match: ".$crc." != ".$msgCrc);
    return;
  }

  #my (undef,undef,undef,undef,$cmd, $id, undef, $port) = unpack("C".$MSG_ROOT_LEN, $root);

  my $cmd = oct(sprintf("0b%s",substr ($bin_string, 56, 5 )));
  #  $log->debug("CMD: $cmd (", sprintf("0b%s",substr ($bin_string, 56, 5 )), ")");
  my $id  = oct(sprintf("0b%s",substr ($bin_string, 61, 11)));
  #  $log->debug("ID: $id (",   sprintf("0b%s",substr ($bin_string, 61, 11 )), ")");
  my $port= oct(sprintf("0b%s",substr ($bin_string, 80, 8 )));
  #  $log->debug("PRT: $port (",sprintf("0b%s",substr ($bin_string, 80, 8  )), ")");


  $log->debug("Got CMD=$cmd from sensor ID=$id PORT=$port");
  # Find associated devices
  my $objid;
  for my $obj (keys %Devs) {
    if ($Devs{$obj}{HW_ID} == $id) {
      $objid = $obj;
      last;
    }
  }
  if (not $objid) { # Sensor isn't configured
    $log->debug("Device for sensor ID=$id isn't configured in the system");
  } else {
    if (not exists $CmdMap{$cmd}) {
      $log->debug("[OBJ=$objid;ID=$id] Ignored unknown command CMD=$cmd");
    } else {
      my $action = $CmdMap{$cmd};
      if ($action->{eventtype} ne 'ignore') {
        push @EventQueue, {
          objid => $objid,
          source => $EVENT_SOURCE,
          msg => $action->{msg},
          eventtype => $action->{eventtype}=~/^alert$/i ?
          $EVENT_TYPE_ALERT :
          $EVENT_TYPE_INFO,
          when => time
        };
      }
    }
  }
}

# nonblock($socket) puts socket into nonblocking mode
sub nonblock {
  my $socket = shift;
  my $flags;

  $flags = fcntl($socket, F_GETFL, 0)
  or die "Can't get flags for socket: $!\n";
  fcntl($socket, F_SETFL, $flags | O_NONBLOCK)
  or die "Can't make socket nonblocking: $!\n";
}

sub main
{
  eval {
    db_master;

    configure;

    gw_connect;
    local_connect;
  };
  if ($@) {
    $log->logdie("Failed to start daemon: $@");
  }

  event_loop;
}

main;


END {
  eval { $dbm->disconnect } if $dbm;
  eval { $Sock->close() } if $Sock;
  eval { $server->close() } if $server;
}
