/*
    event.c

    Generalised event handling for motion

    Copyright Jeroen Vreeken, 2002
    This software is distributed under the GNU Public License Version 2
    see also the file 'COPYING'.
*/

#include "ffmpeg.h"    /* must be first to avoid 'shadow' warning */
#include "picture.h"   /* already includes motion.h */
#include "event.h"
#if (!defined(BSD))
#include "video.h"
#endif

/* Various functions (most doing the actual action) */

const char *eventList[] = {
    "NULL",
    "EVENT_FILECREATE",
    "EVENT_MOTION",
    "EVENT_FIRSTMOTION",
    "EVENT_ENDMOTION",
    "EVENT_STOP",
    "EVENT_TIMELAPSE",
    "EVENT_TIMELAPSEEND",
    "EVENT_STREAM",
    "EVENT_IMAGE_DETECTED",
    "EVENT_IMAGEM_DETECTED",
    "EVENT_IMAGE_SNAPSHOT",
    "EVENT_IMAGE",
    "EVENT_IMAGEM",
    "EVENT_FILECLOSE",
    "EVENT_DEBUG",
    "EVENT_CRITICAL",
    "EVENT_AREA_DETECTED",
    "EVENT_CAMERA_LOST",
    "EVENT_FFMPEG_PUT",
    "EVENT_SDL_PUT",
    "EVENT_LAST"
};

/**
 * eventToString
 *
 * returns string label of the event
 */
 /**
 * Future use debug / notification function
static const char *eventToString(motion_event e)
{
    return eventList[(int)e];
}
*/

/**
 * exec_command
 *      Execute 'command' with 'arg' as its argument.
 *      if !arg command is started with no arguments
 *      Before we call execl we need to close all the file handles
 *      that the fork inherited from the parent in order not to pass
 *      the open handles on to the shell
 */
static void exec_command(struct context *cnt, char *command, char *filename, int filetype)
{
    char stamp[PATH_MAX];
    mystrftime(cnt, stamp, sizeof(stamp), command, &cnt->current_image->timestamp_tm, filename, filetype);

    if (!fork()) {
        int i;

        /* Detach from parent */
        setsid();

        /*
         * Close any file descriptor except console because we will
         * like to see error messages
         */
        for (i = getdtablesize(); i > 2; --i)
            close(i);

        execl("/bin/sh", "sh", "-c", stamp, " &", NULL);

        /* if above function succeeds the program never reach here */
        MOTION_LOG(ALR, TYPE_EVENTS, SHOW_ERRNO, "%s: Unable to start external command '%s'",
                   stamp);

        exit(1);
    }

    MOTION_LOG(DBG, TYPE_EVENTS, NO_ERRNO, "%s: Executing external command '%s'",
               stamp);
}

/*
 * Event handlers
 */

static void event_newfile(struct context *cnt ATTRIBUTE_UNUSED,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED, char *filename, void *ftype,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: File of type %ld saved to: %s",
               (unsigned long)ftype, filename);
}


static void event_beep(struct context *cnt, motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED,
            char *filename ATTRIBUTE_UNUSED,
            void *ftype ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (!cnt->conf.quiet)
        printf("\a");
}

/**
 * on_picture_save_command
 *      handles both on_picture_save and on_movie_start
 *      If arg = FTYPE_IMAGE_ANY on_picture_save script is executed
 *      If arg = FTYPE_MPEG_ANY on_movie_start script is executed
 *      The scripts are executed with the filename of picture or movie appended
 *      to the config parameter.
 */
static void on_picture_save_command(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED,
            char *filename, void *arg, struct tm *tm ATTRIBUTE_UNUSED)
{
    int filetype = (unsigned long)arg;

    if ((filetype & FTYPE_IMAGE_ANY) != 0 && cnt->conf.on_picture_save)
        exec_command(cnt, cnt->conf.on_picture_save, filename, filetype);

    if ((filetype & FTYPE_MPEG_ANY) != 0 && cnt->conf.on_movie_start)
        exec_command(cnt, cnt->conf.on_movie_start, filename, filetype);
}

static void on_motion_detected_command(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->conf.on_motion_detected)
        exec_command(cnt, cnt->conf.on_motion_detected, NULL, 0);
}

#if defined(HAVE_MYSQL) || defined(HAVE_PGSQL) || defined(HAVE_SQLITE3)

