#include "FWCore/SharedMemory/interface/ControllerChannel.h"
#include "FWCore/SharedMemory/interface/WorkerChannel.h"
#include "FWCore/Utilities/interface/Exception.h"

#include <memory>
#include <string>
#include <stdio.h>
#include <cassert>
#include <thread>
namespace {
  int controller(int argc, char** argv) {
    using namespace edm::shared_memory;

    ControllerChannel channel("TestChannel", 0, 5);

    //Pipe has to close AFTER we tell the worker to stop
    auto closePipe = [](FILE* iFile) { pclose(iFile); };
    std::unique_ptr<FILE, decltype(closePipe)> pipe(nullptr, closePipe);

    auto stopWorkerCmd = [](ControllerChannel* iChannel) { iChannel->stopWorker(); };
    std::unique_ptr<ControllerChannel, decltype(stopWorkerCmd)> stopWorkerGuard(&channel, stopWorkerCmd);

    {
      std::string command(argv[0]);
      command += " ";
      command += channel.sharedMemoryName();
      command += " ";
      command += channel.uniqueID();
      //make sure output is flushed before popen does any writing
      fflush(stdout);
      fflush(stderr);

      channel.setupWorkerWithRetry(
          [&]() {
            pipe.reset(popen(command.c_str(), "w"));

            if (not pipe) {
              throw cms::Exception("PipeFailed") << "pipe failed to open " << command;
            }
          },
          []() {
            std::cerr << " retry requested\n";
            return true;
          });
    }
    {
      *channel.toWorkerBufferInfo() = {0, 0};
      auto result = channel.doTransitionWithRetry(
          [&]() {
            if (channel.fromWorkerBufferInfo()->index_ != 1) {
              throw cms::Exception("BadValue") << "wrong index value of fromWorkerBufferInfo "
                                               << static_cast<int>(channel.fromWorkerBufferInfo()->index_);
            }
            if (channel.fromWorkerBufferInfo()->identifier_ != 1) {
              throw cms::Exception("BadValue")
                  << "wrong identifier value of fromWorkerBufferInfo " << channel.fromWorkerBufferInfo()->identifier_;
            }
            if (not channel.shouldKeepEvent()) {
              throw cms::Exception("BadValue") << "told not to keep event";
            }
          },
          []() {
            std::cerr << " retry requested 0\n";
            return true;
          },
          edm::Transition::Event,
          2);
      if (not result) {
        throw cms::Exception("TimeOut") << "doTransition timed out";
      }
    }
    {
      *channel.toWorkerBufferInfo() = {1, 1};
      auto result = channel.doTransitionWithRetry(
          [&]() {
            if (channel.fromWorkerBufferInfo()->index_ != 0) {
              throw cms::Exception("BadValue") << "wrong index value of fromWorkerBufferInfo "
                                               << static_cast<int>(channel.fromWorkerBufferInfo()->index_);
            }
            if (channel.fromWorkerBufferInfo()->identifier_ != 2) {
              throw cms::Exception("BadValue")
                  << "wrong identifier value of fromWorkerBufferInfo " << channel.fromWorkerBufferInfo()->identifier_;
            }
            if (channel.shouldKeepEvent()) {
              throw cms::Exception("BadValue") << "told to keep event";
            }
          },
          []() {
            std::cerr << " retry requested 1\n";
            return true;
          },

          edm::Transition::Event,
          3);
      if (not result) {
        throw cms::Exception("TimeOut") << "doTransition timed out";
      }
    }

    {
      auto result = channel.doTransitionWithRetry([&]() {},
                                                  []() {
                                                    std::cerr << " retry requested 2\n";
                                                    return true;
                                                  },
                                                  edm::Transition::EndLuminosityBlock,
                                                  1);
      if (not result) {
        throw cms::Exception("TimeOut") << "doTransition timed out";
      }
    }

    //std::cout <<"controller going to stop"<<std::endl;
    return 0;
  }

