#include "OCIOConfigManager.h"

#include <DDImage/Op.h>
#include <DDImage/Knob.h>

#include <functional>
#include <list>

#include <iostream>

//------------------------------------------------------------------------------
void OCIOConfigObserver::notifyConfigChanged()
{
  if (_configChangedCallback) {
    _configChangedCallback();
  }
};

void OCIOConfigObserver::setConfigChangedCallback(ConfigChangedCallback callback) 
{
  _configChangedCallback = callback;
}

OCIOObservable::OCIOObservable()
: _ocioObservers()
{
}

std::shared_ptr<OCIOConfigObserver> OCIOObservable::createObserver()
{
  // Note: use of make_shared will result in observer object getting created
  // with shared_ptr control block in a single memory allocation. This means
  // that the observer memory will not get released until all shared_ptrs AND
  // all weak_ptrs that point to the observer are destroyed.
  auto observer = std::shared_ptr<OCIOConfigObserver>(new OCIOConfigObserver());
  _ocioObservers.push_back(std::weak_ptr<OCIOConfigObserver>(observer));
  return observer;
}

void OCIOObservable::notifyAllConfigChanged()
{
  auto observerIter = _ocioObservers.begin();
  while (observerIter != _ocioObservers.end()) {
    auto& observer = *observerIter;
    auto sharedObserver = observer.lock();
    if (sharedObserver) {
      sharedObserver->notifyConfigChanged();
      ++observerIter;
    }
    else {
      // Last shared_ptr to this observer has been destroyed. Remove the
      // dangling weak_ptr (the observer object should have already been
      // destroyed).
      observerIter = _ocioObservers.erase(observerIter);
    }
  }
}

OCIOConfigManager::OCIOConfigManager(): _ocioConfigPathMap()
{
}

OCIO::ConstConfigRcPtr OCIOConfigManager::loadConfig(const std::string& configPath)
{
  mFnAssert(!configPath.empty());

  const bool addNewConfig = !hasConfig(configPath);
  if (addNewConfig) {
   addConfig(configPath);
  }

  return getConfig(configPath);
}

OCIO::ConstConfigRcPtr OCIOConfigManager::reloadConfig(const std::string& configPath)
{
  addConfig(configPath);
  return getConfig(configPath);
}

bool OCIOConfigManager::hasConfig(const std::string& configPath) const
{
  mFnAssert(!configPath.empty());

  return (_ocioConfigPathMap.count(configPath) == 1);
}

void OCIOConfigManager::addConfig(const std::string& configPath)
{
  mFnAssert(!configPath.empty());

  try {
    OCIO::ConstConfigRcPtr config = OCIO::Config::CreateFromFile(configPath.c_str());
    _ocioConfigPathMap[configPath] = { config, config->getCacheID() };
  }
  catch (OCIO::Exception& exception) {
    _ocioConfigPathMap[configPath] = { OCIO::ConstConfigRcPtr(), std::string() };
  }
}

OCIO::ConstConfigRcPtr OCIOConfigManager::getConfig(const std::string& configPath) const
{
  mFnAssert(!configPath.empty());
  mFnAssert(hasConfig(configPath));

  const auto& configCachePair = _ocioConfigPathMap.at(configPath);
  return configCachePair.first;
}

/*static*/ std::shared_ptr<OCIOConfigObserver> NukeOCIOConfigManager::CreateObserver()
{
  return Get()._ocioSubject->createObserver();
}

/*static*/ NukeOCIOConfigManager& NukeOCIOConfigManager::Get()
{
  static NukeOCIOConfigManager* ocioConfigManager = new NukeOCIOConfigManager;
  return *ocioConfigManager;
}

/*static*/ OCIO::ConstConfigRcPtr NukeOCIOConfigManager::LoadConfig(const DD::Image::Op* op)
{
  return Get().loadConfig(op);
}

NukeOCIOConfigManager::NukeOCIOConfigManager()
: _ocioConfigManager()
, _ocioSubject(std::make_unique<OCIOObservable>())
{
}

OCIO::ConstConfigRcPtr NukeOCIOConfigManager::loadConfig(const DD::Image::Op* op)
{
  // We have to register for changes here because its the only place we have access to the op
  registerForChangesOnRootNode(op);

  _configPath = getConfigPath(op);
  return _ocioConfigManager.loadConfig(_configPath);
}

std::string NukeOCIOConfigManager::getConfigPath(const DD::Image::Op* op) const
{
  const DD::Image::Op* rootOp = op->rootOp();
  mFnAssert(rootOp);

  const DD::Image::Knob* ocioConfigKnob = rootOp->knob("OCIOConfigPath");
  mFnAssert(ocioConfigKnob);

  // Pass in a dummy OutputContext to get_text(), so that the File_Knob remaps the path it returns if needed.
  const DD::Image::OutputContext outputContext;
  const char* configPath = ocioConfigKnob->get_text(&outputContext);
  mFnAssert(configPath);

  return std::string(configPath);
}

void NukeOCIOConfigManager::registerForChangesOnRootNode(const DD::Image::Op* op)
{
  const auto root = op->rootOp()->getNode();
  root->registerKnobChangedObserver(this);
}

void NukeOCIOConfigManager::observedKnobChanged(DD::Image::Knob* knob)
{
  if (knob->is("reloadConfig")) {
    _ocioConfigManager.reloadConfig(_configPath);
  }
  else if (knob->is("colorManagement") || knob->is("OCIOConfigPath")) {
    // TODO Observers will get callbacks for all root nodes that have been 
    // registered for knob changed events in registerForChangesOnRootNode().
    // This is because we never call unregisterKnobChangedObserver() for
    // root nodes. As a result, observers may need to check if the config
    // returned by loadConfig() has actually changed in their config changed
    // callback.
    _ocioSubject->notifyAllConfigChanged();
  }
}