static void event_sqlnewfile(struct context *cnt,
            motion_event type  ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED,
            char *filename, void *arg, struct tm *tm ATTRIBUTE_UNUSED)
{
    int sqltype = (unsigned long)arg;

    /* Only log the file types we want */
    if (!(cnt->conf.database_type) || (sqltype & cnt->sql_mask) == 0)
        return;

    /*
     * We place the code in a block so we only spend time making space in memory
     * for the sqlquery and timestr when we actually need it.
     */
    {
        char sqlquery[PATH_MAX];

        mystrftime(cnt, sqlquery, sizeof(sqlquery), cnt->conf.sql_query,
                   &cnt->current_image->timestamp_tm, filename, sqltype);

#ifdef HAVE_MYSQL
        if (!strcmp(cnt->conf.database_type, "mysql")) {
            if (mysql_query(cnt->database, sqlquery) != 0) {
                int error_code = mysql_errno(cnt->database);

                MOTION_LOG(ERR, TYPE_DB, SHOW_ERRNO, "%s: Mysql query failed %s error code %d",
                           mysql_error(cnt->database), error_code);
                /* Try to reconnect ONCE if fails continue and discard this sql query */
                if (error_code >= 2000) {
                    // Close connection before start a new connection
                    mysql_close(cnt->database);

                    cnt->database = mymalloc(sizeof(MYSQL));
                    mysql_init(cnt->database);

                    if (!mysql_real_connect(cnt->database, cnt->conf.database_host,
                                            cnt->conf.database_user, cnt->conf.database_password,
                                            cnt->conf.database_dbname, 0, NULL, 0)) {
                        MOTION_LOG(ALR, TYPE_DB, NO_ERRNO, "%s: Cannot reconnect to MySQL"
                                   " database %s on host %s with user %s MySQL error was %s",
                                   cnt->conf.database_dbname,
                                   cnt->conf.database_host, cnt->conf.database_user,
                                   mysql_error(cnt->database));
                    } else {
                        MOTION_LOG(INF, TYPE_DB, NO_ERRNO, "%s: Re-Connection to Mysql database '%s' Succeed",
                                   cnt->conf.database_dbname);
                        if (mysql_query(cnt->database, sqlquery) != 0) {
                            int error_my = mysql_errno(cnt->database);
                            MOTION_LOG(ERR, TYPE_DB, SHOW_ERRNO, "%s: after re-connection Mysql query failed %s error code %d",
                                       mysql_error(cnt->database), error_my);
                        }
                    }
                }
            }
        }
#endif /* HAVE_MYSQL */

#ifdef HAVE_PGSQL
        if (!strcmp(cnt->conf.database_type, "postgresql")) {
            PGresult *res;

            res = PQexec(cnt->database_pg, sqlquery);

            if (PQstatus(cnt->database_pg) == CONNECTION_BAD) {

                MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: Connection to PostgreSQL database '%s' failed: %s",
                           cnt->conf.database_dbname, PQerrorMessage(cnt->database_pg));

                // This function will close the connection to the server and attempt to reestablish a new connection to the same server,
                // using all the same parameters previously used. This may be useful for error recovery if a working connection is lost
                PQreset(cnt->database_pg);

                if (PQstatus(cnt->database_pg) == CONNECTION_BAD) {
                    MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: Re-Connection to PostgreSQL database '%s' failed: %s",
                               cnt->conf.database_dbname, PQerrorMessage(cnt->database_pg));
                } else {
                    MOTION_LOG(INF, TYPE_DB, NO_ERRNO, "%s: Re-Connection to PostgreSQL database '%s' Succeed",
                               cnt->conf.database_dbname);
                }

            } else if (PQresultStatus(res) != PGRES_COMMAND_OK) {
                MOTION_LOG(ERR, TYPE_DB, SHOW_ERRNO, "%s: PGSQL query [%s] failed", sqlquery);
                PQclear(res);
            }
        }
#endif /* HAVE_PGSQL */

#ifdef HAVE_SQLITE3
        if ((!strcmp(cnt->conf.database_type, "sqlite3")) && (cnt->conf.database_dbname)) {
            int res;
            char *errmsg = 0;
            res = sqlite3_exec(cnt->database_sqlite3, sqlquery, NULL, 0, &errmsg);
            if (res != SQLITE_OK ) {
                MOTION_LOG(ERR, TYPE_DB, NO_ERRNO, "%s: SQLite error was %s",
                           errmsg);
                sqlite3_free(errmsg);
            }
        }
