/*
   Mathieu Stefani, 24 février 2016

   An API description (reflection) mechanism that is based on Swagger
*/

#pragma once

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <type_traits>
#include <vector>

#include <pistache/http_defs.h>
#include <pistache/iterator_adapter.h>
#include <pistache/mime.h>
#include <pistache/optional.h>
#include <pistache/router.h>

namespace Pistache {
namespace Rest {
namespace Type {

// Data Types

#define DATA_TYPE                                                              \
  TYPE(Integer, std::int32_t, "integer", "int32")                              \
  TYPE(Long, std::int64_t, "integer", "int64")                                 \
  TYPE(Float, float, "number", "float")                                        \
  TYPE(Double, double, "number", "double")                                     \
  TYPE(String, std::string, "string", "")                                      \
  TYPE(Byte, char, "string", "byte")                                           \
  TYPE(Binary, std::vector<std::uint8_t>, "string", "binary")                  \
  TYPE(Bool, bool, "boolean", "")                                              \
  COMPLEX_TYPE(Date, "string", "date")                                         \
  COMPLEX_TYPE(Datetime, "string", "date-time")                                \
  COMPLEX_TYPE(Password, "string", "password")                                 \
  COMPLEX_TYPE(Array, "array", "array")

#define TYPE(rest, cpp, _, __) typedef cpp rest;
#define COMPLEX_TYPE(rest, _, __)                                              \
  struct rest {};
DATA_TYPE
#undef TYPE
#undef COMPLEX_TYPE

} // namespace Type

#define SCHEMES                                                                \
  SCHEME(Http, "http")                                                         \
  SCHEME(Https, "https")                                                       \
  SCHEME(Ws, "ws")                                                             \
  SCHEME(Wss, "wss")

enum class Scheme {
#define SCHEME(e, _) e,
  SCHEMES
#undef SCHEME
};

const char *schemeString(Scheme scheme);

namespace Schema {

namespace Traits {

template <typename DT> struct IsDataType : public std::false_type {};

#define TYPE(rest, _, __, ___)                                                 \
  template <> struct IsDataType<Type::rest> : public std::true_type {};
#define COMPLEX_TYPE(rest, _, __)                                              \
  template <> struct IsDataType<Type::rest> : public std::true_type {};
DATA_TYPE
#undef TYPE
#undef COMPLEX_TYPE

template <typename DT> struct DataTypeInfo;

#define TYPE(rest, _, typeStr, formatStr)                                      \
  template <> struct DataTypeInfo<Type::rest> {                                \
    static const char *typeName() { return typeStr; }                          \
    static const char *format() { return formatStr; }                          \
  };
#define COMPLEX_TYPE(rest, typeStr, formatStr)                                 \
  template <> struct DataTypeInfo<Type::rest> {                                \
    static const char *typeName() { return typeStr; }                          \
    static const char *format() { return formatStr; }                          \
  };
DATA_TYPE
#undef TYPE
#undef COMPLEX_TYPE

template <typename DT> struct DataTypeValidation {
  static bool validate(const std::string &) { return true; }
};

} // namespace Traits

struct ProduceConsume {
  ProduceConsume() : produce(), consume() {}

  std::vector<Http::Mime::MediaType> produce;
  std::vector<Http::Mime::MediaType> consume;
};

struct Contact {
  Contact(std::string name, std::string url, std::string email);

  std::string name;
  std::string url;
  std::string email;
};

struct License {
  License(std::string name, std::string url);

  std::string name;
  std::string url;
};

struct Info {
  Info(std::string title, std::string version, std::string description = "");

  std::string title;
  std::string version;
  std::string description;
  std::string termsOfService;

  Optional<Contact> contact;
  Optional<License> license;
};

struct InfoBuilder {
  explicit InfoBuilder(Info *info);

  InfoBuilder &termsOfService(std::string value);
  InfoBuilder &contact(std::string name, std::string url, std::string email);
  InfoBuilder &license(std::string name, std::string url);

private:
  Info *info_;
};

struct DataType {
  virtual const char *typeName() const = 0;
  virtual const char *format() const = 0;

  virtual bool validate(const std::string &input) const = 0;

