/*
 * Copyright 2015-2020 the original author or authors.
 *
 * 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.
 */
package io.rsocket.core;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.netty.util.ReferenceCountUtil;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.frame.FrameType;
import io.rsocket.frame.decoder.PayloadDecoder;
import io.rsocket.plugins.RequestInterceptor;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;

final class FireAndForgetResponderSubscriber
    implements CoreSubscriber<Void>, ResponderFrameHandler {

  static final Logger logger = LoggerFactory.getLogger(FireAndForgetResponderSubscriber.class);

  static final FireAndForgetResponderSubscriber INSTANCE = new FireAndForgetResponderSubscriber();

  final int streamId;
  final ByteBufAllocator allocator;
  final PayloadDecoder payloadDecoder;
  final RequesterResponderSupport requesterResponderSupport;
  final RSocket handler;
  final int maxInboundPayloadSize;

  @Nullable final RequestInterceptor requestInterceptor;

  CompositeByteBuf frames;

  private FireAndForgetResponderSubscriber() {
    this.streamId = 0;
    this.allocator = null;
    this.payloadDecoder = null;
    this.maxInboundPayloadSize = 0;
    this.requesterResponderSupport = null;
    this.handler = null;
    this.requestInterceptor = null;
    this.frames = null;
  }

  FireAndForgetResponderSubscriber(
      int streamId, RequesterResponderSupport requesterResponderSupport) {
    this.streamId = streamId;
    this.allocator = null;
    this.payloadDecoder = null;
    this.maxInboundPayloadSize = 0;
    this.requesterResponderSupport = null;
    this.handler = null;
    this.requestInterceptor = requesterResponderSupport.getRequestInterceptor();
    this.frames = null;
  }

  FireAndForgetResponderSubscriber(
      int streamId,
      ByteBuf firstFrame,
      RequesterResponderSupport requesterResponderSupport,
      RSocket handler) {
    this.streamId = streamId;
    this.allocator = requesterResponderSupport.getAllocator();
    this.payloadDecoder = requesterResponderSupport.getPayloadDecoder();
    this.maxInboundPayloadSize = requesterResponderSupport.getMaxInboundPayloadSize();
    this.requesterResponderSupport = requesterResponderSupport;
    this.handler = handler;
    this.requestInterceptor = requesterResponderSupport.getRequestInterceptor();

    this.frames =
        ReassemblyUtils.addFollowingFrame(
            allocator.compositeBuffer(), firstFrame, true, maxInboundPayloadSize);
  }

  @Override
  public void onSubscribe(Subscription s) {
    s.request(Long.MAX_VALUE);
  }

  @Override
  public void onNext(Void voidVal) {}

  @Override
  public void onError(Throwable t) {
    final RequestInterceptor requestInterceptor = this.requestInterceptor;
    if (requestInterceptor != null) {
      requestInterceptor.onTerminate(this.streamId, FrameType.REQUEST_FNF, t);
    }

    logger.debug("Dropped Outbound error", t);
  }

  @Override
  public void onComplete() {
    final RequestInterceptor requestInterceptor = this.requestInterceptor;
    if (requestInterceptor != null) {
      requestInterceptor.onTerminate(this.streamId, FrameType.REQUEST_FNF, null);
    }
  }

  @Override
  public void handleNext(ByteBuf followingFrame, boolean hasFollows, boolean isLastPayload) {
    final CompositeByteBuf frames = this.frames;

    try {
      ReassemblyUtils.addFollowingFrame(
          frames, followingFrame, hasFollows, this.maxInboundPayloadSize);
    } catch (IllegalStateException t) {
      final int streamId = this.streamId;
      this.requesterResponderSupport.remove(streamId, this);

      this.frames = null;
      frames.release();

      final RequestInterceptor requestInterceptor = this.requestInterceptor;
      if (requestInterceptor != null) {
        requestInterceptor.onTerminate(streamId, FrameType.REQUEST_FNF, t);
      }

      logger.debug("Reassembly has failed", t);
      return;
    }

    if (!hasFollows) {
      this.requesterResponderSupport.remove(this.streamId, this);
      this.frames = null;

      Payload payload;
      try {
        payload = this.payloadDecoder.apply(frames);
        frames.release();
      } catch (Throwable t) {
        ReferenceCountUtil.safeRelease(frames);

        final RequestInterceptor requestInterceptor = this.requestInterceptor;
        if (requestInterceptor != null) {
          requestInterceptor.onTerminate(this.streamId, FrameType.REQUEST_FNF, t);
        }

        logger.debug("Reassembly has failed", t);
        return;
      }

      Mono<Void> source = this.handler.fireAndForget(payload);
      source.subscribe(this);
    }
  }

  @Override
  public final void handleCancel() {
    final CompositeByteBuf frames = this.frames;
    if (frames != null) {
      final int streamId = this.streamId;
      this.requesterResponderSupport.remove(streamId, this);

      this.frames = null;
      frames.release();

      final RequestInterceptor requestInterceptor = this.requestInterceptor;
      if (requestInterceptor != null) {
        requestInterceptor.onCancel(streamId, FrameType.REQUEST_FNF);
      }
    }
  }
}