#endif /* HAVE_SQLITE3 */
    }
}

#endif /* defined HAVE_MYSQL || defined HAVE_PGSQL || defined(HAVE_SQLITE3) */

static void on_area_command(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->conf.on_area_detected)
        exec_command(cnt, cnt->conf.on_area_detected, NULL, 0);
}

static void on_event_start_command(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->conf.on_event_start)
        exec_command(cnt, cnt->conf.on_event_start, NULL, 0);
}

static void on_event_end_command(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->conf.on_event_end)
        exec_command(cnt, cnt->conf.on_event_end, NULL, 0);
}

static void event_stop_stream(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if ((cnt->conf.stream_port) && (cnt->stream.socket != -1))
        stream_stop(cnt);
}

static void event_stream_put(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->conf.stream_port)
        stream_put(cnt, img);
}

#ifdef HAVE_SDL
static void event_sdl_put(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *tm ATTRIBUTE_UNUSED)
{
    sdl_put(img, cnt->imgs.width, cnt->imgs.height);
}
#endif


#if defined(HAVE_LINUX_VIDEODEV_H) && !defined(WITHOUT_V4L) && !defined(BSD)
static void event_vid_putpipe(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy ATTRIBUTE_UNUSED, void *devpipe,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (*(int *)devpipe >= 0) {
        if (vid_putpipe(*(int *)devpipe, img, cnt->imgs.size) == -1)
            MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: Failed to put image into video pipe");
    }
}
#endif /* !WITHOUT_V4L && !BSD */

const char *imageext(struct context *cnt)
{
    if (cnt->imgs.picture_type == IMAGE_TYPE_PPM)
        return "ppm";

    return "jpg";
}

static void event_image_detect(struct context *cnt,
        motion_event type ATTRIBUTE_UNUSED,
        unsigned char *newimg, char *dummy1 ATTRIBUTE_UNUSED,
        void *dummy2 ATTRIBUTE_UNUSED, struct tm *currenttime_tm)
{
    char fullfilename[PATH_MAX];
    char filename[PATH_MAX];

    if (cnt->new_img & NEWIMG_ON) {
        const char *imagepath;

        /*
         *  conf.imagepath would normally be defined but if someone deleted it by control interface
         *  it is better to revert to the default than fail
         */
        if (cnt->conf.imagepath)
            imagepath = cnt->conf.imagepath;
        else
            imagepath = DEF_IMAGEPATH;

        mystrftime(cnt, filename, sizeof(filename), imagepath, currenttime_tm, NULL, 0);
        snprintf(fullfilename, PATH_MAX, "%s/%s.%s", cnt->conf.filepath, filename, imageext(cnt));

        put_picture(cnt, fullfilename, newimg, FTYPE_IMAGE);
    }
}

static void event_imagem_detect(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *newimg ATTRIBUTE_UNUSED, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *currenttime_tm)
{
    struct config *conf = &cnt->conf;
    char fullfilenamem[PATH_MAX];
    char filename[PATH_MAX];
    char filenamem[PATH_MAX];

    if (conf->motion_img) {
        const char *imagepath;

        /*
         *  conf.imagepath would normally be defined but if someone deleted it by control interface
         *  it is better to revert to the default than fail
         */
        if (cnt->conf.imagepath)
            imagepath = cnt->conf.imagepath;
        else
            imagepath = DEF_IMAGEPATH;

        mystrftime(cnt, filename, sizeof(filename), imagepath, currenttime_tm, NULL, 0);
        /* motion images gets same name as normal images plus an appended 'm' */
        snprintf(filenamem, PATH_MAX, "%sm", filename);
        snprintf(fullfilenamem, PATH_MAX, "%s/%s.%s", cnt->conf.filepath, filenamem, imageext(cnt));

        put_picture(cnt, fullfilenamem, cnt->imgs.out, FTYPE_IMAGE_MOTION);
    }
}

