#include "render.hpp"

#include <algorithm>
#include <cstring>
#include <filesystem>
#include <format>
#include <functional>
#include <fstream>
#include <string>

#include <SDL3/SDL_timer.h>

#include "log.hpp"
#include "path.hpp"
#include "process.hpp"

#if _WIN32
  #include "string.hpp"
#endif

using namespace anm2ed::util;

namespace anm2ed
{
  namespace
  {
    std::string ffmpeg_concat_escape(const std::string& value)
    {
      std::string escaped{};
      escaped.reserve(value.size());
      for (auto character : value)
      {
        if (character == '\'') escaped += "'\\''";
        else
          escaped += character;
      }
      return escaped;
    }
  }

  bool animation_render(const std::filesystem::path& ffmpegPath, const std::filesystem::path& path,
                        const std::vector<std::filesystem::path>& framePaths,
                        const std::vector<double>& frameDurations, AudioStream& audioStream, render::Type type, int fps)
  {
    if (framePaths.empty() || ffmpegPath.empty() || path.empty()) return false;
    fps = std::max(fps, 1);

    auto pathString = path::to_utf8(path);
    auto ffmpegPathString = path::to_utf8(ffmpegPath);
    auto loggerPath = Logger::path();
    auto loggerPathString = path::to_utf8(loggerPath);
#if _WIN32
    auto ffmpegTempPath = loggerPath.parent_path() / "ffmpeg_log.temp.txt";
    auto ffmpegTempPathString = path::to_utf8(ffmpegTempPath);
    std::error_code ffmpegTempError;
    std::filesystem::remove(ffmpegTempPath, ffmpegTempError);
#endif

    std::filesystem::path audioPath{};
    std::string audioInputArguments{};
    std::string audioOutputArguments{"-an"};
    std::string command{};
    auto temporaryDirectory = framePaths.front().parent_path();
    if (temporaryDirectory.empty()) temporaryDirectory = std::filesystem::temp_directory_path();

    auto audio_remove = [&]()
    {
      if (!audioPath.empty())
      {
        std::error_code ec;
        std::filesystem::remove(audioPath, ec);
      }
    };

    if (type != render::GIF && !audioStream.stream.empty() && audioStream.spec.freq > 0 &&
        audioStream.spec.channels > 0)
    {
      auto tempFilenameUtf8 = std::format("anm2ed_audio_{}_{}.f32", std::hash<std::string>{}(pathString), SDL_GetTicks());
      audioPath = temporaryDirectory / path::from_utf8(tempFilenameUtf8);

      std::ofstream audioFile(audioPath, std::ios::binary);

      if (audioFile)
      {
        auto data = (const char*)audioStream.stream.data();
        auto byteCount = audioStream.stream.size() * sizeof(float);
        audioFile.write(data, byteCount);
        audioFile.close();

        auto sampleRate = std::max(audioStream.spec.freq, 1);
        auto channels = std::max(audioStream.spec.channels, 1);
        auto audioDurationFilter = std::string{};
        auto frameCount = (double)std::max((int)framePaths.size(), 1);
        auto expectedDurationSeconds = frameCount / (double)fps;
        if (expectedDurationSeconds > 0.0)
          audioDurationFilter += std::format("apad,atrim=duration={:.9f}", expectedDurationSeconds);

        audioInputArguments =
            std::format("-f f32le -ar {0} -ac {1} -i \"{2}\"", sampleRate, channels, path::to_utf8(audioPath));

        switch (type)
        {
          case render::WEBM:
            audioOutputArguments = "-c:a libopus -b:a 160k -shortest";
            break;
          case render::MP4:
            audioOutputArguments = "-c:a aac -b:a 192k -shortest";
            break;
          default:
            break;
        }

        if (!audioDurationFilter.empty())
          audioOutputArguments = std::format("-af \"{}\" {}", audioDurationFilter, audioOutputArguments);
      }
      else
      {
        logger.warning("Failed to open temporary audio file; exporting video without audio.");
        audio_remove();
      }
    }

    auto framesListPath = temporaryDirectory / path::from_utf8(std::format(
                             "anm2ed_frames_{}_{}.txt", std::hash<std::string>{}(pathString), SDL_GetTicks())) ;
    std::ofstream framesListFile(framesListPath);
    if (!framesListFile)
    {
      audio_remove();
      return false;
    }

    auto defaultFrameDuration = 1.0 / (double)fps;
    for (std::size_t index = 0; index < framePaths.size(); ++index)
    {
      auto framePath = framePaths[index];
      auto framePathString = path::to_utf8(framePath);
      auto frameDuration = defaultFrameDuration;
      if (index < frameDurations.size() && frameDurations[index] > 0.0) frameDuration = frameDurations[index];
      framesListFile << "file '" << ffmpeg_concat_escape(framePathString) << "'\n";
      framesListFile << "duration " << std::format("{:.9f}", frameDuration) << "\n";
    }
    auto lastFramePathString = path::to_utf8(framePaths.back());
    framesListFile << "file '" << ffmpeg_concat_escape(lastFramePathString) << "'\n";
    framesListFile.close();

    auto framesListPathString = path::to_utf8(framesListPath);
    command = std::format("\"{0}\" -y -f concat -safe 0 -i \"{1}\"", ffmpegPathString, framesListPathString);

    if (!audioInputArguments.empty()) command += " " + audioInputArguments;
    if (type != render::GIF) command += std::format(" -fps_mode cfr -r {}", fps);

    switch (type)
    {
      case render::GIF:
        command += " -fps_mode vfr";
        command +=
            " -lavfi \"split[s0][s1];[s0]palettegen=stats_mode=full:reserve_transparent=1[p];"
            "[s1][p]paletteuse=dither=floyd_steinberg:alpha_threshold=128\""
            " -loop 0";
        command += std::format(" \"{}\"", pathString);
        break;
      case render::WEBM:
        command += " -c:v libvpx-vp9 -crf 30 -b:v 0 -pix_fmt yuva420p -row-mt 1 -threads 0 -speed 2 -auto-alt-ref 0";
        if (!audioOutputArguments.empty()) command += " " + audioOutputArguments;
        command += std::format(" \"{}\"", pathString);
        break;
      case render::MP4:
        command += " -vf \"format=yuv420p,scale=trunc(iw/2)*2:trunc(ih/2)*2\" -c:v libx265 -crf 20 -preset slow"
                   " -tag:v hvc1 -movflags +faststart";
        if (!audioOutputArguments.empty()) command += " " + audioOutputArguments;
        command += std::format(" \"{}\"", pathString);
        break;
      default:
      {
        std::error_code ec;
        std::filesystem::remove(framesListPath, ec);
        return false;
      }
    }

#if _WIN32
    command = string::quote(command);
#endif

    logger.command(command);

#if _WIN32
    Process process(command.c_str(), "wb");
#else
    Process process(command.c_str(), "w");
#endif

    if (!process.get())
    {
      std::error_code ec;
      std::filesystem::remove(framesListPath, ec);
      audio_remove();
      return false;
    }

    auto ffmpegExitCode = process.close();
    if (ffmpegExitCode != 0)
    {
      logger.error(std::format("FFmpeg exited with code {} while exporting {}", ffmpegExitCode, pathString));
      std::error_code ec;
      std::filesystem::remove(framesListPath, ec);
      audio_remove();
      return false;
    }

    std::error_code ec;
    std::filesystem::remove(framesListPath, ec);
    audio_remove();
    return true;
  }
}
