/* flaneth - flash and ethernet
   Copyright (C) 2007-2012 Stefan Schuermans <stefan@blinkenarea.org>
   Copyleft: GNU public license V2 - http://www.gnu.org/copyleft/gpl.html
   a BlinkenArea project - http://www.blinkenarea.org/ */

#include <stdio.h>
#include <string.h>

#include "apps_tools.h"
#include "app_bbm.h"
#include "debug.h"
#include "dosfs.h"
#include "nethelp.h"
#include "udp.h"
#include "ser115200.h"
#include "timing.h"

///< BBM file header
typedef struct app_bbm_header {
  uint32_t magic;
  uint16_t height;
  uint16_t width;
  uint16_t channels;
  uint16_t maxval;
  uint32_t framecnt;
  uint32_t duration;
  uint32_t frameptr;
} AppBbmHeader;

/**
 * @brief get name of next file from playlist directory
 * @param[in,out] state internal state of application
 * @param[out] filename name of file
 * @param[in] filename_sz length of filename buffer
 * @return 0 if success ful, -1 if not
 */
static char AppBbmNextName(AppBbmState *state, char *filename,
                           unsigned char filename_sz)
{
  // assemble filename of playlist directory
  char dirname[10];
  sprintf(dirname, "bbm%hhu/pl", state->no);

  // open playlist directory
  DIRINFO di;
  di.scratch = *state->sectorBuf;
  if (DFS_OpenDir(state->vi, (uint8_t *)dirname, &di) != DFS_OK) {
    // no playlist dir -> deactivate application
    state->isInit = 0;
    debug_app_bbm_printf("%hhu: cannot open directory %s",
                         state->no, dirname);
    return -1;
  }

  // get file with index idxFile
  DIRENT de;
  unsigned short idx;
  char firstName[11], haveFirst;
  haveFirst = 0;
  for (idx = 0; idx <= /* file 0 -> loop once */ state->idxFile; ++idx) {

    // get next file
    do {
      if (DFS_GetNext(state->vi, &di, &de) != 0) {
        // no files at all -> deactivate application
        if (!haveFirst) {
          state->isInit = 0;
          debug_app_bbm_printf("%hhu: no files in playlist directory",
                               state->no);
          return -1;
        }
        // end of (non-empty) playlist reached -> restart playlist
        // -> return first file name, next file index 1
        snprintf(filename, filename_sz,
                 "bbm%hhu/pl/%-11.11s", state->no, firstName);
        filename[filename_sz - 1] = 0;
        state->idxFile = 1;
        return 0;
      }
    } while (de.name[0] == 0 ||                            // ignore deleted
             de.attr & (ATTR_VOLUME_ID | ATTR_DIRECTORY)); // ignore volumeIDs,
                                                           // dirs, long names

    // first file found -> save name (for restarting playlist when end reached)
    if (!haveFirst) {
      memcpy(firstName, de.name, sizeof(firstName));
      haveFirst = 1;
    }

  } // for idx

  // found name of file with index idxFile
  // -> return file name, increment file index for next file
  snprintf(filename, filename_sz,
           "bbm%hhu/pl/%-11.11s", state->no, de.name);
  filename[filename_sz - 1] = 0;
  state->idxFile++;
  return 0;
}

/**
 * @brief parse header of BBM file
 * @param[in,out] state internal state of application
 */
