/* bluebox distributor
 * version 0.3.2 date 2007-07-18
 * Copyright (C) 2006-2007 Stefan Schuermans <stefan@blinkenarea.org>
 * Copyleft: GNU public license V2.0 - http://www.gnu.org/copyleft/gpl.html
 * a BlinkenArea project - http://www.blinkenarea.org/
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <netinet/in.h>

#include "protocols.h"
#include "bd_config.h"
#include "bd_fmt.h"

//global variables
int end = 0; //set to 1 by signal handler to indicate end of program

//signal handler to end program
static void end_signal( int sig_no )
{
  //end program
  end = 1;
  //keep compiler happy
  (void)sig_no;
}

//get current number of milliseconds (warps around)
static unsigned int get_ms( )
{
  struct timeval tv;
  gettimeofday( &tv, NULL );
  return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

//main program
int main( int arg_cnt, char * * args )
{
  char * p_config_file;
  unsigned int in_buffer_len, out_buffer_len, turn_off_buffer_len;
  unsigned char * p_in_buffer, * * p_out_buffers, * * p_turn_off_buffers;
  char * p_device_usage;
  st_bd_fmt * p_bd_fmt;
  int in_sock_fd, out_sock_fd;
  struct sockaddr_in addr;
  fd_set fdset;
  unsigned int last_recv, cur;
  struct timeval timeout;
  unsigned short dev_no, ser_no, pix_no, suffix_no;
  int cnt, len, i, y, x;
  unsigned int addr_len;
  unsigned char * ptr;
  in_addr_t in_addr;
  int timeout_detected;

  //print message
  printf( "\n"
          "bluebox distributor\n"
          "version 0.3.2 date 2007-07-18\n"
          "Copyright (C) 2006-2007 Stefan Schuermans <stefan@blinkenarea.org>\n"
          "Copyleft: GNU public license V2.0 - http://www.gnu.org/copyleft/gpl.html\n"
          "a BlinkenArea project - http://www.blinkenarea.org/\n"
          "\n" );

  //no config file supplied
  if( arg_cnt < 2 )
  {
    fprintf( stderr, "no config file supplied (syntax: \"%s <config-file>\"), using \"blue_dist.conf\"\n", args[0] );
    p_config_file = "blue_dist.conf";
  }
  //config file supplied
  else
    p_config_file = args[1];

  //read config file and get settings
  bd_config_get( p_config_file );
  printf( "configuration read: %d devices with %d serial ports with %d pixels\n",
          bd_out_dev_cnt, bd_out_ser_cnt, bd_out_pix_cnt );

  //allocate input buffer
  in_buffer_len = 65536; //maximum size of a datagram
  p_in_buffer = (unsigned char *)malloc( in_buffer_len );
  if( p_in_buffer == NULL )
  {
    fprintf( stderr, "could not allocate buffer (input) of size %d\n\n", in_buffer_len );
    return -1;
  }

  //allocate output buffers
  out_buffer_len = (1 + bd_out_pix_cnt + bd_out_suffix_cnt) * bd_out_ser_cnt; //length of output buffer for 1 device
  len = bd_out_dev_cnt * sizeof( unsigned char * ) //dev_cnt pointers to 1D arrays
      + bd_out_dev_cnt * out_buffer_len; //dev_cnt output buffers
  p_out_buffers = (unsigned char * *)malloc( len );
  if( p_out_buffers == NULL )
  {
    free( p_in_buffer );
    fprintf( stderr, "could not allocate buffer (output) of size %d\n\n", len );
    return -1;
  }
  //initialize output buffer array structure
  ptr = (unsigned char *)p_out_buffers + bd_out_dev_cnt * sizeof( unsigned char * ); //array of pointers to 1D arrays
  for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ ) //output buffers
  {
    p_out_buffers[dev_no] = ptr;
    ptr += out_buffer_len;
  }
  //initialize data in output buffers
  for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ ) {
    i = 0;
    for( ser_no = 0; ser_no < bd_out_ser_cnt; ser_no++ ) //command byte
      p_out_buffers[dev_no][i++] = bd_out_command;
    for( pix_no = 0; pix_no < bd_out_pix_cnt; pix_no++ ) //pixel values
      for( ser_no = 0; ser_no < bd_out_ser_cnt; ser_no++ )
        p_out_buffers[dev_no][i++] = 0x00;
    for( suffix_no = 0; suffix_no < bd_out_suffix_cnt; suffix_no++ ) //suffix bytes
      for( ser_no = 0; ser_no < bd_out_ser_cnt; ser_no++ )
        p_out_buffers[dev_no][i++] = bd_out_suffix;
  }

  //allocate turn off buffers
  turn_off_buffer_len = (1 + bd_out_suffix_cnt) * bd_out_ser_cnt; // length of turn off buffer for 1 device
  len = bd_out_dev_cnt * sizeof( unsigned char * ) //dev_cnt pointers to 1D arrays
      + bd_out_dev_cnt * turn_off_buffer_len; //dev_cnt turn off buffers
  p_turn_off_buffers = (unsigned char * *)malloc( len );
  if( p_turn_off_buffers == NULL )
  {
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not allocate buffer (turn off) of size %d\n\n", len );
    return -1;
  }
  //initialize turn off buffer array structure
  ptr = (unsigned char *)p_turn_off_buffers + bd_out_dev_cnt * sizeof( unsigned char * ); //array of pointers to 1D arrays
  for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ ) //turn off buffers
  {
    p_turn_off_buffers[dev_no] = ptr;
    ptr += turn_off_buffer_len;
  }
  //initialize data in turn off buffers
  for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ ) {
    i = 0;
    for( ser_no = 0; ser_no < bd_out_ser_cnt; ser_no++ )
      p_turn_off_buffers[dev_no][i++] = bd_out_turn_off; //turn off byte
    for( suffix_no = 0; suffix_no < bd_out_suffix_cnt; suffix_no++ ) //suffix bytes
      for( ser_no = 0; ser_no < bd_out_ser_cnt; ser_no++ )
        p_turn_off_buffers[dev_no][i++] = bd_out_suffix;
  }

  //generate format from format file
  p_bd_fmt = bd_fmt_load( bd_fmt_file );
  if( p_bd_fmt == NULL )
  {
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not create format from \"%s\"\n\n", bd_fmt_file );
    return -1;
  }
  printf( "format file read: %dx%d pixels\n", p_bd_fmt->width, p_bd_fmt->height );

  //get used devices from format
  len = bd_out_dev_cnt * sizeof( char );
  p_device_usage = (char *)malloc( len );
  if( p_device_usage == NULL )
  {
    bd_fmt_free( p_bd_fmt );
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not allocate buffer (used output devices) of size %d\n\n", len );
    return -1;
  }
  for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ ) //default: device not used
    p_device_usage[dev_no] = 0;
  for( y = 0; y < p_bd_fmt->height; y++ ) //get used devices
    for( x = 0; x < p_bd_fmt->width; x++ )
      p_device_usage[p_bd_fmt->pixels[y][x].dev_no] = 1;
  //show used devices
  printf( "used devices:" );
  x = -1;
  y = -1;
  for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ )
  {
    if( p_device_usage[dev_no] )
    {
      if( y < 0 )
      {
        printf( "%s %d", x >= 0 ? "," : "", dev_no );
        x = dev_no;
      }
      y = dev_no;
    }
    else
    {
      if( y >= 0 && y != x )
        printf( "..%d", y );
      y = -1;
    }
  }
  if( y >= 0 && y != x )
     printf( "..%d", y );
  printf( "\n" );

  //create input socket
  in_sock_fd = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
  if( in_sock_fd == -1 )
  {
    free( p_device_usage );
    bd_fmt_free( p_bd_fmt );
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not create input socket: error: %s\n\n", strerror( errno ) );
    return -1;
  }

  //bind input socket
  addr.sin_family = AF_INET;
  addr.sin_port = htons( bd_in_listen_port );
  addr.sin_addr.s_addr = bd_in_listen_addr;
  if( bind( in_sock_fd, (struct sockaddr *)&addr, sizeof( addr ) ) == -1 )
  {
    close( in_sock_fd );
    free( p_device_usage );
    bd_fmt_free( p_bd_fmt );
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not bind input socket to \"%d.%d.%d.%d:%d\" (udp): error: %s\n\n",
      ((unsigned char *)&addr.sin_addr.s_addr)[0],
      ((unsigned char *)&addr.sin_addr.s_addr)[1],
      ((unsigned char *)&addr.sin_addr.s_addr)[2],
      ((unsigned char *)&addr.sin_addr.s_addr)[3],
      ntohs( addr.sin_port ),
      strerror( errno ) );
    return -1;
  }
  printf( "input socket listening on \"%d.%d.%d.%d:%d\" (udp)\n",
    ((unsigned char *)&addr.sin_addr.s_addr)[0],
    ((unsigned char *)&addr.sin_addr.s_addr)[1],
    ((unsigned char *)&addr.sin_addr.s_addr)[2],
    ((unsigned char *)&addr.sin_addr.s_addr)[3],
    ntohs( addr.sin_port ) );

  //create output socket
  out_sock_fd = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
  if( out_sock_fd == -1 )
  {
    close( in_sock_fd );
    free( p_device_usage );
    bd_fmt_free( p_bd_fmt );
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not create output socket: error: %s\n\n", strerror( errno ) );
    return -1;
  }

  //unblock output socket
  i = 1;
  if( ioctl( out_sock_fd, FIONBIO, &i ) == -1 )
  {
    close( out_sock_fd );
    close( in_sock_fd );
    free( p_device_usage );
    bd_fmt_free( p_bd_fmt );
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not unblock output socket: error: %s\n\n", strerror( errno ) );
    return -1;
  }

  //bind output socket
  addr.sin_family = AF_INET;
  addr.sin_port = htons( bd_out_bind_port );
  addr.sin_addr.s_addr = bd_out_bind_addr;
  if( bind( out_sock_fd, (struct sockaddr *)&addr, sizeof( addr ) ) == -1 )
  {
    close( out_sock_fd );
    close( in_sock_fd );
    free( p_device_usage );
    bd_fmt_free( p_bd_fmt );
    free( p_turn_off_buffers );
    free( p_out_buffers );
    free( p_in_buffer );
    fprintf( stderr, "could not bind output socket to \"%d.%d.%d.%d:%d\" (udp): error: %s\n\n",
      ((unsigned char *)&addr.sin_addr.s_addr)[0],
      ((unsigned char *)&addr.sin_addr.s_addr)[1],
      ((unsigned char *)&addr.sin_addr.s_addr)[2],
      ((unsigned char *)&addr.sin_addr.s_addr)[3],
      ntohs( addr.sin_port ),
      strerror( errno ) );
    return -1;
  }
  printf( "output socket bound to \"%d.%d.%d.%d:%d\" (udp)\n",
    ((unsigned char *)&addr.sin_addr.s_addr)[0],
    ((unsigned char *)&addr.sin_addr.s_addr)[1],
    ((unsigned char *)&addr.sin_addr.s_addr)[2],
    ((unsigned char *)&addr.sin_addr.s_addr)[3],
    ntohs( addr.sin_port ) );

  //initialize mapping from gayscale values to PWM values
  printf( "grayscale value to PWM value mapping:\n"
          "  base=%f\n"
          "  spread=%f\n"
          "  gamma=%f\n",
          bd_map_base, bd_map_spread, bd_map_gamma );
  gray2pwm_init( bd_map_base, bd_map_spread, bd_map_gamma );

  //install signal handler to end program
  signal( SIGTERM, end_signal );
  signal( SIGINT, end_signal );
  signal( SIGHUP, end_signal );

  //main loop
  last_recv = get_ms( ) - bd_in_timeout * 1000;
  timeout_detected = 1;
  while( ! end )
  {
    cur = get_ms( );
    unsigned int wait_time = cur - last_recv;

    //detect timeout
    if( wait_time >= bd_in_timeout * 1000 )
      timeout_detected = 1;
    
    //detect elapsed timeout interval
    if( timeout_detected && wait_time >= bd_in_timeout_interval * 1000 ) {
      //remember this time as time of last reception
      last_recv = cur;
      //send turn off commands using output socket
      if( bd_verbose )
        printf( "outputting turn off data to %d devices (length=%d)\n", bd_out_dev_cnt, turn_off_buffer_len );
      in_addr = bd_out_ip_base;
      for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ )
      {
        //send only to used devices
        if( p_device_usage[dev_no] )
        {
          //assemble destination address
          addr.sin_family = AF_INET;
          addr.sin_port = htons( bd_out_dest_port );
          addr.sin_addr.s_addr = in_addr;
          //send
          if( sendto( out_sock_fd, p_turn_off_buffers[dev_no], turn_off_buffer_len, 0,
                      (struct sockaddr *)&addr, sizeof( addr ) ) != (int)turn_off_buffer_len )
          {
            if( bd_verbose )
              fprintf( stderr, "could not send to \"%d.%d.%d.%d:%d\" (udp) (length=%d): error: %s\n",
                ((unsigned char *)&addr.sin_addr.s_addr)[0],
                ((unsigned char *)&addr.sin_addr.s_addr)[1],
                ((unsigned char *)&addr.sin_addr.s_addr)[2],
                ((unsigned char *)&addr.sin_addr.s_addr)[3],
                ntohs( addr.sin_port ),
                turn_off_buffer_len,
                strerror( errno ) );
          }
        }  //if( p_device_usage[dev_no] )
        //calculate IP address of next device
        in_addr += bd_out_ip_step;
      } //for( dev_no ...
    } //if( timeout_detected && wait_time >= bd_in_timeout_interval * 1000 )

    //wait for reception of a datagram
    FD_ZERO( &fdset );
    FD_SET( in_sock_fd, &fdset );
    timeout.tv_sec = 0;
    unsigned int wait_time_x = cur - last_recv;
    if( wait_time_x >= bd_in_timeout * 1000 || (timeout_detected && wait_time_x >= bd_in_timeout_interval * 1000) )
      timeout.tv_usec = 0;
    else
    {
      unsigned int timeout_ms_to = bd_in_timeout * 1000 - wait_time_x;
      unsigned int timeout_ms_to_i = bd_in_timeout_interval * 1000 - wait_time_x;
      unsigned int timeout_ms = timeout_ms_to < timeout_ms_to_i ? timeout_ms_to : timeout_ms_to_i;
      timeout.tv_usec = timeout_ms < 100 ? timeout_ms * 1000 : 100000;
    }
    cnt = select( in_sock_fd + 1, &fdset, NULL, NULL, &timeout );
    if( cnt == -1 && errno != EINTR )
    {
      fprintf( stderr, "select returned error: %s\n\n", strerror( errno ) );
      break;
    }
    //reception of a datagram
    if( cnt > 0 && FD_ISSET( in_sock_fd, &fdset ) )
    {
      //get datagram
      addr_len = sizeof( addr );
      len = recvfrom( in_sock_fd, p_in_buffer, in_buffer_len, 0,
                      (struct sockaddr *)&addr, &addr_len );
      if( len >= 0 )
      {
        if( bd_verbose )
          printf( "reception from \"%d.%d.%d.%d:%d\" (udp) (length=%d)\n",
            ((unsigned char *)&addr.sin_addr.s_addr)[0],
            ((unsigned char *)&addr.sin_addr.s_addr)[1],
            ((unsigned char *)&addr.sin_addr.s_addr)[2],
            ((unsigned char *)&addr.sin_addr.s_addr)[3],
            ntohs( addr.sin_port ),
            len );
        //remember time of last reception
        last_recv = get_ms( );
        // no timeout
        timeout_detected = 0;
        //processs datagram
        if( proto_datagram( p_in_buffer, (unsigned int)len, p_bd_fmt, p_out_buffers ) )
        {
          //send output data using output socket
          if( bd_verbose )
            printf( "outputting pixel data to %d devices (length=%d)\n", bd_out_dev_cnt, out_buffer_len );
          in_addr = bd_out_ip_base;
          for( dev_no = 0; dev_no < bd_out_dev_cnt; dev_no++ )
          {
            //send only to used devices
            if( p_device_usage[dev_no] )
            {
              //assemble destination address
              addr.sin_family = AF_INET;
              addr.sin_port = htons( bd_out_dest_port );
              addr.sin_addr.s_addr = in_addr;
              //send
              if( sendto( out_sock_fd, p_out_buffers[dev_no], out_buffer_len, 0,
                          (struct sockaddr *)&addr, sizeof( addr ) ) != (int)out_buffer_len )
              {
                if( bd_verbose )
                  fprintf( stderr, "could not send to \"%d.%d.%d.%d:%d\" (udp) (length=%d): error: %s\n",
                    ((unsigned char *)&addr.sin_addr.s_addr)[0],
                    ((unsigned char *)&addr.sin_addr.s_addr)[1],
                    ((unsigned char *)&addr.sin_addr.s_addr)[2],
                    ((unsigned char *)&addr.sin_addr.s_addr)[3],
                    ntohs( addr.sin_port ),
                    out_buffer_len,
                    strerror( errno ) );
              }
            }  //if( p_device_usage[dev_no] )
            //calculate IP address of next device
            in_addr += bd_out_ip_step;
          } //for( dev_no ...
        } //if( proto_datagram( ...
      } //if( len ...
    } // if( cnt > 0 ...
  } //while( ! end )
  printf( "terminated\n\n" );

  //clean up
  close( out_sock_fd );
  close( in_sock_fd );
  free( p_device_usage );
  bd_fmt_free( p_bd_fmt );
  free( p_turn_off_buffers );
  free( p_out_buffers );
  free( p_in_buffer );

  return 0;
}