static void event_image_snapshot(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *currenttime_tm)
{
    char fullfilename[PATH_MAX];
    char filename[PATH_MAX];
    char filepath[PATH_MAX];
    int offset = 0;
    int len = strlen(cnt->conf.snappath);

    if (len >= 9)
        offset = len - 8;

    if (strcmp(cnt->conf.snappath+offset, "lastsnap")) {
        char linkpath[PATH_MAX];
        const char *snappath;
        /*
         *  conf.snappath would normally be defined but if someone deleted it by control interface
         *  it is better to revert to the default than fail
         */
        if (cnt->conf.snappath)
            snappath = cnt->conf.snappath;
        else
            snappath = DEF_SNAPPATH;

        mystrftime(cnt, filepath, sizeof(filepath), snappath, currenttime_tm, NULL, 0);
        snprintf(filename, PATH_MAX, "%s.%s", filepath, imageext(cnt));
        snprintf(fullfilename, PATH_MAX, "%s/%s", cnt->conf.filepath, filename);
        put_picture(cnt, fullfilename, img, FTYPE_IMAGE_SNAPSHOT);

        /*
         *  Update symbolic link *after* image has been written so that
         *  the link always points to a valid file.
         */
        snprintf(linkpath, PATH_MAX, "%s/lastsnap.%s", cnt->conf.filepath, imageext(cnt));
        remove(linkpath);

        if (symlink(filename, linkpath)) {
            MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: Could not create symbolic link [%s]",
                       filename);
            return;
        }
    } else {
        mystrftime(cnt, filepath, sizeof(filepath), cnt->conf.snappath, currenttime_tm, NULL, 0);
        snprintf(filename, PATH_MAX, "%s.%s", filepath, imageext(cnt));
        snprintf(fullfilename, PATH_MAX, "%s/%s", cnt->conf.filepath, filename);
        remove(fullfilename);
        put_picture(cnt, fullfilename, img, FTYPE_IMAGE_SNAPSHOT);
    }

    cnt->snapshot = 0;
}

static void event_camera_lost(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img ATTRIBUTE_UNUSED, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *currenttime_tm ATTRIBUTE_UNUSED)
{
    if (cnt->conf.on_camera_lost)
        exec_command(cnt, cnt->conf.on_camera_lost, NULL, 0);
}

static void on_movie_end_command(struct context *cnt,
                                 motion_event type ATTRIBUTE_UNUSED,
                                 unsigned char *dummy ATTRIBUTE_UNUSED, char *filename,
                                 void *arg, struct tm *tm ATTRIBUTE_UNUSED)
{
    int filetype = (unsigned long) arg;

    if ((filetype & FTYPE_MPEG_ANY) && cnt->conf.on_movie_end)
        exec_command(cnt, cnt->conf.on_movie_end, filename, filetype);
}

static void event_extpipe_end(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->extpipe_open) {
        cnt->extpipe_open = 0;
        fflush(cnt->extpipe);
        MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: CLOSING: extpipe file desc %d, error state %d",
                   fileno(cnt->extpipe), ferror(cnt->extpipe));
        MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: pclose return: %d",
                   pclose(cnt->extpipe));
        event(cnt, EVENT_FILECLOSE, NULL, cnt->extpipefilename, (void *)FTYPE_MPEG, NULL);
    }
}

static void event_create_extpipe(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *currenttime_tm)
{
    if ((cnt->conf.useextpipe) && (cnt->conf.extpipe)) {
        char stamp[PATH_MAX] = "";
        const char *moviepath;
        FILE *fd_dummy = NULL;

        /*
         *  conf.mpegpath would normally be defined but if someone deleted it by control interface
         *  it is better to revert to the default than fail
         */
        if (cnt->conf.moviepath) {
            moviepath = cnt->conf.moviepath;
        } else {
            moviepath = DEF_MOVIEPATH;
            MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: moviepath: %s",
                       moviepath);
        }

        mystrftime(cnt, stamp, sizeof(stamp), moviepath, currenttime_tm, NULL, 0);
        snprintf(cnt->extpipefilename, PATH_MAX - 4, "%s/%s", cnt->conf.filepath, stamp);

        /* Open a dummy file to check if path is correct */
        fd_dummy = myfopen(cnt->extpipefilename, "w");

        /* TODO: trigger some warning instead of only log an error message */
        if (fd_dummy == NULL) {
            /* Permission denied */
            if (errno ==  EACCES) {
                MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: error opening file %s ..."
                           "check access rights to target directory",
                           cnt->extpipefilename);
                return ;
            } else {
                MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: error opening file %s",
                           cnt->extpipefilename);
                return ;
            }

        }

        myfclose(fd_dummy);
        unlink(cnt->extpipefilename);

        mystrftime(cnt, stamp, sizeof(stamp), cnt->conf.extpipe, currenttime_tm, cnt->extpipefilename, 0);

        MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: pipe: %s",
                   stamp);
        MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: cnt->moviefps: %d",
                   cnt->movie_fps);

        event(cnt, EVENT_FILECREATE, NULL, cnt->extpipefilename, (void *)FTYPE_MPEG, NULL);
        cnt->extpipe = popen(stamp, "w");

        if (cnt->extpipe == NULL) {
            MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: popen failed");
            return;
        }

        setbuf(cnt->extpipe, NULL);
        cnt->extpipe_open = 1;
    }
}

