/*
 * 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.apache.flume.api;

import org.apache.flume.Event;
import org.apache.flume.FlumeException;
import org.apache.flume.event.EventBuilder;
import org.apache.flume.thrift.Status;
import org.apache.flume.thrift.ThriftFlumeEvent;
import org.apache.flume.thrift.ThriftSourceProtocol;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.server.THsHaServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TSSLTransportFactory;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TNonblockingServerTransport;
import org.apache.thrift.transport.TFastFramedTransport;
import org.apache.thrift.transport.TServerTransport;

import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class ThriftTestingSource {
  public final Queue<Event> flumeEvents = new ConcurrentLinkedQueue<Event>();
  private final TServer server;
  public int batchCount = 0;
  public int individualCount = 0;
  public int incompleteBatches = 0;
  private AtomicLong delay = null;

  public void setDelay(AtomicLong delay) {
    this.delay = delay;
  }

  private class ThriftOKHandler implements ThriftSourceProtocol.Iface {

    public ThriftOKHandler() {

    }

    @Override
    public Status append(ThriftFlumeEvent event) throws TException {
      flumeEvents.add(EventBuilder.withBody(event.getBody(), event.getHeaders()));
      individualCount++;
      return Status.OK;
    }

    @Override
    public Status appendBatch(List<ThriftFlumeEvent> events) throws TException {
      batchCount++;
      if (events.size() < 10) {
        incompleteBatches++;
      }
      for (ThriftFlumeEvent event : events) {
        flumeEvents.add(EventBuilder.withBody(event.getBody(), event.getHeaders()));
      }
      return Status.OK;
    }
  }

  private class ThriftFailHandler implements ThriftSourceProtocol.Iface {

    @Override
    public Status append(ThriftFlumeEvent event) throws TException {
      return Status.FAILED;
    }

    @Override
    public Status appendBatch(List<ThriftFlumeEvent> events) throws
                                                             TException {
      return Status.FAILED;
    }
  }

  private class ThriftErrorHandler implements ThriftSourceProtocol.Iface {

    @Override
    public Status append(ThriftFlumeEvent event) throws TException {
      throw new FlumeException("Forced Error");
    }

    @Override
    public Status appendBatch(List<ThriftFlumeEvent> events) throws TException {
      throw new FlumeException("Forced Error");
    }
  }

  private class ThriftSlowHandler extends ThriftOKHandler {

    @Override
    public Status append(ThriftFlumeEvent event) throws TException {
      try {
        TimeUnit.MILLISECONDS.sleep(1550);
      } catch (InterruptedException e) {
        throw new FlumeException("Error", e);
      }
      return super.append(event);
    }

    @Override
    public Status appendBatch(List<ThriftFlumeEvent> events) throws TException {
      try {
        TimeUnit.MILLISECONDS.sleep(1550);
      } catch (InterruptedException e) {
        throw new FlumeException("Error", e);
      }
      return super.appendBatch(events);
    }
  }

  private class ThriftTimeoutHandler extends ThriftOKHandler {

    @Override
    public Status append(ThriftFlumeEvent event) throws TException {
      try {
        TimeUnit.MILLISECONDS.sleep(5000);
      } catch (InterruptedException e) {
        throw new FlumeException("Error", e);
      }
      return super.append(event);
    }

    @Override
    public Status appendBatch(List<ThriftFlumeEvent> events) throws TException {
      try {
        TimeUnit.MILLISECONDS.sleep(5000);
      } catch (InterruptedException e) {
        throw new FlumeException("Error", e);
      }
      return super.appendBatch(events);
    }
  }

  private class ThriftAlternateHandler extends ThriftOKHandler {

    @Override
    public Status append(ThriftFlumeEvent event) throws TException {
      try {
        if (delay != null) {
          TimeUnit.MILLISECONDS.sleep(delay.get());
        }
      } catch (InterruptedException e) {
        throw new FlumeException("Error", e);
      }
      return super.append(event);
    }

    @Override
    public Status appendBatch(List<ThriftFlumeEvent> events) throws TException {
      try {
        if (delay != null) {
          TimeUnit.MILLISECONDS.sleep(delay.get());
        }
      } catch (InterruptedException e) {
        throw new FlumeException("Error", e);
      }
      return super.appendBatch(events);
    }
  }

  private ThriftSourceProtocol.Iface getHandler(String handlerName) {
    ThriftSourceProtocol.Iface handler = null;
    if (handlerName.equals(HandlerType.OK.name())) {
      handler = new ThriftOKHandler();
    } else if (handlerName.equals(HandlerType.FAIL.name())) {
      handler = new ThriftFailHandler();
    } else if (handlerName.equals(HandlerType.ERROR.name())) {
      handler = new ThriftErrorHandler();
    } else if (handlerName.equals(HandlerType.SLOW.name())) {
      handler = new ThriftSlowHandler();
    } else if (handlerName.equals(HandlerType.TIMEOUT.name())) {
      handler = new ThriftTimeoutHandler();
    } else if (handlerName.equals(HandlerType.ALTERNATE.name())) {
      handler = new ThriftAlternateHandler();
    }
    return handler;
  }

  public ThriftTestingSource(String handlerName, int port, String protocol) throws Exception {
    TNonblockingServerTransport serverTransport =
        new TNonblockingServerSocket(new InetSocketAddress("0.0.0.0", port));
    ThriftSourceProtocol.Iface handler = getHandler(handlerName);

    TProtocolFactory transportProtocolFactory = null;
    if (protocol != null && protocol == ThriftRpcClient.BINARY_PROTOCOL) {
      transportProtocolFactory = new TBinaryProtocol.Factory();
    } else {
      transportProtocolFactory = new TCompactProtocol.Factory();
    }
    server = new THsHaServer(new THsHaServer.Args(serverTransport).processor(
        new ThriftSourceProtocol.Processor(handler)).protocolFactory(
            transportProtocolFactory));
    Executors.newSingleThreadExecutor().submit(new Runnable() {
      @Override
      public void run() {
        server.serve();
      }
    });
  }

  public ThriftTestingSource(String handlerName, int port,
                             String protocol, String keystore,
                             String keystorePassword, String keyManagerType,
                             String keystoreType) throws Exception {
    TSSLTransportFactory.TSSLTransportParameters params =
            new TSSLTransportFactory.TSSLTransportParameters();
    params.setKeyStore(keystore, keystorePassword, keyManagerType, keystoreType);

    TServerSocket serverTransport = TSSLTransportFactory.getServerSocket(
            port, 10000, InetAddress.getByName("0.0.0.0"), params);

    ThriftSourceProtocol.Iface handler = getHandler(handlerName);

    Class serverClass = Class.forName("org.apache.thrift" +
            ".server.TThreadPoolServer");
    Class argsClass = Class.forName("org.apache.thrift.server" +
            ".TThreadPoolServer$Args");
    TServer.AbstractServerArgs args = (TServer.AbstractServerArgs) argsClass
            .getConstructor(TServerTransport.class)
            .newInstance(serverTransport);
    Method m = argsClass.getDeclaredMethod("maxWorkerThreads", int.class);
    m.invoke(args, Integer.MAX_VALUE);
    TProtocolFactory transportProtocolFactory = null;
    if (protocol != null && protocol == ThriftRpcClient.BINARY_PROTOCOL) {
      transportProtocolFactory = new TBinaryProtocol.Factory();
    } else {
      transportProtocolFactory = new TCompactProtocol.Factory();
    }
    args.protocolFactory(transportProtocolFactory);
    args.inputTransportFactory(new TFastFramedTransport.Factory());
    args.outputTransportFactory(new TFastFramedTransport.Factory());
    args.processor(new ThriftSourceProtocol.Processor<ThriftSourceProtocol.Iface>(handler));
    server = (TServer) serverClass.getConstructor(argsClass).newInstance(args);
    Executors.newSingleThreadExecutor().submit(new Runnable() {
      @Override
      public void run() {
        server.serve();
      }
    });
  }

  public enum HandlerType {
    OK,
    FAIL,
    ERROR,
    SLOW,
    TIMEOUT,
    ALTERNATE;
  }

  public void stop() {
    server.stop();
  }

}
