/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jooby.internal.undertow;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.net.ssl.SSLContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnio.Option;
import org.xnio.Options;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigValue;

import io.undertow.Undertow;
import io.undertow.Undertow.Builder;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpHandler;
import io.undertow.server.handlers.GracefulShutdownHandler;

public class UndertowServer implements org.jooby.spi.Server {

  private interface SetOption {
    void set(String name);
  }

  /** The logging system. */
  private static final Logger log = LoggerFactory.getLogger(org.jooby.spi.Server.class);

  private Undertow server;

  private final GracefulShutdownHandler shutdown;

  private long awaitShutdown;

  @Inject
  public UndertowServer(final org.jooby.spi.HttpHandler dispatcher, final Config conf,
      final Provider<SSLContext> sslContext) throws Exception {

    awaitShutdown = conf.getDuration("undertow.awaitShutdown", TimeUnit.MILLISECONDS);
    boolean http2 = conf.getBoolean("server.http2.enabled");
    shutdown = new GracefulShutdownHandler(doHandler(dispatcher, conf));
    Undertow.Builder ubuilder = configure(conf, io.undertow.Undertow.builder())
        .addHttpListener(conf.getInt("application.port"),
            host(conf.getString("application.host")));

    ubuilder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2);

    boolean securePort = conf.hasPath("application.securePort");

    if (securePort) {
      ubuilder.addHttpsListener(conf.getInt("application.securePort"),
          host(conf.getString("application.host")), sslContext.get());
    }

    this.server = ubuilder.setHandler(shutdown)
        .build();
  }

  private String host(final String host) {
    return "localhost".equals(host) ? "0.0.0.0" : host;
  }

  @SuppressWarnings("unchecked")
  static Builder configure(final Config config, final Builder builder) {
    Config $undertow = config.getConfig("undertow");

    set($undertow, "bufferSize", name -> {
      int value = $undertow.getBytes(name).intValue();
      log.debug("undertow.bufferSize({})", value);
      builder.setBufferSize(value);
    });

    set($undertow, "directBuffers", name -> {
      boolean value = $undertow.getBoolean(name);
      log.debug("undertow.directBuffers({})", value);
      builder.setDirectBuffers(value);
    });

    set($undertow, "ioThreads", name -> {
      int value = $undertow.getInt(name);
      log.debug("undertow.ioThreads({})", value);
      builder.setIoThreads(value);
    });

    set($undertow, "workerThreads", name -> {
      int value = $undertow.getInt(name);
      log.debug("undertow.workerThreads({})", value);
      builder.setWorkerThreads(value);
    });

    $undertow.getConfig("server").root().entrySet()
        .forEach(setOption($undertow, "server",
            (option, value) -> builder.setServerOption(option, value)));

    $undertow.getConfig("worker").root().entrySet()
        .forEach(setOption($undertow, "worker",
            (option, value) -> builder.setWorkerOption(option, value)));

    $undertow.getConfig("socket").root().entrySet()
        .forEach(setOption($undertow, "socket",
            (option, value) -> builder.setSocketOption(option, value)));

    return builder;
  }

  @SuppressWarnings("rawtypes")
  private static Consumer<Map.Entry<String, ConfigValue>> setOption(final Config config,
      final String level,
      final BiConsumer<Option, Object> setter) {
    return entry -> {
      String name = entry.getKey();
      Object value = entry.getValue().unwrapped();
      Option option = findOption(name, UndertowOptions.class, Options.class);
      if (option != null) {
        // parse option to adjust correct type
        Object parsedValue = value.toString();
        try {
          parsedValue = option.parseValue(value.toString(), null);
        } catch (NumberFormatException ex) {
          // try bytes
          try {
            parsedValue = option.parseValue(config.getBytes(level + "." + name).toString(), null);
          } catch (ConfigException.BadValue badvalue) {
            // try duration
            parsedValue = option.parseValue(
                config.getDuration(level + "." + name, TimeUnit.MILLISECONDS) + "", null);
          }
        }
        log.debug("{}.{}({})", level, option.getName(), parsedValue);
        setter.accept(option, parsedValue);
      } else {
        log.error("Unknown option: 'undertow.{}.{} = {}'", level, name, value);
      }
    };
  }

  @SuppressWarnings("rawtypes")
  private static Option findOption(final String name, final Class... where) {
    for (Class owner : where) {
      try {
        Field fld = owner.getDeclaredField(name);
        return (Option) fld.get(null);
      } catch (NoSuchFieldException | IllegalAccessException ex) {
        log.trace("Unknown option: '{}'", name);
      }
    }
    return null;
  }

  private static void set(final Config config, final String name, final SetOption setter) {
    if (config.hasPath(name)) {
      setter.set(name);
    }
  }

  private static HttpHandler doHandler(final org.jooby.spi.HttpHandler dispatcher,
      final Config config) {
    return new UndertowHandler(dispatcher, config);
  }

  @Override
  public void start() throws Exception {
    server.start();
  }

  @Override
  public void join() throws InterruptedException {
    // NOOP
  }

  @Override
  public void stop() throws Exception {
    shutdown.shutdown();
    shutdown.awaitShutdown(awaitShutdown);
    server.stop();
  }

  @Override
  public Optional<Executor> executor() {
    return Optional.ofNullable(server.getWorker());
  }

}