static void event_extpipe_put(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *tm ATTRIBUTE_UNUSED)
{
    /* Check use_extpipe enabled and ext_pipe not NULL */
    if ((cnt->conf.useextpipe) && (cnt->extpipe != NULL)) {
        MOTION_LOG(DBG, TYPE_EVENTS, NO_ERRNO, "%s:");

        /* Check that is open */
        if ((cnt->extpipe_open) && (fileno(cnt->extpipe) > 0)) {
            if (!fwrite(img, cnt->imgs.size, 1, cnt->extpipe))
                MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: Error writing in pipe , state error %d",
                           ferror(cnt->extpipe));
        } else {
            MOTION_LOG(ERR, TYPE_EVENTS, NO_ERRNO, "%s: pipe %s not created or closed already ",
                       cnt->extpipe);
        }
    }
}


static void event_new_video(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy ATTRIBUTE_UNUSED, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *tm ATTRIBUTE_UNUSED)
{
    cnt->movie_last_shot = -1;

    cnt->movie_fps = cnt->lastrate;

    MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s Source FPS %d", cnt->movie_fps);

    if (cnt->movie_fps < 2) cnt->movie_fps = 2;
}

#ifdef HAVE_FFMPEG

static void grey2yuv420p(unsigned char *u, unsigned char *v, int width, int height)
{
    memset(u, 128, width * height / 4);
    memset(v, 128, width * height / 4);
}


