// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm
// VideoStitch Autocrop

// VideoStitch SDK
#include <libvideostitch/logging.hpp>
#include <libvideostitch/parse.hpp>
#include <libvideostitch/imageProcessingUtils.hpp>
#include <fstream>
#include <iostream>
#include <sstream>
#include <opencv2/imgproc.hpp>

// System-dependant filesystem stuff.
#ifdef _MSC_VER
#include <opencv2/imgcodecs.hpp>
#include <direct.h>
#define chdir _chdir
#define snprintf _snprintf
#define getcwd _getcwd
#include <io.h>
#include <sys/types.h>
#include <sys/stat.h>
const char dirSep = '\\';
#else
#include <unistd.h>
const char dirSep = '/';
#endif

#ifdef _MSC_VER
#include <Windows.h>
#endif
#include <iostream>
#include <cassert>
#include <memory>

#ifndef _MSC_VER
#include <dirent.h>
#include <dlfcn.h>
#else
#include <libvideostitch/win32/dirent.h>
#endif

using namespace VideoStitch;

namespace {
/**
 * @brief Prints the executable usage.
 * @param execName Name of the executable (argv[0]).
 * @param os Output stream: e.g; std::cerr to output in the standard error stream.
 */
void printUsage(const char* execName, ThreadSafeOstream& os) {
  os << "Usage: " << execName << " -i <template> -o <output.json> [options]" << std::endl;
  os << "  -i <template.png>: can be an image file / or directory that contains all inputs for detection" << std::endl
#ifdef _MSC_VER
     << " Note that only PNG, JPEG, JPG files are supported." << std::endl
#else
     << " Note that only PNG file is supported." << std::endl
#endif
     << " This option can be used several times." << std::endl;
  os << "<output.json> the output json file to be written." << std::endl;
  os << "[options] are:" << std::endl;
  os << "  -d: Set this option to dump debug images." << std::endl;
  os << std::endl;
}

/**
 * @brief fileExists checks the existence of a file.
 * @param filename The input full filename.
 * @return true if the file exists, false otherwise.
 */
bool fileExists(const std::string& filename) {
  std::ifstream file(filename.c_str());
  if (file.good()) {
    file.close();
    return true;
  } else {
    file.close();
    return false;
  }
}

/**
 * Give a pointer on the file component of a full input filename.
 * @param input The input full filename.
 * @return the filename, belonging to the same char* as @input.
 */
const char* extractFilename(const char* input) {
  const char* file = input;
  // separator is '\' or '/'
  for (const char* p = input; *p != 0; ++p) {
    if (*p == '/' || *p == '\\') {
      file = p + 1;
    }
  }
  return file;
}

/**
 * List files with a certain extension in a directory.
 * @param directory: directory to list.
 * @param ext: the file prefix (may begin with '.').
 * @return A list of matching files.
 */
std::vector<std::string> listDirectory(const std::string& directory, const std::string& ext) {
  size_t extSize = ext.size();
  std::vector<std::string> filenames;
  DIR* dir = opendir(directory.c_str());
  if (dir) {
    /*print all the files and directories within directory*/
    struct dirent* ent = readdir(dir);
    while (ent) {
      std::string str(ent->d_name);
      ent = readdir(dir);
      if (str.size() > extSize) {
        std::string strLower = str;
        std::transform(strLower.begin(), strLower.end(), strLower.begin(), ::tolower);
        if (strLower.substr(strLower.size() - extSize) == ext) {
          filenames.push_back(directory + dirSep + str);
          continue;
        }
      }
    }
    closedir(dir);
  }
  return filenames;
}

Status readImage(const std::string& filename, int64_t& width, int64_t& height, std::vector<unsigned char>& data) {
  int channelCount;
  return VideoStitch::Util::ImageProcessing::readImage(filename, width, height, channelCount, data);
}

int extractArg(int argc, char** argv, std::vector<std::string>& inputFilenames, std::string& outputFilename,
               bool& outputDebugImage, std::string& algoSettingFilename) {
  // Check if the output folder/file is writable
  const std::string program = std::string(argv[0]);
  const size_t found = program.find_last_of("/\\");
  std::string executableDirectory = program.substr(0, found);
  auto& errLog = Logger::get(Logger::Error);
  char currentDir[1024];
  if (getcwd(currentDir, 1024)) {
    executableDirectory = (executableDirectory.empty() || executableDirectory == std::string(argv[0]))
                              ? std::string(currentDir)
                              : executableDirectory;
  }

  // Parse command line
  std::vector<std::string> inputDirs;
  outputDebugImage = false;
  outputFilename = "";
  for (int i = 1; i < argc; ++i) {
    if (argv[i][0] != '\0' && argv[i][1] != '\0' && argv[i][0] == '-') {
      switch (argv[i][1]) {
        case 'i': /* input-directory or input-file*/
          if (i >= argc - 1) {
            errLog << "The -i option takes a parameter." << std::endl;
            printUsage(argv[0], errLog);
            return -1;
          }
          ++i;
          while (argv[i][0] != '-') {
            inputDirs.push_back(argv[i]);
            i++;
            if (i == argc) {
              break;
            }
          }
          i--;
          break;
        case 'o': /* output-file name*/
          outputFilename = argv[++i];
          break;
        case 'd': /* output debug images*/
          outputDebugImage = true;
          break;
        case 's':
          algoSettingFilename = argv[++i];
          break;
        default:
          errLog << "No such option: " << argv[i] << std::endl << std::endl;
          printUsage(argv[0], errLog);
          return -1;
      }
    } else {
      errLog << "No such option: " << argv[i] << std::endl;
      printUsage(argv[0], errLog);
      return -1;
    }
  }

#ifdef _MSC_VER
  const std::vector<std::string> exts = {std::string("png"), std::string("jpeg"), std::string("jpg")};
#else
  const std::vector<std::string> exts = {std::string("png")};
#endif

  for (auto inputDir : inputDirs) {
    if (fileExists(inputDir)) {
      std::string strLower = inputDir;
      std::transform(strLower.begin(), strLower.end(), strLower.begin(), ::tolower);
      for (auto ext : exts) {
        if ((strLower.substr(strLower.size() - ext.size()) == ext)) {
          inputFilenames.push_back(inputDir);
          break;
        }
      }

    } else {
      for (auto ext : exts) {
        std::vector<std::string> newFilenames = listDirectory(inputDir, ext);
        inputFilenames.insert(inputFilenames.end(), newFilenames.begin(), newFilenames.end());
      }
    }
  }

  if (!inputFilenames.size()) {
    errLog << "Missing input file. Use -i for file or folder." << std::endl;
    printUsage(argv[0], errLog);
    return -1;
  }
  if (!outputFilename.size()) {
    errLog << "Missing output file. Use -o to specify the output file." << std::endl;
    printUsage(argv[0], errLog);
    return -1;
  }
  return 0;
}

}  // namespace