static void AppBbmParseHeader(AppBbmState *state)
{
  // read BBM header
  AppBbmHeader hdr;
  uint32_t len;
  if (DFS_ReadFile(&state->fi, *state->sectorBuf,
                   (uint8_t *)&hdr, &len, sizeof(hdr)) != DFS_OK)
    len = 0;
  if (len < sizeof(hdr)) {
    debug_app_bbm_printf("%hhu: truncated BBM file", state->no);
    return; // retry with next file in next iteration
  }

  // check header
  if (ntohl(hdr.magic) != 0x23542666) {
    debug_app_bbm_printf("%hhu: invalid magic %lX in BBM file",
                         state->no, hdr.magic);
    return; // retry with next file in next iteration
  }
  state->height   = ntohs(hdr.height);
  state->width    = ntohs(hdr.width);
  state->channels = ntohs(hdr.channels);
  state->maxval   = ntohs(hdr.maxval);
  if (state->height < 1 || state->height > 256 ||
      state->width < 1 || state->width > 256 ||
      state->channels < 1 || state->channels > 3 ||
      state->maxval < 1 || state->maxval > 255) {
    debug_app_bbm_printf("%hhu: invalid dimensions in BBM file", state->no);
    return; // retry with next file in next iteration
  }
  state->dataSz = state->height * state->width * state->channels;
  if (state->dataSz > 768) {
    debug_app_bbm_printf("%hhu: BBM frame size too large", state->no);
    return; // retry with next file in next iteration
  }
  uint32_t frameptr = ntohl(hdr.frameptr);
  if (frameptr < sizeof(hdr)) {
    debug_app_bbm_printf("%hhu: invalid framepointer in BBM", state->no);
    return; // retry with next file in next iteration
  }

  // seek to start of first frame
  DFS_Seek(&state->fi, frameptr, *state->sectorBuf);
  if (state->fi.pointer != frameptr) {
    debug_app_bbm_printf("%hhu: seek to first frame failed", state->no);
    return; // retry with next file in next iteration
  }

  // read frame start mrker
  uint8_t framestart[4];
  if (DFS_ReadFile(&state->fi, *state->sectorBuf,
                   framestart, &len, sizeof(framestart)) != DFS_OK)
    len = 0;
  if (len < sizeof(framestart)) {
    debug_app_bbm_printf("%hhu: truncated BBM file", state->no);
    return; // retry with next file in next iteration
  }
  if (memcmp(framestart, "frms", 4) != 0) {
    debug_app_bbm_printf("%hhu: invalid frame start marker in BBM file",
                         state->no);
    return; // retry with next file in next iteration
  }

  // a file has been found and opened, header is parsed
  state->haveFile = 1;
  debug_app_bbm_printf("%hhu: header parsed: %hux%hu-%hu/%hu", state->no,
                       state->width, state->height, state->channels,
                       state->maxval + 1);
}

/**
 * @brief open next file from playlist directory
 * @param[in,out] state internal state of application
 */
static void AppBbmNextFile(AppBbmState *state)
{
  // get name of next file to open
  char filename[22];
  if (AppBbmNextName(state, filename, sizeof(filename)) != 0)
    return;

  // open file
  if (DFS_OpenFile(state->vi, (uint8_t *)filename, DFS_READ,
                   *state->sectorBuf, &state->fi) != 0) {
    debug_app_bbm_printf("%hhu: cannot open file %s", state->no, filename);
    return; // retry with next file in next iteration
  }
  debug_app_bbm_printf("%hhu: file %s", state->no, filename);

  // parse header
  AppBbmParseHeader(state);
}

/**
 * @brief process next frame
 * @param[in,out] state internal state of application
 */
static void AppBbmProcFrame(AppBbmState *state)
{
  struct packet {
    struct UdpPacket udp;
    uint8_t mcuf[12 + state->dataSz];
  } pack;

  // read next frame from file
  //   - read to mcuf pos 10, so data is placed at pos 12 for MCUF frame
  uint32_t pos_before_frame = state->fi.pointer;
  uint32_t len;
  if (DFS_ReadFile(&state->fi, *state->sectorBuf,
                   pack.mcuf + 10, &len, state->dataSz + 2) != DFS_OK)
    len = 0;
  if (len < state->dataSz + 2) {
    // end of file reached -> use next file in next iteration
    state->haveFile = 0;
    debug_app_bbm_printf("%hhu: end of file", state->no);
    return;
  }

  // get frame duration
  uint16_t duration = ntohs(*(uint16_t *)(pack.mcuf + 10));
  debug_app_bbm_printf("%hhu: frame duration %u", state->no, duration);
  // subtract time this frame has already been shown
  if (state->thisFrameMs < duration)
    duration -= state->thisFrameMs;
  else
    duration = 0;

  // fill in MCUF header
  *(uint32_t *)(pack.mcuf +  0) = htonl(0x23542666);
  *(uint16_t *)(pack.mcuf +  4) = htons(state->height);
  *(uint16_t *)(pack.mcuf +  6) = htons(state->width);
  *(uint16_t *)(pack.mcuf +  8) = htons(state->channels);
  *(uint16_t *)(pack.mcuf + 10) = htons(state->maxval);

  // send MCUF frame
  if (state->haveAddr) {
    memcpy(pack.udp.IpHdr.Dest, state->addr, sizeof(pack.udp.IpHdr.Dest));
    pack.udp.UdpHdr.SrcPort = htons(2323);
    pack.udp.UdpHdr.DestPort = htons(state->port);
    UdpSend((unsigned char *)&pack, sizeof(pack));
  }

  // serial output 115200 8N1
  if (state->ser115k2)
    Ser115200Send((unsigned char *)&pack.mcuf, sizeof(pack.mcuf));

  // frame duration not more than one second
  if (duration <= 1000) {
    // wait for frame duration
    state->nextActMs += duration;
    // next time, a new frame will be read
    state->thisFrameMs = 0;
    // done
    return;
  }

  // show same frame again in 1 second

  // jump back to position before this frame
  DFS_Seek(&state->fi, pos_before_frame, *state->sectorBuf);
  if (state->fi.pointer != pos_before_frame) {
    // broken file -> use next file in next iteration
    state->haveFile = 0;
    debug_app_bbm_printf("%hhu: seek failed", state->no);
    return;
  }

  // next action in 1 second
  state->nextActMs += 1000;
  // next time, the frame has been shown 1 second longer
  state->thisFrameMs += 1000;
}

