/*
 * Copyright (C) 2006-2020  Music Technology Group - Universitat Pompeu Fabra
 *
 * This file is part of Essentia
 *
 * Essentia is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation (FSF), either version 3 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the Affero GNU General Public License
 * version 3 along with this program.  If not, see http://www.gnu.org/licenses/
 */



#include "framecutter.h"
#include "essentiamath.h" // for isSilent

using namespace std;

namespace essentia {
namespace standard {

const char* FrameCutter::name = "FrameCutter";
const char* FrameCutter::category = "Standard";
const char* FrameCutter::description = DOC("This algorithm slices the input buffer into frames. It returns a frame of a constant size and jumps a constant amount of samples forward in the buffer on every compute() call until no more frames can be extracted; empty frame vectors are returned afterwards. Incomplete frames (frames starting before the beginning of the input buffer or going past its end) are zero-padded or dropped according to the \"validFrameThresholdRatio\" parameter.\n"
"\n"
"The algorithm outputs as many frames as needed to consume all the information contained in the input buffer. Depending on the \"startFromZero\" parameter:\n"
"  - startFromZero = true: a frame is the last one if its end position is at or beyond the end of the stream. The last frame will be zero-padded if its size is less than \"frameSize\"\n"
"  - startFromZero = false: a frame is the last one if its center position is at or beyond the end of the stream\n"
"In both cases the start time of the last frame is never beyond the end of the stream.\n");


void FrameCutter::configure() {
  _frameSize = parameter("frameSize").toInt();
  _hopSize = parameter("hopSize").toInt();
  _startFromZero = parameter("startFromZero").toBool();
  _lastFrameToEndOfFile = parameter("lastFrameToEndOfFile").toBool();

  Real ratio = parameter("validFrameThresholdRatio").toReal();
  if (ratio > 0.5 && !_startFromZero) {
    throw EssentiaException("FrameCutter: validFrameThresholdRatio cannot be "
                            "larger than 0.5 if startFromZero is false (this "
                            "is to prevent loss of the first frame which would "
                            "be only half a valid frame since the first frame "
                            "is centered on the beginning of the audio)");
  }
  _validFrameThreshold = (int)round(ratio*_frameSize);

  reset();
}

void FrameCutter::reset() {
  _lastFrame = false;

  if (_startFromZero) {
    _startIndex = 0;
  }
  else {
    _startIndex = -(_frameSize+1)/2; // +1 so that odd frameSize start before
  }
}


void FrameCutter::compute() {

  const vector<Real>& buffer = _buffer.get();
  vector<Real>& frame = _frame.get();

  // if we're already lastFrame or the input stream is empty, don't return any frame
  if (_lastFrame || buffer.empty()) {
    frame.clear();
    return;
  }

  // if we're past the end of stream, don't return anything either
  if (_startIndex >= (int)buffer.size()) {
    frame.clear();
    return;
  }


  frame.resize(_frameSize);
  int idxInFrame = 0;

  // if we're before the beginning of the buffer, fill the frame with 0
  if (_startIndex < 0) {
    int howmuch = min(-_startIndex, _frameSize);
    for (; idxInFrame<howmuch; idxInFrame++) {
      frame[idxInFrame] = (Real)0.0;
    }
  }

  // now, just copy from the buffer to the frame
  int howmuch = min(_frameSize, (int)buffer.size() - _startIndex) - idxInFrame;

  fastcopy(&frame[0]+idxInFrame, &buffer[0]+_startIndex+idxInFrame, howmuch);
  idxInFrame += howmuch;

  // check if the idxInFrame is below the threshold (this would only happen
  // for the last frame in the stream)
  if (idxInFrame < _validFrameThreshold) {
    frame.clear();
    _lastFrame = true;
    return;
  }

  if (_startIndex + idxInFrame >= (int)buffer.size() &&
      _startFromZero && !_lastFrameToEndOfFile) _lastFrame = true;

  if (idxInFrame < _frameSize) {
    if (_startFromZero) {
      if (_lastFrameToEndOfFile) {
        if (_startIndex >= (int)buffer.size()) _lastFrame = true;
      }
      // if we're zero-padding with startFromZero=true, it means we're filling
      // in the last frame, so we'll have to stop after this one
      else _lastFrame = true;
    }
    else {
      // if we're zero-padding and the center of the frame is past the end of the
      // stream, then this is the last frame and we need to stop after this one
      if (_startIndex + _frameSize/2 >= (int)buffer.size()) {
        _lastFrame = true;
      }
    }
    // fill in the frame with 0 until the end of the buffer
    for (; idxInFrame < _frameSize; idxInFrame++) {
      frame[idxInFrame] = (Real)0.0;
    }
  }

  // advance frame position
  _startIndex += _hopSize;
}

} // namespace standard
} // namespace essentia


