// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC 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.openqa.selenium.netty.server;

import static io.netty.handler.codec.http.HttpMethod.DELETE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.HEAD;
import static io.netty.handler.codec.http.HttpMethod.OPTIONS;
import static io.netty.handler.codec.http.HttpMethod.POST;

import com.google.common.io.ByteSource;
import com.google.common.io.FileBackedOutputStream;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.util.ReferenceCountUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import org.openqa.selenium.internal.Debug;
import org.openqa.selenium.remote.http.Contents;
import org.openqa.selenium.remote.http.HttpMethod;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.tracing.AttributeKey;

class RequestConverter extends SimpleChannelInboundHandler<HttpObject> {

  private static final Logger LOG = Logger.getLogger(RequestConverter.class.getName());
  private static final List<io.netty.handler.codec.http.HttpMethod> SUPPORTED_METHODS =
      Arrays.asList(DELETE, GET, POST, OPTIONS);
  private volatile FileBackedOutputStream buffer;
  private volatile int length;
  private volatile HttpRequest request;

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
    LOG.log(Debug.getDebugLogLevel(), "Incoming message: {0}", msg);

    if (msg instanceof io.netty.handler.codec.http.HttpRequest) {
      LOG.log(Debug.getDebugLogLevel(), "Start of http request: {0}", msg);

      io.netty.handler.codec.http.HttpRequest nettyRequest =
          (io.netty.handler.codec.http.HttpRequest) msg;

      if (nettyRequest.headers().contains("Sec-WebSocket-Version")
          && "upgrade".equalsIgnoreCase(nettyRequest.headers().get("Connection"))) {
        // Pass this on to later in the pipeline.
        ReferenceCountUtil.retain(msg);
        ctx.fireChannelRead(msg);
        return;
      }

      request = createRequest(ctx, nettyRequest);
      if (request == null) {
        return;
      }

      if (HttpUtil.is100ContinueExpected(nettyRequest)) {
        ctx.write(new HttpResponse().setStatus(100));
        return;
      }

      request.setAttribute(
          AttributeKey.HTTP_SCHEME.getKey(), nettyRequest.protocolVersion().protocolName());
      request.setAttribute(
          AttributeKey.HTTP_FLAVOR.getKey(), nettyRequest.protocolVersion().majorVersion());

      buffer = null;
      length = -1;
    }

    if (request != null && msg instanceof HttpContent) {
      ByteBuf buf = ((HttpContent) msg).content().retain();
      int nBytes = buf.readableBytes();

      if (nBytes > 0) {
        if (buffer == null) {
          buffer = new FileBackedOutputStream(3 * 1024 * 1024, true);
          length = 0;
        }

        try {
          buf.readBytes(buffer, nBytes);
          length += nBytes;
        } finally {
          buf.release();
        }
      }

      if (msg instanceof LastHttpContent) {
        LOG.log(Debug.getDebugLogLevel(), "End of http request: {0}", msg);

        if (buffer != null) {
          ByteSource source = buffer.asByteSource();
          int len = length;

          request.setContent(
              new Contents.Supplier() {
                @Override
                public InputStream get() {
                  try {
                    return source.openBufferedStream();
                  } catch (IOException e) {
                    throw new UncheckedIOException(e);
                  }
                }

                @Override
                public int length() {
                  return len;
                }

                @Override
                public void close() throws IOException {
                  buffer.reset();
                }
              });
        } else {
          request.setContent(Contents.empty());
        }

        ctx.fireChannelRead(request);
      }
    }
  }

  @Override
  public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    LOG.log(Debug.getDebugLogLevel(), "Channel became inactive.");
    super.channelInactive(ctx);
  }

  private HttpRequest createRequest(
      ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpRequest nettyRequest) {

    // Attempt to map the netty method
    HttpMethod method;
    if (nettyRequest.method().equals(HEAD)) {
      method = HttpMethod.GET;
    } else if (SUPPORTED_METHODS.contains(nettyRequest.method())) {
      try {
        method = HttpMethod.valueOf(nettyRequest.method().name());
      } catch (IllegalArgumentException e) {
        ctx.writeAndFlush(
            new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED));
        return null;
      }
    } else {
      ctx.writeAndFlush(
          new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.METHOD_NOT_ALLOWED));
      return null;
    }

    // Attempt to decode parameters
    try {
      QueryStringDecoder decoder = new QueryStringDecoder(nettyRequest.uri());

      HttpRequest req = new HttpRequest(method, decoder.path());

      decoder
          .parameters()
          .forEach((key, values) -> values.forEach(value -> req.addQueryParameter(key, value)));

      nettyRequest.headers().entries().stream()
          .filter(entry -> entry.getKey() != null)
          .forEach(entry -> req.addHeader(entry.getKey(), entry.getValue()));
      return req;
    } catch (Exception ignore) {
      ctx.writeAndFlush(
          new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
      LOG.log(
          Debug.getDebugLogLevel(), "Not possible to decode parameters. {0}", nettyRequest.uri());
      return null;
    }
  }
}
