/**
 * Licensed 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.
 *
 * Copyright 2014 Edgar Espina
 */
package io.jooby.internal.utow;

import io.jooby.Body;
import io.jooby.Err;
import io.jooby.Router;
import io.jooby.StatusCode;
import io.undertow.io.Receiver;
import io.undertow.server.ExchangeCompletionListener;
import io.undertow.server.HttpServerExchange;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.WRITE;

public class UtowBodyHandler
    implements Receiver.FullBytesCallback, Receiver.PartialBytesCallback,
    ExchangeCompletionListener {

  private final int bufferSize;
  private final long maxRequestSize;
  private Router.Match route;
  private UtowContext context;
  private long chunkSize;
  private List chunks;
  private Path file;
  private FileChannel channel;

  public UtowBodyHandler(Router.Match route, UtowContext context, int bufferSize,
      long maxRequestSize) {
    this.route = route;
    this.context = context;
    this.bufferSize = bufferSize;
    this.maxRequestSize = maxRequestSize;
  }

  @Override public void handle(HttpServerExchange exchange, byte[] bytes) {
    context.body = Body.of(bytes);
    route.execute(context);
  }

  @Override public void exchangeEvent(HttpServerExchange exchange, NextListener next) {
    try {
      Files.deleteIfExists(file);
    } catch (IOException x) {
      // ignore
    } finally {
      next.proceed();
    }
  }

  @Override public void handle(HttpServerExchange exchange, byte[] chunk, boolean last) {
    try {
      if (chunk.length > 0) {
        chunkSize += chunk.length;
        if (chunkSize > maxRequestSize) {
          try {
            context.sendError(new Err(StatusCode.REQUEST_ENTITY_TOO_LARGE));
          } finally {
            closeChannel();
            channel = null;
          }
          return;
        }
        if (chunkSize <= bufferSize) {
          if (chunks == null) {
            chunks = new ArrayList<>();
          }
          chunks.add(chunk);
        } else {
          // overflow
          if (file == null) {
            file = context.getRouter().getTmpdir().resolve("undertow" + System.nanoTime() + "body");
            channel = FileChannel.open(file, CREATE, WRITE);
          }
          if (chunks != null) {
            List source = chunks;
            chunks = null;
            for (Object s : source) {
              channel.write(ByteBuffer.wrap((byte[]) s));
            }
            source.clear();
          }
          channel.write(ByteBuffer.wrap(chunk));
        }
      }
      if (last) {
        if (channel != null) {
          exchange.addExchangeCompleteListener(this);
          forceAndClose();
          channel = null;
          context.body = Body.of(file);
        } else {
          context.body = Body.of(bytes((int) chunkSize));
        }
        route.execute(context);
      }
    } catch (IOException x) {
      try {
        context.sendError(x);
      } finally {
        closeChannel();
        channel = null;
        exchange.endExchange();
      }
    }
  }

  private void closeChannel() {
    if (channel != null) {
      try {
        channel.close();
      } catch (IOException x) {
        // close didn't succeed
      }
    }
  }

  private void forceAndClose() throws IOException {
    if (channel != null) {
      try {
        channel.force(false);
      } finally {
        closeChannel();
      }
    }
  }

  private byte[] bytes(int size) {
    byte[] bytes = new byte[size];
    int offset = 0;
    for (Object c : chunks) {
      byte[] chunk = (byte[]) c;
      System.arraycopy(chunk, 0, bytes, offset, chunk.length);
      offset += chunk.length;
    }
    return bytes;
  }
}

@@ -903,24 +903,74 @@
 ByteBuffer;%0A
+import java.nio.channels.AsynchronousFileChannel;%0A
 import java.
@@ -1559,16 +1559,28 @@
 private 
+Asynchronous
 FileChan
@@ -1591,16 +1591,41 @@
 channel;
+%0A  private long position;
 %0A%0A  publ
@@ -2995,16 +2995,28 @@
 annel = 
+Asynchronous
 FileChan
@@ -3185,24 +3185,65 @@
 : source) %7B%0A
+              byte%5B%5D bytes = (byte%5B%5D) s;%0A
             
@@ -3278,22 +3278,67 @@
 rap(
-(
 byte
-%5B%5D) s)
+s), position
 );%0A
+              position += bytes.length;%0A
     
@@ -3433,17 +3433,63 @@
 p(chunk)
-)
+, position);%0A          position += chunk.length
 ;%0A      

use an async file channel for large utow bodies