/*
 * nghttp2 - HTTP/2 C Library
 *
 * Copyright (c) 2016 Tatsuhiro Tsujikawa
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include "shrpx_dns_resolver.h"

#include <cstring>
#include <sys/time.h>

#include "shrpx_log.h"
#include "shrpx_connection.h"
#include "shrpx_config.h"

namespace shrpx {

namespace {
void sock_state_cb(void *data, int s, int read, int write) {
  auto resolv = static_cast<DNSResolver *>(data);

  if (resolv->get_status(nullptr) != DNS_STATUS_RUNNING) {
    return;
  }

  if (read) {
    resolv->start_rev(s);
  } else {
    resolv->stop_rev(s);
  }
  if (write) {
    resolv->start_wev(s);
  } else {
    resolv->stop_wev(s);
  }
}
} // namespace

namespace {
void host_cb(void *arg, int status, int timeouts, hostent *hostent) {
  auto resolv = static_cast<DNSResolver *>(arg);
  resolv->on_result(status, hostent);
}
} // namespace

namespace {
void process_result(DNSResolver *resolv) {
  auto cb = resolv->get_complete_cb();
  if (!cb) {
    return;
  }
  Address result;
  auto status = resolv->get_status(&result);
  switch (status) {
  case DNS_STATUS_OK:
  case DNS_STATUS_ERROR:
    cb(status, &result);
    break;
  }
  // resolv may be deleted here.
}
} // namespace

namespace {
void readcb(struct ev_loop *loop, ev_io *w, int revents) {
  auto resolv = static_cast<DNSResolver *>(w->data);
  resolv->on_read(w->fd);
  process_result(resolv);
}
} // namespace

namespace {
void writecb(struct ev_loop *loop, ev_io *w, int revents) {
  auto resolv = static_cast<DNSResolver *>(w->data);
  resolv->on_write(w->fd);
  process_result(resolv);
}
} // namespace

namespace {
void timeoutcb(struct ev_loop *loop, ev_timer *w, int revents) {
  auto resolv = static_cast<DNSResolver *>(w->data);
  resolv->on_timeout();
  process_result(resolv);
}
} // namespace

namespace {
void stop_ev(struct ev_loop *loop,
             const std::vector<std::unique_ptr<ev_io>> &evs) {
  for (auto &w : evs) {
    ev_io_stop(loop, w.get());
  }
}
} // namespace

DNSResolver::DNSResolver(struct ev_loop *loop)
    : result_{},
      loop_(loop),
      channel_(nullptr),
      family_(AF_UNSPEC),
      status_(DNS_STATUS_IDLE) {
  ev_timer_init(&timer_, timeoutcb, 0., 0.);
  timer_.data = this;
}

DNSResolver::~DNSResolver() {
  if (channel_) {
    ares_destroy(channel_);
  }

  stop_ev(loop_, revs_);
  stop_ev(loop_, wevs_);

  ev_timer_stop(loop_, &timer_);
}

int DNSResolver::resolve(const StringRef &name, int family) {
  if (status_ != DNS_STATUS_IDLE) {
    return -1;
  }

  if (LOG_ENABLED(INFO)) {
    LOG(INFO) << "Start resolving host " << name << " in IPv"
              << (family == AF_INET ? "4" : "6");
  }

  name_ = name;
  family_ = family;

  int rv;

  auto &dnsconf = get_config()->dns;

  ares_options opts{};
  opts.sock_state_cb = sock_state_cb;
  opts.sock_state_cb_data = this;
  opts.timeout = static_cast<int>(dnsconf.timeout.lookup * 1000);
  opts.tries = dnsconf.max_try;

  auto optmask = ARES_OPT_SOCK_STATE_CB | ARES_OPT_TIMEOUTMS | ARES_OPT_TRIES;

  ares_channel chan;
  rv = ares_init_options(&chan, &opts, optmask);
  if (rv != ARES_SUCCESS) {
    if (LOG_ENABLED(INFO)) {
      LOG(INFO) << "ares_init_options failed: " << ares_strerror(rv);
    }
    status_ = DNS_STATUS_ERROR;
    return -1;
  }

  channel_ = chan;
  status_ = DNS_STATUS_RUNNING;

  ares_gethostbyname(channel_, name_.c_str(), family_, host_cb, this);
  reset_timeout();

  return 0;
}

int DNSResolver::on_read(int fd) { return handle_event(fd, ARES_SOCKET_BAD); }

int DNSResolver::on_write(int fd) { return handle_event(ARES_SOCKET_BAD, fd); }

int DNSResolver::on_timeout() {
  return handle_event(ARES_SOCKET_BAD, ARES_SOCKET_BAD);
}

int DNSResolver::handle_event(int rfd, int wfd) {
  if (status_ == DNS_STATUS_IDLE) {
    return -1;
  }

  ares_process_fd(channel_, rfd, wfd);

  switch (status_) {
  case DNS_STATUS_RUNNING: {
    reset_timeout();
    return 0;
  }
  case DNS_STATUS_OK:
    return 0;
  case DNS_STATUS_ERROR:
    return -1;
  default:
    // Unreachable
    assert(0);
  }
}

void DNSResolver::reset_timeout() {
  if (status_ != DNS_STATUS_RUNNING) {
    return;
  }
  timeval tvout;
  auto tv = ares_timeout(channel_, nullptr, &tvout);
  if (tv == nullptr) {
    return;
  }
  // To avoid that timer_.repeat becomes 0, which makes ev_timer_again
  // useless, add tiny fraction of time.
  timer_.repeat = tv->tv_sec + tv->tv_usec / 1000000. + 1e-9;
  ev_timer_again(loop_, &timer_);
}

int DNSResolver::get_status(Address *result) const {
  if (status_ != DNS_STATUS_OK) {
    return status_;
  }

  if (result) {
    memcpy(result, &result_, sizeof(result_));
  }

  return status_;
}

namespace {
void start_ev(std::vector<std::unique_ptr<ev_io>> &evs, struct ev_loop *loop,
              int fd, int event, IOCb cb, void *data) {
  for (auto &w : evs) {
    if (w->fd == fd) {
      return;
    }
  }
  for (auto &w : evs) {
    if (w->fd == -1) {
      ev_io_set(w.get(), fd, event);
      ev_io_start(loop, w.get());
      return;
    }
  }

  auto w = make_unique<ev_io>();
  ev_io_init(w.get(), cb, fd, event);
  w->data = data;
  ev_io_start(loop, w.get());
  evs.emplace_back(std::move(w));
}
} // namespace

namespace {
void stop_ev(std::vector<std::unique_ptr<ev_io>> &evs, struct ev_loop *loop,
             int fd, int event) {
  for (auto &w : evs) {
    if (w->fd == fd) {
      ev_io_stop(loop, w.get());
      ev_io_set(w.get(), -1, event);
      return;
    }
  }
}
} // namespace

void DNSResolver::start_rev(int fd) {
  start_ev(revs_, loop_, fd, EV_READ, readcb, this);
}

void DNSResolver::stop_rev(int fd) { stop_ev(revs_, loop_, fd, EV_READ); }

void DNSResolver::start_wev(int fd) {
  start_ev(wevs_, loop_, fd, EV_WRITE, writecb, this);
}

void DNSResolver::stop_wev(int fd) { stop_ev(wevs_, loop_, fd, EV_WRITE); }

void DNSResolver::on_result(int status, hostent *hostent) {
  stop_ev(loop_, revs_);
  stop_ev(loop_, wevs_);
  ev_timer_stop(loop_, &timer_);

  if (status != ARES_SUCCESS) {
    if (LOG_ENABLED(INFO)) {
      LOG(INFO) << "Name lookup for " << name_
                << " failed: " << ares_strerror(status);
    }
    status_ = DNS_STATUS_ERROR;
    return;
  }

  auto ap = *hostent->h_addr_list;
  if (!ap) {
    if (LOG_ENABLED(INFO)) {
      LOG(INFO) << "Name lookup for " << name_ << "failed: no address returned";
    }
    status_ = DNS_STATUS_ERROR;
    return;
  }

  switch (hostent->h_addrtype) {
  case AF_INET:
    status_ = DNS_STATUS_OK;
    result_.len = sizeof(result_.su.in);
    result_.su.in = {};
    result_.su.in.sin_family = AF_INET;
#ifdef HAVE_SOCKADDR_IN_SIN_LEN
    result_.su.in.sin_len = sizeof(result_.su.in);
#endif // HAVE_SOCKADDR_IN_SIN_LEN
    memcpy(&result_.su.in.sin_addr, ap, sizeof(result_.su.in.sin_addr));
    break;
  case AF_INET6:
    status_ = DNS_STATUS_OK;
    result_.len = sizeof(result_.su.in6);
    result_.su.in6 = {};
    result_.su.in6.sin6_family = AF_INET6;
#ifdef HAVE_SOCKADDR_IN6_SIN6_LEN
    result_.su.in6.sin6_len = sizeof(result_.su.in6);
#endif // HAVE_SOCKADDR_IN6_SIN6_LEN
    memcpy(&result_.su.in6.sin6_addr, ap, sizeof(result_.su.in6.sin6_addr));
    break;
  default:
    assert(0);
  }

  if (status_ == DNS_STATUS_OK) {
    if (LOG_ENABLED(INFO)) {
      LOG(INFO) << "Name lookup succeeded: " << name_ << " -> "
                << util::numeric_name(&result_.su.sa, result_.len);
    }
    return;
  }

  status_ = DNS_STATUS_ERROR;
}

void DNSResolver::set_complete_cb(CompleteCb cb) {
  completeCb_ = std::move(cb);
}

CompleteCb DNSResolver::get_complete_cb() const { return completeCb_; }

} // namespace shrpx
