

#include "fl/video/video_impl.h"

#include "fl/fx/time.h"
#include "fl/stl/assert.h"
#include "fl/math/math.h"
#include "fl/log/log.h"
#include "fl/system/file_system.h"
#include "fl/video/frame_interpolator.h"
#include "fl/video/pixel_stream.h"
#include "crgb.h"
#include "fl/stl/noexcept.h"

namespace fl {
namespace video {

VideoImpl::VideoImpl(size_t pixelsPerFrame, float fpsVideo,
                     size_t nFramesInBuffer)
    : mPixelsPerFrame(pixelsPerFrame),
      mFrameInterpolator(
          fl::make_shared<FrameInterpolator>(fl::max(1, nFramesInBuffer), fpsVideo)) {}

void VideoImpl::pause(fl::u32 now) {
    if (!mTime) {
        mTime = fl::make_shared<TimeWarp>(now);
    }
    mTime->pause(now);
}
void VideoImpl::resume(fl::u32 now) {
    if (!mTime) {
        mTime = fl::make_shared<TimeWarp>(now);
    }
    mTime->resume(now);
}

void VideoImpl::setTimeScale(float timeScale) {
    mTimeScale = timeScale;
    if (mTime) {
        mTime->setSpeed(timeScale);
    }
}

void VideoImpl::setFade(fl::u32 fadeInTime, fl::u32 fadeOutTime) {
    mFadeInTime = fadeInTime;
    mFadeOutTime = fadeOutTime;
}

bool VideoImpl::needsFrame(fl::u32 now) const {
    fl::u32 f1, f2;
    bool out = mFrameInterpolator->needsFrame(now, &f1, &f2);
    return out;
}

VideoImpl::~VideoImpl() FL_NOEXCEPT { end(); }

void VideoImpl::begin(filebuf_ptr h) {
    end();
    mStream = fl::make_shared<PixelStream>(mPixelsPerFrame * kSizeRGB8);
    mStream->begin(h);
    mPrevNow = 0;
}

void VideoImpl::end() {
    mFrameInterpolator->clear();
    // Removed resetFrameCounter and setStartTime calls
    mStream.reset();
}

bool VideoImpl::full() const { return mFrameInterpolator->getFrames()->full(); }

bool VideoImpl::draw(fl::u32 now, Frame *frame) {
    return draw(now, frame->rgb());
}

i32 VideoImpl::durationMicros() const {
    if (!mStream) {
        return -1;
    }
    i32 frames = mStream->framesRemaining();
    if (frames < 0) {
        return -1; // Stream case, duration unknown
    }
    fl::u32 micros_per_frame =
        mFrameInterpolator->getFrameTracker().microsecondsPerFrame();
    return (frames * micros_per_frame); // Convert to milliseconds
}

bool VideoImpl::draw(fl::u32 now, fl::span<CRGB> leds) {
    if (!mTime) {
        mTime = fl::make_shared<TimeWarp>(now);
        mTime->setSpeed(mTimeScale);
        mTime->reset(now);
    }
    now = mTime->update(now);
    if (!mStream) {
        FL_WARN("no stream");
        return false;
    }
    bool ok = updateBufferIfNecessary(mPrevNow, now);
    mPrevNow = now;
    if (!ok) {
        FL_WARN("updateBufferIfNecessary failed");
        return false;
    }
    mFrameInterpolator->draw(now, leds);

    fl::u32 time = mTime->time();
    fl::u32 brightness = 255;
    // Compute fade in/out brightness.
    if (mFadeInTime || mFadeOutTime) {
        brightness = 255;
        if (time <= mFadeInTime) {
            if (mFadeInTime == 0) {
                brightness = 255;
            } else {
                brightness = time * 255 / mFadeInTime;
            }
        } else if (mFadeOutTime) {
            i32 frames_remaining = mStream->framesRemaining();
            if (frames_remaining < 0) {
                // -1 means this is a stream.
                brightness = 255;
            } else {
                FrameTracker &frame_tracker =
                    mFrameInterpolator->getFrameTracker();
                fl::u32 micros_per_frame =
                    frame_tracker.microsecondsPerFrame();
                fl::u32 millis_left =
                    (frames_remaining * micros_per_frame) / 1000;
                if (millis_left < mFadeOutTime) {
                    brightness = millis_left * 255 / mFadeOutTime;
                }
            }
        }
    }
    if (brightness < 255) {
        if (brightness == 0) {
            for (size_t i = 0; i < mPixelsPerFrame; ++i) {
                leds[i] = CRGB::Black;
            }
        } else {
            for (size_t i = 0; i < mPixelsPerFrame; ++i) {
                leds[i].nscale8(brightness);
            }
        }
    }
    return true;
}

bool VideoImpl::updateBufferFromStream(fl::u32 now) {
    FASTLED_ASSERT(mTime, "mTime is null");
    if (!mStream) {
        FL_WARN("no stream");
        return false;
    }
    if (mStream->atEnd()) {
        return false;
    }

    fl::u32 currFrameNumber = 0;
    fl::u32 nextFrameNumber = 0;
    bool needs_frame =
        mFrameInterpolator->needsFrame(now, &currFrameNumber, &nextFrameNumber);
    if (!needs_frame) {
        return true;
    }

    if (mFrameInterpolator->capacity() == 0) {
        FL_WARN("capacity == 0");
        return false;
    }

    const bool has_current_frame = mFrameInterpolator->has(currFrameNumber);
    const bool has_next_frame = mFrameInterpolator->has(nextFrameNumber);

    fl::FixedVector<fl::u32, 2> frame_numbers;
    if (!has_current_frame) {
        frame_numbers.push_back(currFrameNumber);
    }
    size_t capacity = mFrameInterpolator->capacity();
    if (capacity > 1 && !has_next_frame) {
        frame_numbers.push_back(nextFrameNumber);
    }

    for (size_t i = 0; i < frame_numbers.size(); ++i) {
        FramePtr recycled_frame;
        if (mFrameInterpolator->full()) {
            fl::u32 frame_to_erase = 0;
            bool ok =
                mFrameInterpolator->get_oldest_frame_number(&frame_to_erase);
            if (!ok) {
                FL_WARN("get_oldest_frame_number failed");
                return false;
            }
            recycled_frame = mFrameInterpolator->erase(frame_to_erase);
            if (!recycled_frame) {
                FL_WARN("erase failed for frame: " << frame_to_erase);
                return false;
            }
        }
        fl::u32 frame_to_fetch = frame_numbers[i];
        if (!recycled_frame) {
            // Happens when we are not full and we need to allocate a new frame.
            recycled_frame = fl::make_shared<Frame>(mPixelsPerFrame);
        }

        if (!mStream->readFrame(recycled_frame.get())) {
            if (mStream->atEnd()) {
                if (!mStream->rewind()) {
                    FL_WARN("rewind failed");
                    return false;
                }
                mTime->reset(now);
                frame_to_fetch = 0;
                if (!mStream->readFrameAt(frame_to_fetch,
                                          recycled_frame.get())) {
                    FL_WARN("readFrameAt failed");
                    return false;
                }
            } else {
                // For streaming mode, buffer might not have data yet - this is OK
                // Producer (effect renderer) will fill buffer on next call
                // Just return success and we'll try again next frame
                if (mStream->getType() == PixelStream::kStreaming) {
                    return true;  // Gracefully handle empty streaming buffer
                }
                FL_WARN("We failed for some other reason");
                return false;
            }
        }
        bool ok = mFrameInterpolator->insert(frame_to_fetch, recycled_frame);
        if (!ok) {
            FL_WARN("insert failed");
            return false;
        }
    }
    return true;
}

bool VideoImpl::updateBufferFromFile(fl::u32 now, bool forward) {
    fl::u32 currFrameNumber = 0;
    fl::u32 nextFrameNumber = 0;
    bool needs_frame =
        mFrameInterpolator->needsFrame(now, &currFrameNumber, &nextFrameNumber);
    if (!needs_frame) {
        return true;
    }
    bool has_curr_frame = mFrameInterpolator->has(currFrameNumber);
    bool has_next_frame = mFrameInterpolator->has(nextFrameNumber);
    if (has_curr_frame && has_next_frame) {
        return true;
    }
    if (mFrameInterpolator->capacity() == 0) {
        FL_WARN("capacity == 0");
        return false;
    }

    fl::FixedVector<fl::u32, 2> frame_numbers;
    if (!mFrameInterpolator->has(currFrameNumber)) {
        frame_numbers.push_back(currFrameNumber);
    }
    if (mFrameInterpolator->capacity() > 1 &&
        !mFrameInterpolator->has(nextFrameNumber)) {
        frame_numbers.push_back(nextFrameNumber);
    }

    for (size_t i = 0; i < frame_numbers.size(); ++i) {
        FramePtr recycled_frame;
        if (mFrameInterpolator->full()) {
            fl::u32 frame_to_erase = 0;
            bool ok = false;
            if (forward) {
                ok = mFrameInterpolator->get_oldest_frame_number(
                    &frame_to_erase);
                if (!ok) {
                    FL_WARN("get_oldest_frame_number failed");
                    return false;
                }
            } else {
                ok = mFrameInterpolator->get_newest_frame_number(
                    &frame_to_erase);
                if (!ok) {
                    FL_WARN("get_newest_frame_number failed");
                    return false;
                }
            }
            recycled_frame = mFrameInterpolator->erase(frame_to_erase);
            if (!recycled_frame) {
                FL_WARN("erase failed for frame: " << frame_to_erase);
                return false;
            }
        }
        fl::u32 frame_to_fetch = frame_numbers[i];
        if (!recycled_frame) {
            // Happens when we are not full and we need to allocate a new frame.
            recycled_frame = fl::make_shared<Frame>(mPixelsPerFrame);
        }

        do { // only to use break
            if (!mStream->readFrameAt(frame_to_fetch, recycled_frame.get())) {
                if (!forward) {
                    // nothing more we can do, we can't go negative.
                    return false;
                }
                if (mStream->atEnd()) {
                    if (!mStream->rewind()) { // Is this still
                        FL_WARN("rewind failed");
                        return false;
                    }
                    mTime->reset(now);
                    frame_to_fetch = 0;
                    if (!mStream->readFrameAt(frame_to_fetch,
                                              recycled_frame.get())) {
                        FL_WARN("readFrameAt failed");
                        return false;
                    }
                    break; // we have the frame, so we can break out of the loop
                }
                FL_WARN("We failed for some other reason");
                return false;
            }
            break;
        } while (false);

        bool ok = mFrameInterpolator->insert(frame_to_fetch, recycled_frame);
        if (!ok) {
            FL_WARN("insert failed");
            return false;
        }
    }
    return true;
}

bool VideoImpl::updateBufferIfNecessary(fl::u32 prev, fl::u32 now) {
    const bool forward = now >= prev;

    PixelStream::Type type = mStream->getType();
    switch (type) {
    case PixelStream::kFile:
        return updateBufferFromFile(now, forward);
    case PixelStream::kStreaming:
        return updateBufferFromStream(now);
    default:
        FL_WARN("Unknown type: " << fl::u32(type));
        return false;
    }
}

bool VideoImpl::rewind() {
    if (!mStream || !mStream->rewind()) {
        return false;
    }
    mFrameInterpolator->clear();
    return true;
}

bool VideoImpl::hasEmbeddedScreenMap() const FL_NOEXCEPT {
    if (!mStream) return false;
    return mStream->hasEmbeddedScreenMap();
}

const fl::string &VideoImpl::embeddedScreenMapJson() const FL_NOEXCEPT {
    static const fl::string kEmpty;
    if (!mStream) return kEmpty;
    return mStream->embeddedScreenMapJson();
}

} // namespace video
} // namespace fl