static void event_ffmpeg_newfile(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *currenttime_tm)
{
    int width = cnt->imgs.width;
    int height = cnt->imgs.height;
    unsigned char *convbuf, *y, *u, *v;
    char stamp[PATH_MAX];
    const char *moviepath;
    const char *codec;
    long codenbr;

    if (!cnt->conf.ffmpeg_output && !cnt->conf.ffmpeg_output_debug)
        return;

    /*
     *  conf.mpegpath would normally be defined but if someone deleted it by control interface
     *  it is better to revert to the default than fail
     */
    if (cnt->conf.moviepath)
        moviepath = cnt->conf.moviepath;
    else
        moviepath = DEF_MOVIEPATH;

    mystrftime(cnt, stamp, sizeof(stamp), moviepath, currenttime_tm, NULL, 0);

    /*
     *  motion movies get the same name as normal movies plus an appended 'm'
     *  PATH_MAX - 4 to allow for .mpg to be appended without overflow
     */

     /* The following section allows for testing of all the various containers
      * that Motion permits. The container type is pre-pended to the name of the
      * file so that we can determine which container type created what movie.
      * The intent for this is be used for developer testing when the ffmpeg libs
      * change or the code inside our ffmpeg module changes.  For each event, the
      * container type will change.  This way, you can turn on emulate motion, then
      * specify a maximum movie time and let Motion run for days creating all the
      * different types of movies checking for crashes, warnings, etc.
     */
    codec = cnt->conf.ffmpeg_video_codec;
    if (strcmp(codec, "ogg") == 0) {
        MOTION_LOG(WRN, TYPE_ENCODER, NO_ERRNO, "%s The ogg container is no longer supported.  Changing to mpeg4");
        codec = "mpeg4";
    }
    if (strcmp(codec, "test") == 0) {
        MOTION_LOG(NTC, TYPE_ENCODER, NO_ERRNO, "%s Running test of the various output formats.");
        codenbr = cnt->event_nr % 10;
        switch (codenbr) {
        case 1:
            codec = "mpeg4";
            break;
        case 2:
            codec = "msmpeg4";
            break;
        case 3:
            codec = "swf";
            break;
        case 4:
            codec = "flv";
            break;
        case 5:
            codec = "ffv1";
            break;
        case 6:
            codec = "mov";
            break;
        case 7:
            codec = "mp4";
            break;
        case 8:
            codec = "mkv";
            break;
        case 9:
            codec = "hevc";
            break;
        default:
            codec = "msmpeg4";
            break;
        }
        snprintf(cnt->motionfilename, PATH_MAX - 4, "%s/%s_%sm", cnt->conf.filepath, codec, stamp);
        snprintf(cnt->newfilename, PATH_MAX - 4, "%s/%s_%s", cnt->conf.filepath, codec, stamp);
    } else {
        snprintf(cnt->motionfilename, PATH_MAX - 4, "%s/%sm", cnt->conf.filepath, stamp);
        snprintf(cnt->newfilename, PATH_MAX - 4, "%s/%s", cnt->conf.filepath, stamp);
    }
    if (cnt->conf.ffmpeg_output) {
        if (cnt->imgs.type == VIDEO_PALETTE_GREY) {
            convbuf = mymalloc((width * height) / 2);
            y = img;
            u = convbuf;
            v = convbuf + (width * height) / 4;
            grey2yuv420p(u, v, width, height);
        } else {
            convbuf = NULL;
            y = img;
            u = img + width * height;
            v = u + (width * height) / 4;
        }

        if ((cnt->ffmpeg_output =
            ffmpeg_open(codec, cnt->newfilename, y, u, v,
                         cnt->imgs.width, cnt->imgs.height, cnt->movie_fps, cnt->conf.ffmpeg_bps,
                         cnt->conf.ffmpeg_vbr,TIMELAPSE_NONE)) == NULL) {
            MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: ffopen_open error creating (new) file [%s]",
                       cnt->newfilename);
            cnt->finish = 1;
            return;
        }

        ((struct ffmpeg *)cnt->ffmpeg_output)->udata = convbuf;
        event(cnt, EVENT_FILECREATE, NULL, cnt->newfilename, (void *)FTYPE_MPEG, NULL);
    }

    if (cnt->conf.ffmpeg_output_debug) {
        if (cnt->imgs.type == VIDEO_PALETTE_GREY) {
            convbuf = mymalloc((width * height) / 2);
            y = cnt->imgs.out;
            u = convbuf;
            v = convbuf + (width * height) / 4;
            grey2yuv420p(u, v, width, height);
        } else {
            y = cnt->imgs.out;
            u = cnt->imgs.out + width *height;
            v = u + (width * height) / 4;
            convbuf = NULL;
        }

        if ((cnt->ffmpeg_output_debug =
            ffmpeg_open(codec, cnt->motionfilename, y, u, v,
                        cnt->imgs.width, cnt->imgs.height, cnt->movie_fps, cnt->conf.ffmpeg_bps,
                        cnt->conf.ffmpeg_vbr,TIMELAPSE_NONE)) == NULL) {
            MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: ffopen_open error creating (motion) file [%s]",
                       cnt->motionfilename);
            cnt->finish = 1;
            return;
        }

        cnt->ffmpeg_output_debug->udata = convbuf;
        event(cnt, EVENT_FILECREATE, NULL, cnt->motionfilename, (void *)FTYPE_MPEG_MOTION, NULL);
    }
}

static void event_ffmpeg_timelapse(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED, unsigned char *img,
            char *dummy1 ATTRIBUTE_UNUSED, void *dummy2 ATTRIBUTE_UNUSED,
            struct tm *currenttime_tm)
{
    int width = cnt->imgs.width;
    int height = cnt->imgs.height;
    unsigned char *convbuf, *y, *u, *v;