  virtual ~DataType() {}
};

template <typename T> struct DataTypeT : public DataType {
  const char *typeName() const override {
    return Traits::DataTypeInfo<T>::typeName();
  }
  const char *format() const override {
    return Traits::DataTypeInfo<T>::format();
  }

  bool validate(const std::string &input) const override {
    return Traits::DataTypeValidation<T>::validate(input);
  }

  virtual ~DataTypeT() {}
};

template <typename T> std::unique_ptr<DataType> makeDataType() {
  static_assert(Traits::IsDataType<T>::value, "Unknown Data Type");
  return std::unique_ptr<DataType>(new DataTypeT<T>());
}

struct Parameter {
  Parameter(std::string name, std::string description);

  template <typename T, typename... Args>
  static Parameter create(Args &&... args) {
    Parameter p(std::forward<Args>(args)...);
    p.type = makeDataType<T>();
    return p;
  }

  std::string name;
  std::string description;
  bool required;
  std::shared_ptr<DataType> type;
};

struct Response {
  Response(Http::Code statusCode, std::string description);

  Http::Code statusCode;
  std::string description;
};

struct ResponseBuilder {
  ResponseBuilder(Http::Code statusCode, std::string description);

  operator Response() const { return response_; }

private:
  Response response_;
};

struct PathDecl {
  PathDecl(std::string value, Http::Method method);

  std::string value;
  Http::Method method;
};

struct Path {
  Path(std::string value, Http::Method method, std::string description);

  std::string value;
  Http::Method method;
  std::string description;
  bool hidden;

  ProduceConsume pc;
  std::vector<Parameter> parameters;
  std::vector<Response> responses;

  Route::Handler handler;

  static std::string swaggerFormat(const std::string &path);

  bool isBound() const { return handler != nullptr; }
};

class PathGroup {
public:
  struct Group : public std::vector<Path> {
    bool isHidden() const {
      if (empty())
        return false;

      return std::all_of(begin(), end(),
                         [](const Path &path) { return path.hidden; });
    }
  };

  typedef std::unordered_map<std::string, Group> Map;
  typedef Map::iterator iterator;
  typedef Map::const_iterator const_iterator;

  typedef std::vector<Path>::iterator group_iterator;

  typedef FlatMapIteratorAdapter<Map> flat_iterator;

  enum class Format { Default, Swagger };

  bool hasPath(const std::string &name, Http::Method method) const;
  bool hasPath(const Path &path) const;

  PathGroup() : groups_() {}

  Group paths(const std::string &name) const;
  Optional<Path> path(const std::string &name, Http::Method method) const;

  group_iterator add(Path path);

  template <typename... Args> group_iterator emplace(Args &&... args) {
    return add(Path(std::forward<Args>(args)...));
  }

  const_iterator begin() const;
  const_iterator end() const;

  flat_iterator flatBegin() const;
  flat_iterator flatEnd() const;

  Map groups() const { return groups_; }

private:
  Map groups_;
};

struct PathBuilder {
  explicit PathBuilder(Path *path);

  template <typename... Mimes> PathBuilder &produces(Mimes... mimes) {
    Http::Mime::MediaType m[sizeof...(Mimes)] = {mimes...};
    std::copy(std::begin(m), std::end(m),
              std::back_inserter(path_->pc.produce));
    return *this;
  }

  template <typename... Mimes> PathBuilder &consumes(Mimes... mimes) {
    Http::Mime::MediaType m[sizeof...(Mimes)] = {mimes...};
    std::copy(std::begin(m), std::end(m),
              std::back_inserter(path_->pc.consume));
    return *this;
  }

  template <typename T>
  PathBuilder &parameter(std::string name, std::string description) {
    path_->parameters.push_back(
        Parameter::create<T>(std::move(name), std::move(description)));
    return *this;
  }

  PathBuilder &response(Http::Code statusCode, std::string description) {
    path_->responses.push_back(Response(statusCode, std::move(description)));
    return *this;
  }

  PathBuilder &response(Response response) {
    path_->responses.push_back(std::move(response));
    return *this;
  }