int main(int argc, char** argv) {
  std::vector<std::string> inputFilenames;
  std::string outputFilename;
  bool outputDebugImage;
  std::string algoSettingFilename;
  auto& errorLog = Logger::get(Logger::Error);

  // Extract command-line parameters
  if (extractArg(argc, argv, inputFilenames, outputFilename, outputDebugImage, algoSettingFilename) < 0) {
    return -1;
  }

  // Extract algorithm parameters
  std::unique_ptr<Ptv::Value> algoConfig = nullptr;
  if (algoSettingFilename.length() > 0) {
    auto& errLog = Logger::get(Logger::Error);
    Potential<Ptv::Parser> algoParser = Ptv::Parser::create();
    if (!algoParser->parse(algoSettingFilename)) {
      errLog << "Error: Cannot parse algos config PTV file: " << algoSettingFilename << std::endl;
      errLog << algoParser->getErrorMessage() << std::endl;
      return -1;
    }

    const Ptv::Value& algos = algoParser->getRoot();
    if (!(algos.has("algorithms") && algos.has("algorithms")->getType() == Ptv::Value::LIST)) {
      errLog << "Invalid algorithms file." << std::endl;
      return -1;
    }
    const std::vector<Ptv::Value*>& algoDefs = algos.has("algorithms")->asList();
    for (size_t i = 0; i < algoDefs.size(); ++i) {
      const Ptv::Value* algoDef = algoDefs[i];
      if (algoDef->has("name")->asString().compare("autocrop") == 0) {
        algoConfig.reset(algoDef->has("config")->clone());
        break;
      }
    }
  } else {
    algoConfig.reset(Ptv::Value::stringObject("fake"));
  }

  std::unique_ptr<Ptv::Value> jsonInputs(Ptv::Value::emptyObject());
  jsonInputs->asList();
  int success = 0;
  int fail = 0;
  for (auto filename : inputFilenames) {
    bool getCircle = true;
    // This likely is the visualization file --> skip it
    if (filename.find("_circle.png") != std::string::npos) {
      continue;
    }
    int64_t width, height;
    std::vector<unsigned char> data;
    readImage(filename, width, height, data);
    int x, y, radius;

    Status status = VideoStitch::Util::ImageProcessing::findCropCircle(
        (int)width, (int)height, &data[0], x, y, radius, algoConfig.get(), outputDebugImage ? &filename : nullptr);

    errorLog << "Input File '" << filename << "': ";
    if (status.ok()) {
      errorLog << "crop circle found!" << std::endl;
    } else {
      errorLog << "could not find crop circle!" << std::endl;
      getCircle = false;
    }

    Ptv::Value* res = Ptv::Value::emptyObject();
    res->push("reader_config", Ptv::Value::stringObject(filename));
    res->push("width", Ptv::Value::intObject(width));
    res->push("height", Ptv::Value::intObject(height));
    res->push("proj", Ptv::Value::stringObject(std::string("circular_fisheye_opt")));

    res->push("crop_left", Ptv::Value::intObject(x - radius));
    res->push("crop_right", Ptv::Value::intObject(x + radius));
    res->push("crop_top", Ptv::Value::intObject(y - radius));
    res->push("crop_bottom", Ptv::Value::intObject(y + radius));

    // Supplementary data
    res->push("center_x", Ptv::Value::intObject(x));
    res->push("center_y", Ptv::Value::intObject(y));
    res->push("radius", Ptv::Value::intObject(radius));
    if (outputDebugImage) {
      std::string outputFilePath = filename + "_circle.png";
      res->push("output_circle", Ptv::Value::stringObject(outputFilePath));
    }
    jsonInputs->asList().push_back(res);
    std::ofstream ofs(outputFilename);
    jsonInputs->printJson(ofs);
    if (ofs.fail()) {
      errorLog << "Could not write result to json file!" << std::endl;
      getCircle = false;
    }
    ofs.close();

    if (getCircle) {
      success++;
    } else {
      fail++;
    }
  }
  if (fail == 0) {
    std::cout << "Output Json file was written to: " << outputFilename << std::endl;
  } else if (success > 0) {
    std::cout << "Only " << success << " over " << success + fail << " results were written to: " << outputFilename
              << std::endl;
  } else {
    std::cout << "Failed to write result to: " << outputFilename << std::endl;
  }

  if (fail == 0) {
    return 0;
  } else {
    return -1;
  }
}