    if (!cnt->ffmpeg_timelapse) {
        char tmp[PATH_MAX];
        const char *timepath;
        const char *codec_mpg = "mpg";
        const char *codec_mpeg = "mpeg4";

        /*
         *  conf.timepath would normally be defined but if someone deleted it by control interface
         *  it is better to revert to the default than fail
         */
        if (cnt->conf.timepath)
            timepath = cnt->conf.timepath;
        else
            timepath = DEF_TIMEPATH;

        mystrftime(cnt, tmp, sizeof(tmp), timepath, currenttime_tm, NULL, 0);

        /* PATH_MAX - 4 to allow for .mpg to be appended without overflow */
        snprintf(cnt->timelapsefilename, PATH_MAX - 4, "%s/%s", cnt->conf.filepath, tmp);

        if (cnt->imgs.type == VIDEO_PALETTE_GREY) {
            convbuf = mymalloc((width * height) / 2);
            y = img;
            u = convbuf;
            v = convbuf + (width * height) / 4;
            grey2yuv420p(u, v, width, height);
        } else {
            convbuf = NULL;
            y = img;
            u = img + width * height;
            v = u + (width * height) / 4;
        }


        if ((strcmp(cnt->conf.ffmpeg_video_codec,"mpg") == 0) ||
            (strcmp(cnt->conf.ffmpeg_video_codec,"swf") == 0) ){

            if (strcmp(cnt->conf.ffmpeg_video_codec,"swf") == 0) {
                MOTION_LOG(WRN, TYPE_EVENTS, NO_ERRNO, "%s: The swf container for timelapse no longer supported.  Using mpg container.");
            }

            MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: Timelapse using mpg codec.");
            MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: Events will be appended to file");
            cnt->ffmpeg_timelapse =
                ffmpeg_open(codec_mpg,cnt->timelapsefilename, y, u, v
                        ,cnt->imgs.width, cnt->imgs.height, cnt->conf.frame_limit
                        ,cnt->conf.ffmpeg_bps,cnt->conf.ffmpeg_vbr,TIMELAPSE_APPEND);
        } else {
            MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: Timelapse using mpeg4 codec.");
            MOTION_LOG(NTC, TYPE_EVENTS, NO_ERRNO, "%s: Events will be trigger new files");
            cnt->ffmpeg_timelapse =
                ffmpeg_open(codec_mpeg ,cnt->timelapsefilename, y, u, v
                        ,cnt->imgs.width, cnt->imgs.height, cnt->conf.frame_limit
                        ,cnt->conf.ffmpeg_bps,cnt->conf.ffmpeg_vbr,TIMELAPSE_NEW);
        }

        if (cnt->ffmpeg_timelapse == NULL){
            MOTION_LOG(ERR, TYPE_EVENTS, SHOW_ERRNO, "%s: ffopen_open error creating "
                       "(timelapse) file [%s]", cnt->timelapsefilename);
            cnt->finish = 1;
            return;
        }

        cnt->ffmpeg_timelapse->udata = convbuf;
        event(cnt, EVENT_FILECREATE, NULL, cnt->timelapsefilename, (void *)FTYPE_MPEG_TIMELAPSE, NULL);
    }

    y = img;

    if (cnt->imgs.type == VIDEO_PALETTE_GREY)
        u = cnt->ffmpeg_timelapse->udata;
    else
        u = img + width * height;

    v = u + (width * height) / 4;

    if (ffmpeg_put_other_image(cnt->ffmpeg_timelapse, y, u, v) == -1) {
        cnt->finish = 1;
        cnt->restart = 0;
    }

}

static void event_ffmpeg_put(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *img, char *dummy1 ATTRIBUTE_UNUSED,
            void *dummy2 ATTRIBUTE_UNUSED, struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->ffmpeg_output) {
        int width = cnt->imgs.width;
        int height = cnt->imgs.height;
        unsigned char *y = img;
        unsigned char *u, *v;

        if (cnt->imgs.type == VIDEO_PALETTE_GREY)
            u = cnt->ffmpeg_output->udata;
        else
            u = y + (width * height);

        v = u + (width * height) / 4;

        if (ffmpeg_put_other_image(cnt->ffmpeg_output, y, u, v) == -1) {
            cnt->finish = 1;
            cnt->restart = 0;
        }
    }

    if (cnt->ffmpeg_output_debug) {
        if (ffmpeg_put_image(cnt->ffmpeg_output_debug) == -1) {
            cnt->finish = 1;
            cnt->restart = 0;
        }
    }
}