  int worker(int argc, char** argv) {
    using namespace edm::shared_memory;

    using namespace std::chrono_literals;
    std::this_thread::sleep_for(20s);
    assert(argc == 3);
    WorkerChannel channel(argv[1], argv[2]);

    std::cerr << "worker setup\n";
    using namespace std::chrono_literals;
    std::this_thread::sleep_for(15s);

    std::cerr << "  worker setup awake\n";

    channel.workerSetupDone();
    std::cerr << "workerSetupDone finished\n";

    using namespace std::chrono_literals;
    std::this_thread::sleep_for(15s);

    int transitionCount = 0;
    channel.handleTransitions([&](edm::Transition iTransition, unsigned long long iTransitionID) {
      std::cerr << " transition\n";
      using namespace std::chrono_literals;
      std::this_thread::sleep_for(15s);

      switch (transitionCount) {
        case 0: {
          if (iTransition != edm::Transition::Event) {
            throw cms::Exception("BadValue") << "wrong transition received " << static_cast<int>(iTransition);
          }
          if (iTransitionID != 2ULL) {
            throw cms::Exception("BadValue") << "wrong transitionID received " << static_cast<int>(iTransitionID);
          }

          if (channel.toWorkerBufferInfo()->index_ != 0) {
            throw cms::Exception("BadValue")
                << "wrong toWorkerBufferInfo index received " << static_cast<int>(channel.toWorkerBufferInfo()->index_);
          }
          if (channel.toWorkerBufferInfo()->identifier_ != 0) {
            throw cms::Exception("BadValue")
                << "wrong toWorkerBufferInfo identifier received " << channel.toWorkerBufferInfo()->identifier_;
          }
          *channel.fromWorkerBufferInfo() = {1, 1};
          channel.shouldKeepEvent(true);
          break;
        }

        case 1: {
          if (iTransition != edm::Transition::Event) {
            throw cms::Exception("BadValue") << "wrong transition received " << static_cast<int>(iTransition);
          }
          if (iTransitionID != 3ULL) {
            throw cms::Exception("BadValue") << "wrong transitionID received " << static_cast<int>(iTransitionID);
          }

          if (channel.toWorkerBufferInfo()->index_ != 1) {
            throw cms::Exception("BadValue")
                << "wrong toWorkerBufferInfo index received " << static_cast<int>(channel.toWorkerBufferInfo()->index_);
          }
          if (channel.toWorkerBufferInfo()->identifier_ != 1) {
            throw cms::Exception("BadValue")
                << "wrong toWorkerBufferInfo identifier received " << channel.toWorkerBufferInfo()->identifier_;
          }
          *channel.fromWorkerBufferInfo() = {2, 0};
          channel.shouldKeepEvent(false);
          break;
        }

        case 2: {
          if (iTransition != edm::Transition::EndLuminosityBlock) {
            throw cms::Exception("BadValue") << "wrong transition received " << static_cast<int>(iTransition);
          }
          if (iTransitionID != 1ULL) {
            throw cms::Exception("BadValue") << "wrong transitionID received " << static_cast<int>(iTransitionID);
          }
          break;
        }
        default: {
          throw cms::Exception("MissingStop") << "stopRequested not set";
        }
      }
      ++transitionCount;
    });
    if (transitionCount != 3) {
      throw cms::Exception("MissingStop") << "stop requested too soon " << transitionCount;
    }
    return 0;
  }
  const char* jobType(bool isWorker) {
    if (isWorker) {
      return "Worker";
    }
    return "Controller";
  }

}  // namespace

int main(int argc, char** argv) {
  bool isWorker = true;
  int retValue = 0;
  try {
    if (argc > 1) {
      retValue = worker(argc, argv);
    } else {
      isWorker = false;
      retValue = controller(argc, argv);
    }
  } catch (std::exception const& iException) {
    std::cerr << "Caught exception\n" << iException.what() << "\n";
    if (isWorker) {
      std::cerr << "in worker\n";
    } else {
      std::cerr << "in controller\n";
    }
    return 1;
  }
  if (0 == retValue) {
    std::cout << jobType(isWorker) << " success" << std::endl;
  } else {
    std::cout << jobType(isWorker) << " failed" << std::endl;
  }
  return 0;
}