/**
 * @brief initialize BBM play application
 * @param[in,out] state internal state of application
 *
 * state.sectorBuf, state.vi, state.no have to be initialized before calling
 */
void AppBbmInit(AppBbmState *state) // (extern)
{
  // default: not initialized
  state->isInit = 0;

  // get destination address for MCUF frames
  {
    char filename[12];
    sprintf(filename, "bbm%hhu/addr", state->no);
    if (AppsToolsReadIp(*state->sectorBuf, state->vi,
                        filename, state->addr) == 0) {
      state->haveAddr = 1;
      debug_app_bbm_printf("%hhu: address %hhu.%hhu.%hhu.%hhu", state->no,
                           state->addr[0], state->addr[1],
                           state->addr[2], state->addr[3]);
    } else {
      state->haveAddr = 0;
      debug_app_bbm_printf("%hhu: no address", state->no);
    }
  }

  // get destination port for MCUF frames
  if (state->haveAddr) {
    char filename[12];
    sprintf(filename, "bbm%hhu/port", state->no);
    if (AppsToolsReadPort(*state->sectorBuf, state->vi,
                          filename, &state->port) != 0)
      state->port = 2323; // default MCUF destination port
    debug_app_bbm_printf("%hhu: port %hu", state->no, state->port);
  }

  // check if serial output 115200 8N1 is requested
  {
    char filename[13], serDummy[3];
    sprintf(filename, "bbm%hhu/115k2", state->no);
    state->ser115k2 = AppsToolsReadStr(*state->sectorBuf, state->vi, filename,
                                       serDummy, sizeof(serDummy)) == 0;
    debug_app_bbm_printf("%hhu: serial output 115200 8N1: %c",
                         state->no, state->ser115k2 ? 'Y' : 'n');
  }

  // initialize internal state
  state->idxFile = 0;
  state->haveFile = 0;
  state->thisFrameMs = 0;

  // initialized, start with first action now
  state->isInit = 1;
  TimingGetMs(&state->nextActMs);
}

/**
 * @brief run BBM play application
 * @param[in,out] state internal state of application
 */
void AppBbmRun(AppBbmState *state) // (extern)
{
  // not initialized -> leave
  if (!state->isInit)
    return;

  // time for next action not yet reached -> leave
  unsigned long ms;
  TimingGetMs(&ms);
  long delta = state->nextActMs - ms;
  if (delta > 0)
    return;
  /* do not manipulate frame time - this can harm synchronous playback
  // if lagging behind more than 100ms, do not try to catch up
  if (delta < -100)
    state->nextActMs = ms;
  */

  // no file -> open next one
  if (!state->haveFile) {
    AppBbmNextFile(state);
    return; // do not work too much in one step / app might be deactivated now
  }

  // process next frame
  AppBbmProcFrame(state);
}