static void event_ffmpeg_closefile(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{

    if (cnt->ffmpeg_output) {
        free(cnt->ffmpeg_output->udata);

        ffmpeg_close(cnt->ffmpeg_output);
        cnt->ffmpeg_output = NULL;

        event(cnt, EVENT_FILECLOSE, NULL, cnt->newfilename, (void *)FTYPE_MPEG, NULL);
    }

    if (cnt->ffmpeg_output_debug) {
        free(cnt->ffmpeg_output_debug->udata);

        ffmpeg_close(cnt->ffmpeg_output_debug);
        cnt->ffmpeg_output_debug = NULL;

        event(cnt, EVENT_FILECLOSE, NULL, cnt->motionfilename, (void *)FTYPE_MPEG_MOTION, NULL);
    }
}

static void event_ffmpeg_timelapseend(struct context *cnt,
            motion_event type ATTRIBUTE_UNUSED,
            unsigned char *dummy1 ATTRIBUTE_UNUSED,
            char *dummy2 ATTRIBUTE_UNUSED, void *dummy3 ATTRIBUTE_UNUSED,
            struct tm *tm ATTRIBUTE_UNUSED)
{
    if (cnt->ffmpeg_timelapse) {
        free(cnt->ffmpeg_timelapse->udata);

        ffmpeg_close(cnt->ffmpeg_timelapse);
        cnt->ffmpeg_timelapse = NULL;

        event(cnt, EVENT_FILECLOSE, NULL, cnt->timelapsefilename, (void *)FTYPE_MPEG_TIMELAPSE, NULL);
    }
}

#endif /* HAVE_FFMPEG */


/*
 * Starting point for all events
 */

struct event_handlers {
    motion_event type;
    event_handler handler;
};

struct event_handlers event_handlers[] = {
#if defined(HAVE_MYSQL) || defined(HAVE_PGSQL) || defined(HAVE_SQLITE3)
    {
    EVENT_FILECREATE,
    event_sqlnewfile
    },
#endif
    {
    EVENT_FILECREATE,
    on_picture_save_command
    },
    {
    EVENT_FILECREATE,
    event_newfile
    },
    {
    EVENT_MOTION,
    event_beep
    },
    {
    EVENT_MOTION,
    on_motion_detected_command
    },
    {
    EVENT_AREA_DETECTED,
    on_area_command
    },
    {
    EVENT_FIRSTMOTION,
    on_event_start_command
    },
    {
    EVENT_ENDMOTION,
    on_event_end_command
    },
    {
    EVENT_IMAGE_DETECTED,
    event_image_detect
    },
    {
    EVENT_IMAGEM_DETECTED,
    event_imagem_detect
    },
    {
    EVENT_IMAGE_SNAPSHOT,
    event_image_snapshot
    },
#ifdef HAVE_SDL
    {
    EVENT_SDL_PUT,
    event_sdl_put
    },
#endif
#if defined(HAVE_LINUX_VIDEODEV_H) && !defined(WITHOUT_V4L) && !defined(BSD)
    {
    EVENT_IMAGE,
    event_vid_putpipe
    },
    {
    EVENT_IMAGEM,
    event_vid_putpipe
    },
#endif /* !WITHOUT_V4L && !BSD */
    {
    EVENT_STREAM,
    event_stream_put
    },
    {
    EVENT_FIRSTMOTION,
    event_new_video
    },
#ifdef HAVE_FFMPEG
    {
    EVENT_FIRSTMOTION,
    event_ffmpeg_newfile
    },
    {
    EVENT_IMAGE_DETECTED,
    event_ffmpeg_put
    },
    {
    EVENT_FFMPEG_PUT,
    event_ffmpeg_put
    },
    {
    EVENT_ENDMOTION,
    event_ffmpeg_closefile
    },
    {
    EVENT_TIMELAPSE,
    event_ffmpeg_timelapse
    },
    {
    EVENT_TIMELAPSEEND,
    event_ffmpeg_timelapseend
    },
#endif /* HAVE_FFMPEG */
    {
    EVENT_FILECLOSE,
    on_movie_end_command
    },
    {
    EVENT_FIRSTMOTION,
    event_create_extpipe
    },
    {
    EVENT_IMAGE_DETECTED,
    event_extpipe_put
    },
    {
    EVENT_FFMPEG_PUT,
    event_extpipe_put
    },
    {
    EVENT_ENDMOTION,
    event_extpipe_end
    },
    {
    EVENT_CAMERA_LOST,
    event_camera_lost
    },
    {
    EVENT_STOP,
    event_stop_stream
    },
    {0, NULL}
};


/**
 * event
 *   defined with the following parameters:
 *      - Type as defined in event.h (EVENT_...)
 *      - The global context struct cnt
 *      - image - A pointer to unsigned char as used for images
 *      - filename - A pointer to typically a string for a file path
 *      - eventdata - A void pointer that can be cast to anything. E.g. FTYPE_...
 *      - tm - A tm struct that carries a full time structure
 * The split between unsigned images and signed filenames was introduced in 3.2.2
 * as a code reading friendly solution to avoid a stream of compiler warnings in gcc 4.0.
 */
void event(struct context *cnt, motion_event type, unsigned char *image,
           char *filename, void *eventdata, struct tm *tm)
{
    int i=-1;

    while (event_handlers[++i].handler) {
        if (type == event_handlers[i].type)
            event_handlers[i].handler(cnt, type, image, filename, eventdata,
                                      tm);
    }
}
