/**
 * collectd - src/utils_tail.c
 * Copyright (C) 2007-2008  C-Ware, Inc.
 * Copyright (C) 2008       Florian Forster
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * Author:
 *   Luke Heberling <lukeh at c-ware.com>
 *   Florian Forster <octo at collectd.org>
 *
 * Description:
 *   Encapsulates useful code for plugins which must watch for appends to
 *   the end of a file.
 **/

#include "collectd.h"

#include "utils/common/common.h"
#include "utils/tail/tail.h"

struct cu_tail_s {
  char *file;
  FILE *fh;
  struct stat stat;
};

static int cu_tail_reopen(cu_tail_t *obj, bool force_rewind) {
  int seek_end = 0;
  struct stat stat_buf = {0};

  int status = stat(obj->file, &stat_buf);
  if (status != 0) {
    P_ERROR("utils_tail: stat (%s) failed: %s", obj->file, STRERRNO);
    return -1;
  }

  /* The file is already open.. */
  if ((obj->fh != NULL) && (stat_buf.st_ino == obj->stat.st_ino)) {
    /* Seek to the beginning if file was truncated */
    if (stat_buf.st_size < obj->stat.st_size) {
      P_INFO("utils_tail: File `%s' was truncated.", obj->file);
      status = fseek(obj->fh, 0, SEEK_SET);
      if (status != 0) {
        P_ERROR("utils_tail: fseek (%s) failed: %s", obj->file, STRERRNO);
        fclose(obj->fh);
        obj->fh = NULL;
        return -1;
      }
    }
    memcpy(&obj->stat, &stat_buf, sizeof(struct stat));
    return 1;
  }

  /* Unless flag for rewinding to the start is set, seek to the end
   * if we re-open the same file again or the file opened is the first at all
   * or the first after an error */
  if ((obj->stat.st_ino == 0) || (obj->stat.st_ino == stat_buf.st_ino))
    seek_end = !force_rewind;

  FILE *fh = fopen(obj->file, "r");
  if (fh == NULL) {
    P_ERROR("utils_tail: fopen (%s) failed: %s", obj->file, STRERRNO);
    return -1;
  }

  if (seek_end != 0) {
    status = fseek(fh, 0, SEEK_END);
    if (status != 0) {
      P_ERROR("utils_tail: fseek (%s) failed: %s", obj->file, STRERRNO);
      fclose(fh);
      return -1;
    }
  }

  if (obj->fh != NULL)
    fclose(obj->fh);
  obj->fh = fh;
  memcpy(&obj->stat, &stat_buf, sizeof(struct stat));

  return 0;
} /* int cu_tail_reopen */

cu_tail_t *cu_tail_create(const char *file) {
  cu_tail_t *obj;

  obj = calloc(1, sizeof(*obj));
  if (obj == NULL)
    return NULL;

  obj->file = strdup(file);
  if (obj->file == NULL) {
    free(obj);
    return NULL;
  }

  obj->fh = NULL;

  return obj;
} /* cu_tail_t *cu_tail_create */

int cu_tail_destroy(cu_tail_t *obj) {
  if (obj->fh != NULL)
    fclose(obj->fh);
  free(obj->file);
  free(obj);

  return 0;
} /* int cu_tail_destroy */

int cu_tail_readline(cu_tail_t *obj, char *buf, int buflen, bool force_rewind) {
  int status;

  if (buflen < 1) {
    ERROR("utils_tail: cu_tail_readline: buflen too small: %i bytes.", buflen);
    return -1;
  }

  if (obj->fh == NULL) {
    status = cu_tail_reopen(obj, force_rewind);
    if (status < 0)
      return status;
  }
  assert(obj->fh != NULL);

  /* Try to read from the filehandle. If that succeeds, everything appears to
   * be fine and we can return. */
  clearerr(obj->fh);
  if (fgets(buf, buflen, obj->fh) != NULL) {
    buf[buflen - 1] = '\0';
    return 0;
  }

  /* Check if we encountered an error */
  if (ferror(obj->fh) != 0) {
    /* Jupp, error. Force `cu_tail_reopen' to reopen the file.. */
    fclose(obj->fh);
    obj->fh = NULL;
  }
  /* else: eof -> check if the file was moved away and reopen the new file if
   * so.. */

  status = cu_tail_reopen(obj, force_rewind);
  /* error -> return with error */
  if (status < 0)
    return status;
  /* file end reached and file not reopened -> nothing more to read */
  else if (status > 0) {
    buf[0] = 0;
    return 0;
  }

  /* If we get here: file was re-opened and there may be more to read.. Let's
   * try again. */
  if (fgets(buf, buflen, obj->fh) != NULL) {
    buf[buflen - 1] = '\0';
    return 0;
  }

  if (ferror(obj->fh) != 0) {
    WARNING("utils_tail: fgets (%s) returned an error: %s", obj->file,
            STRERRNO);
    fclose(obj->fh);
    obj->fh = NULL;
    return -1;
  }

  /* EOf, well, apparently the new file is empty.. */
  buf[0] = 0;
  return 0;
} /* int cu_tail_readline */

int cu_tail_read(cu_tail_t *obj, char *buf, int buflen, tailfunc_t *callback,
                 void *data, bool force_rewind) {
  int status;

  while (42) {
    size_t len;

    status = cu_tail_readline(obj, buf, buflen, force_rewind);
    if (status != 0) {
      ERROR("utils_tail: cu_tail_read: cu_tail_readline "
            "failed.");
      break;
    }

    /* check for EOF */
    if (buf[0] == 0)
      break;

    len = strlen(buf);
    while (len > 0) {
      if (buf[len - 1] != '\n')
        break;
      buf[len - 1] = '\0';
      len--;
    }

    status = callback(data, buf, buflen);
    if (status != 0) {
      ERROR("utils_tail: cu_tail_read: callback returned "
            "status %i.",
            status);
      break;
    }
  }

  return status;
} /* int cu_tail_read */