  /* @CodeDup: should re-use Routes::bind */
  template <typename Result, typename Cls, typename... Args, typename Obj>
  PathBuilder &bind(Result (Cls::*func)(Args...), Obj obj) {

#define CALL_MEMBER_FN(obj, pmf) ((obj)->*(pmf))

    path_->handler = [=](const Rest::Request &request,
                         Http::ResponseWriter response) {
      CALL_MEMBER_FN(obj, func)(request, std::move(response));

      return Route::Result::Ok;
    };

#undef CALL_MEMBER_FN

    return *this;
  }

  template <typename Result, typename... Args>
  PathBuilder &bind(Result (*func)(Args...)) {

    path_->handler = [=](const Rest::Request &request,
                         Http::ResponseWriter response) {
      func(request, std::move(response));

      return Route::Result::Ok;
    };

    return *this;
  }

  PathBuilder &hide(bool value = true) {
    path_->hidden = value;
    return *this;
  }

private:
  Path *path_;
};

struct SubPath {
  SubPath(std::string prefix, PathGroup *paths);

  PathBuilder route(const std::string &name, Http::Method method,
                    std::string description = "");
  PathBuilder route(PathDecl fragment, std::string description = "");

  SubPath path(const std::string &prefix);

  template <typename T>
  void parameter(std::string name, std::string description) {
    parameters.push_back(
        Parameter::create<T>(std::move(name), std::move(description)));
  }

  std::string prefix;
  std::vector<Parameter> parameters;
  PathGroup *paths;
};

} // namespace Schema

class Description {
public:
  Description(std::string title, std::string version,
              std::string description = "");

  Schema::InfoBuilder info();

  Description &host(std::string value);
  Description &basePath(std::string value);

  template <typename... Schemes> Description &schemes(Schemes... _schemes) {
    Scheme s[sizeof...(Schemes)] = {_schemes...};
    std::copy(std::begin(s), std::end(s), std::back_inserter(schemes_));
    return *this;
  }

  template <typename... Mimes> Description &produces(Mimes... mimes) {
    Http::Mime::MediaType m[sizeof...(Mimes)] = {mimes...};
    std::copy(std::begin(m), std::end(m), std::back_inserter(pc.produce));
    return *this;
  }

  template <typename... Mimes> Description &consumes(Mimes... mimes) {
    Http::Mime::MediaType m[sizeof...(Mimes)] = {mimes...};
    std::copy(std::begin(m), std::end(m), std::back_inserter(pc.consume));
    return *this;
  }

  Schema::PathDecl options(std::string name);
  Schema::PathDecl get(std::string name);
  Schema::PathDecl post(std::string name);
  Schema::PathDecl head(std::string name);
  Schema::PathDecl put(std::string name);
  Schema::PathDecl patch(std::string name);
  Schema::PathDecl del(std::string name);
  Schema::PathDecl trace(std::string name);
  Schema::PathDecl connect(std::string name);

  Schema::SubPath path(std::string name);

  Schema::PathBuilder route(std::string name, Http::Method method,
                            std::string description = "");
  Schema::PathBuilder route(Schema::PathDecl fragment,
                            std::string description = "");

  Schema::ResponseBuilder response(Http::Code statusCode,
                                   std::string description);

  Schema::Info rawInfo() const { return info_; }
  std::string rawHost() const { return host_; }
  std::string rawBasePath() const { return basePath_; }
  std::vector<Scheme> rawSchemes() const { return schemes_; }
  Schema::ProduceConsume rawPC() const { return pc; }
  const Schema::PathGroup &rawPaths() const { return paths_; }

private:
  Schema::Info info_;
  std::string host_;
  std::string basePath_;
  std::vector<Scheme> schemes_;
  Schema::ProduceConsume pc;

  Schema::PathGroup paths_;
};

class Swagger {
public:
  explicit Swagger(const Description &description)
      : description_(description), uiPath_(), uiDirectory_(), apiPath_(),
        serializer_() {}

  typedef std::function<std::string(const Description &)> Serializer;

  Swagger &uiPath(std::string path);
  Swagger &uiDirectory(std::string dir);
  Swagger &apiPath(std::string path);
  Swagger &serializer(Serializer serialize);

  void install(Rest::Router &router);

private:
  Description description_;
  std::string uiPath_;
  std::string uiDirectory_;
  std::string apiPath_;
  Serializer serializer_;
};

} // namespace Rest
} // namespace Pistache