namespace essentia {
namespace streaming {

const char* FrameCutter::name = standard::FrameCutter::name;
const char* FrameCutter::description = DOC("This algorithm slices the input stream into frames. The frames are of constant size and are separated by a constant amount of samples. The algorithm outputs frames as single tokens in the output stream. Incomplete frames (frames starting before the beginning of the input stream or going past its end) are zero-padded or dropped according to the \"validFrameThresholdRatio\" parameter.\n"
"\n"
"The algorithm outputs as many frames as needed to consume all the information contained in the stream. Depending on the \"startFromZero\" parameter:\n"
"  - startFromZero = true: a frame is the last one if its end position is at or beyond the end of the stream. The last frame will be zero-padded if its size is less than \"frameSize\"\n"
"  - startFromZero = false: a frame is the last one if its center position is at or beyond the end of the stream\n"
"In both cases the start time of the last frame is never beyond the end of the stream.\n");


void FrameCutter::reset() {
  Algorithm::reset();
  //_reschedule = false;
  _streamIndex = 0;
  if (_startFromZero) _startIndex = 0;
  else                _startIndex = -(_frameSize+1)/2;

  _audio.setAcquireSize(_frameSize);
  _audio.setReleaseSize(_hopSize);
  _frames.setAcquireSize(1);
  _frames.setReleaseSize(1);
}

FrameCutter::SilenceType FrameCutter::typeFromString(const std::string& name) const {
  if (name == "keep") return KEEP;
  if (name == "drop") return DROP;
  return ADD_NOISE;
}

void FrameCutter::configure() {
  _frameSize = parameter("frameSize").toInt();
  _hopSize = parameter("hopSize").toInt();
  _silentFrames = typeFromString(parameter("silentFrames").toString());
  _lastFrameToEndOfFile = parameter("lastFrameToEndOfFile").toBool();

  _startFromZero = parameter("startFromZero").toBool();
  if (_startFromZero) {
    _startIndex = 0;
  }
  else {
    _startIndex = -(_frameSize+1)/2;
  }

  Real ratio = parameter("validFrameThresholdRatio").toReal();
  if (ratio > 0.5 && !_startFromZero) {
    throw EssentiaException("FrameCutter: validFrameThresholdRatio cannot be "
                            "larger than 0.5 if startFromZero is false (this "
                            "is to prevent loss of the first frame which would "
                            "be only half a valid frame since the first frame "
                            "is centered on the beginning of the audio)");
  }
  _validFrameThreshold = (int)round(ratio*_frameSize);
  // Adding noise to avoid divisions by zero (in case the user chooses to do so
  // by setting the silentFrames parameter to ADD_NOISE).  The level of such noise
  // is chosen to be -100dB because it will still be detected as a silent frame
  // by essentia::isSilent() and is unhearable by humans
  _noiseAdder->configure("fixSeed", false, "level", -100);
  reset();
}

/*
  FrameCutter algo (pseudocode):

  bool lastFrame = false

  do {
    - we need to know whether we have to zero-pad on the left: ie, _startIndex < 0

    - then, we need to see how many more tokens we need to acquire from the stream to fill a frame: howmuch = _frameSize - _startIndex
      - if there are not enough tokens in the stream (howmuch < available):
        -> if shouldStop = false, just return producedData and wait for more to come
        -> if shouldStop = True, we need to acquire what's left: howmuch = available (here we need to pay attention whether _startFromZero = true or false)
            -> if _startFromZero = true: lastFrame = True
            -> if _startFromZero = false and _startIndex + frameSize/2 > _streamIndex + available: lastFrame = true (if center of frame is past the end of stream)

    doAcquire( howmuch )

    acquireFrame(1)

    frame[0:zp] = 0
    frame[zp:zp+howmuch] = memcpy
    frame[zp+homwuch:frameSize] = 0

    release(frame)
    produceData = true

    if (lastFrame) return true;

  } while(true)


 */

AlgorithmStatus FrameCutter::process() {
  bool lastFrame = false;

  EXEC_DEBUG("process()");

  // if _streamIndex < _startIndex, we need to advance into the stream until we
  // arrive at _startIndex
  if (_streamIndex < _startIndex) {
    // to make sure we can skip that many, use frameSize (buffer has been resized
    // to be able to accomodate at least that many sample before starting processing)
    int skipSize = _frameSize;
    int howmuch = min(_startIndex - _streamIndex, skipSize);
    _audio.setAcquireSize(howmuch);
    _audio.setReleaseSize(howmuch);
    _frames.setAcquireSize(0);
    _frames.setReleaseSize(0);

    if (acquireData() != OK) return NO_INPUT;

    releaseData();
    _streamIndex += howmuch;

    return OK;
  }

  // need to know whether we have to zero-pad on the left: ie, _startIndex < 0
  int zeropadSize = 0;
  int acquireSize = _frameSize;
  int releaseSize = min(_hopSize, _frameSize); // in case hopsize > framesize
  int available = _audio.available();

  // we need this check anyway because we might be at the very end of the stream and try to acquire 0
  // for our last frame, which will unfortunately work, so just get rid of this case right now
  if (available == 0) return NO_INPUT;

  if (_startIndex < 0) {
    // left zero-padding and only acquire  as much as _frameSize + startIndex tokens and should release zero
    acquireSize = _frameSize + _startIndex;
    releaseSize = 0;
    zeropadSize = -_startIndex;
  }

  // if there are not enough tokens in the stream (howmuch < available):
  if (acquireSize >= available) { // has to be >= in case the size of the audio fits exactly with frameSize & hopSize
    if (!shouldStop()) return NO_INPUT; // not end of stream -> return and wait for more data to come

    acquireSize = available; // need to acquire what's left
    releaseSize = _startIndex >= 0 ? min(available, _hopSize) : 0; // cannot release more tokens than there are available
    if (_startFromZero) {
      if (_lastFrameToEndOfFile) {
        if (_startIndex >= _streamIndex+available) lastFrame = true;
      }
      else lastFrame = true;
    }
    else {
      if (_startIndex + _frameSize/2 >= _streamIndex + available) // center of frame >= end of stream
        lastFrame = true;
    }
  }

  _frames.setAcquireSize(1);
  _frames.setReleaseSize(1);
  _audio.setAcquireSize(acquireSize);
  _audio.setReleaseSize(releaseSize);

  /*
  EXEC_DEBUG("zeropadSize: " << zeropadSize
             << "\tacquireSize: " << acquireSize
             << "\treleaseSize: " << releaseSize
             << "\tavailable: " << available
             << "\tlast frame: " << lastFrame
             << "\tstartIndex: " << _startIndex
             << "\tstreamIndex: " << _streamIndex);
  */

  AlgorithmStatus status = acquireData();
  EXEC_DEBUG("data acquired (audio: " << acquireSize << " - frames: 1)");

  if (status != OK) {
    if (status == NO_INPUT) return NO_INPUT;
    if (status == NO_OUTPUT) return NO_OUTPUT;
    throw EssentiaException("FrameCutter: something weird happened.");
  }

  // some semantic description to not get mixed up between the 2 meanings
  // of a vector<Real> (which acts both as a stream of Real tokens at the
  // input and as a single vector<Real> token at the output)
  typedef vector<AudioSample> Frame;

  // get the audio input and copy it as a frame to the output
  const vector<AudioSample>& audio = _audio.tokens();
  Frame& frame = _frames.firstToken();


  frame.resize(_frameSize);

  // left zero-padding of the frame
  int idxInFrame = 0;
  for (; idxInFrame < zeropadSize; idxInFrame++) {
    frame[idxInFrame] = (Real)0.0;
  }

  fastcopy(frame.begin()+idxInFrame, audio.begin(), acquireSize);
  idxInFrame += acquireSize;

  // check if the idxInFrame is below the threshold (this would only happen
  // for the last frame in the stream) and if so, don't produce data
  if (idxInFrame < _validFrameThreshold) {
    E_INFO("FrameCutter: dropping incomplete frame");

    // release inputs (advance to next frame), but not the output frame (we didn't produce anything)
    _audio.release(_audio.releaseSize());
    return NO_INPUT;
  }

  // right zero-padding on the last frame
  for (; idxInFrame < _frameSize; idxInFrame++) {
    frame[idxInFrame] = (Real)0.0;
  }

  _startIndex += _hopSize;

  if (isSilent(frame)) {
    switch (_silentFrames) {
    case DROP:
      E_INFO("FrameCutter: dropping silent frame");

      // release inputs (advance to next frame), but not the output frame (we didn't produce anything)
      _audio.release(_audio.releaseSize());
      return OK;

    case ADD_NOISE: {
      vector<AudioSample> inputFrame(_frameSize, 0.0);
      fastcopy(&inputFrame[0]+zeropadSize, &frame[0], acquireSize);
      _noiseAdder->input("signal").set(inputFrame);
      _noiseAdder->output("signal").set(frame);
      _noiseAdder->compute();
      break;
    }

    // otherwise, don't do nothing...
    case KEEP:
    default:
      ;
    }
  }

  EXEC_DEBUG("produced frame; releasing");
  releaseData();
  _streamIndex += _audio.releaseSize();

  EXEC_DEBUG("released");

  if (lastFrame) return PASS;

  return OK;
}

} // namespace streaming
} // namespace essentia
