// AddTimeCode.C
// Copyright (c) 2009 The Foundry Visionmongers Ltd.  All Rights Reserved.

static const char* const CLASS = "AddTimeCode";
static const char* const HELP =
  "AddTimeCode:\n"
  "Adds a timecode to the metadata passed through.";
static const double MAX_FPS_VAL = 1000;

#include <limits.h>
#include <stdio.h>

#include "DDImage/Iop.h"
#include "DDImage/Knobs.h"
#include "DDImage/Row.h"
#include "DDImage/MetaData.h"
#include "DDImage/DDMath.h"

#include <Base/fnTimecode.h>
#include <Base/fnTimeBase.h>

using namespace DD::Image;

static const float DEFAULT_FPS = 24.0f;

class AddTimeCode : public Iop
{
  MetaData::Bundle _meta;

  float _fps;
  bool _fpsFromMeta;
  const char* _startCode;
  int _startFrame;
  bool _startSpecify;

public:
  void _validate(bool);
  void _request(int, int, int, int, ChannelMask, int);
  void engine(int y, int x, int r, ChannelMask, Row &);

  int split_input(int) const
  {
    return 1;
  }

  int maximum_inputs() const { return 1; }
  int minimum_inputs() const { return 1; }

  const char* Class() const { return CLASS; }
  static const Iop::Description d;
  const char* node_help() const { return HELP; }
  void knobs(Knob_Callback);

  AddTimeCode(Node* node) : Iop(node)
  {
    _startCode = "01:00:00:00";
    _fps = DEFAULT_FPS;
    _fpsFromMeta = true;
    _startSpecify = false;
    _startFrame = 1;
  }

  Iop* in() const
  {
    return input(0);
  }

  void append(Hash& hash)
  {
    hash.append(outputContext().frame());
  }

  int knob_changed(DD::Image::Knob* k)
  {
    if (k->is("useFrame")) {
      knob("frame")->enable(k->get_value() != 0.0);
      return true;
    }

    if (k->is("metafps")) {
      knob("fps")->enable(!k->get_value());
      return true;
    }

    // set up the enabled/disabled knobs here, at least the first time through
    // this gets called the first time the ui is updated, which presumably means that the knobs have been set properly at least once
    if (k == &Knob::showPanel){
      knob("frame")->enable(_startSpecify);
      knob("fps")->enable(!_fpsFromMeta);
    }

    return Iop::knob_changed(k);
  }

  const MetaData::Bundle& _fetchMetaData(const char* key)
  {
    _meta = Iop::_fetchMetaData(key);

    int startFrame = info_.first_frame();
    if (_startSpecify) {
      startFrame = _startFrame;
    }

    float fps = DEFAULT_FPS;

    if (_fpsFromMeta) {
      double fpsAsFl = _meta.getDouble(MetaData::FRAME_RATE);
      if ((!isfinite(fpsAsFl)) || (fpsAsFl > MAX_FPS_VAL)) {
        fps = DEFAULT_FPS;
      }
      else {
        fps = float(fpsAsFl);
      }
    }
    else
      fps = _fps;

    if (fps == 0.0f)
      fps = DEFAULT_FPS;

    int frame = int(outputContext().frame()) - startFrame;

    Foundry::Base::TimeBase timebase(fps);

    bool supportsDropFrames = timebase.supportsDropFrames();
    bool dropFrames = supportsDropFrames && (Foundry::Base::Timecode::displayTypeFromString(_startCode) == Foundry::Base::Timecode::kDisplayDropFrameTimecode);

    // set the frame rate for downstream stuff
    _meta.setData(MetaData::FRAME_RATE, fps);

    Foundry::Base::Time startFrameOffset = Foundry::Base::Timecode::stringToTime(_startCode, timebase, dropFrames ? Foundry::Base::Timecode::kDisplayDropFrameTimecode : Foundry::Base::Timecode::kDisplayTimecode);

    if (startFrameOffset == Foundry::Base::kInvalidTime)
      startFrameOffset = 0;

    Foundry::Base::Time currentFrame = startFrameOffset + frame;
    std::string timeCodeString = Foundry::Base::Timecode::timeToString(currentFrame, timebase, dropFrames ? Foundry::Base::Timecode::kDisplayDropFrameTimecode : Foundry::Base::Timecode::kDisplayTimecode);

    _meta.setData(MetaData::TIMECODE, timeCodeString);

    return _meta;
  }
};

void AddTimeCode::_validate(bool)
{
  copy_info();

  // check if the parameters work
  float fps = DEFAULT_FPS;

  if (_fpsFromMeta) {
    MetaData::Bundle metaData = Iop::_fetchMetaData(MetaData::FRAME_RATE);
    double fpsAsFl = _meta.getDouble(MetaData::FRAME_RATE);
    if ((!isfinite(fpsAsFl)) || (fpsAsFl > MAX_FPS_VAL)) {
      fps = DEFAULT_FPS;
    }
    else {
      fps = float(fpsAsFl);
    }
  }
  else
    fps = _fps;

  if (fps == 0.0f)
    fps = DEFAULT_FPS;

  Foundry::Base::TimeBase timebase(fps);

  bool supportsDropFrames = timebase.supportsDropFrames();
  bool dropFrames = (Foundry::Base::Timecode::displayTypeFromString(_startCode) == Foundry::Base::Timecode::kDisplayDropFrameTimecode);
  if (!supportsDropFrames && dropFrames)
    error("start code indicates drop frames (';') but the fps does not.");

  Foundry::Base::Time startFrameOffset = Foundry::Base::Timecode::stringToTime(_startCode, timebase, dropFrames ? Foundry::Base::Timecode::kDisplayDropFrameTimecode : Foundry::Base::Timecode::kDisplayTimecode);
  if (startFrameOffset == Foundry::Base::kInvalidTime)
    error("Invalid start time code");
}

void AddTimeCode::_request(int x, int y, int r, int t, ChannelMask channels, int count)
{
  in()->request(x, y, r, t, channels, count);
}

void AddTimeCode::engine(int y, int x, int r, ChannelMask channels, Row& out)
{
  out.get(*in(), y, x, r, channels);
}


void AddTimeCode::knobs(Knob_Callback f)
{
  Eval_String_knob(f, &_startCode, "startcode", "start code");
  Tooltip(f, "The start of the time code. Must be in the format of either HH:MM:SS:FF or HH;MM;SS;FF (for drop frames). Supports expressions, e.g. \"[frame]:00:00:00\" will set a different hour for each frame. Drop frames are assumed when the ; is used to delimit the time code, if the frame rate is 29.97 or 59.94.");
  Float_knob(f, &_fps, "fps");
  Tooltip(f, "The frame rate to use to generate the time codes per frame of output. To write the time codes in drop frame format, the start code must have semi-colons (;) in it, and the frame rate must be one of the frame rates that support drop frames (29.97 or 59.94).");
  ClearFlags(f, DD::Image::Knob::SLIDER);
  SetFlags(f, DD::Image::Knob::DISABLED);
  Bool_knob(f, &_fpsFromMeta, "metafps", "get FPS from metadata");
  Int_knob(f, &_startFrame, "frame", "start frame");
  SetFlags(f, DD::Image::Knob::DISABLED);
  Bool_knob(f, &_startSpecify, "useFrame", "use start frame?");
}

static Iop* build(Node* node) { return new AddTimeCode(node); }
const Iop::Description AddTimeCode::d(CLASS, "MetaData/Modify", build);
