/*
 * 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
 *
 *   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.streams.facebook.provider;

import org.apache.streams.config.ComponentConfigurator;
import org.apache.streams.config.StreamsConfigurator;
import org.apache.streams.core.StreamsDatum;
import org.apache.streams.core.StreamsProvider;
import org.apache.streams.core.StreamsResultSet;
import org.apache.streams.facebook.FacebookConfiguration;
import org.apache.streams.facebook.IdConfig;
import org.apache.streams.jackson.StreamsJacksonMapper;
import org.apache.streams.util.ComponentUtils;
import org.apache.streams.util.SerializationUtil;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.typesafe.config.ConfigRenderOptions;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Abstract {@link org.apache.streams.core.StreamsProvider} for facebook.
 */
public abstract class FacebookProvider implements StreamsProvider {

  private static final String STREAMS_ID = "FacebookProvider";

  private static final Logger LOGGER = LoggerFactory.getLogger(FacebookProvider.class);
  private static final ObjectMapper MAPPER = StreamsJacksonMapper.getInstance();
  private static final int MAX_BATCH_SIZE = 2000;

  protected FacebookConfiguration configuration;
  protected BlockingQueue<StreamsDatum> datums;

  private AtomicBoolean isComplete;
  private ListeningExecutorService executor;
  List<ListenableFuture<Object>> futures = new ArrayList<>();

  private FacebookDataCollector dataCollector;

  /**
   * FacebookProvider constructor - resolves FacebookConfiguration from JVM 'facebook'.
   */
  public FacebookProvider() {
    this.configuration = new ComponentConfigurator<>(FacebookConfiguration.class).detectConfiguration();
  }

  /**
   * FacebookProvider constructor - uses supplied FacebookConfiguration.
   */
  public FacebookProvider(FacebookConfiguration configuration) {
    this.configuration = SerializationUtil.cloneBySerialization(configuration);
  }

  @Override
  public String getId() {
    return STREAMS_ID;
  }

  @Override
  public void startStream() {
    ListenableFuture future = executor.submit(getDataCollector());
    futures.add(future);
    executor.shutdown();
  }

  protected abstract FacebookDataCollector getDataCollector();

  @Override
  public StreamsResultSet readCurrent() {
    int batchSize = 0;
    BlockingQueue<StreamsDatum> batch = new LinkedBlockingQueue<>();
    while (!this.datums.isEmpty() && batchSize < MAX_BATCH_SIZE) {
      ComponentUtils.offerUntilSuccess(ComponentUtils.pollWhileNotEmpty(this.datums), batch);
      ++batchSize;
    }
    return new StreamsResultSet(batch);
  }

  @Override
  public StreamsResultSet readNew(BigInteger sequence) {
    return null;
  }

  @Override
  public StreamsResultSet readRange(DateTime start, DateTime end) {
    return null;
  }

  @Override
  public void prepare(Object configurationObject) {
    this.datums = new LinkedBlockingQueue<>();
    this.isComplete = new AtomicBoolean(false);
    this.executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1));
  }

  @Override
  public void cleanUp() {
    ComponentUtils.shutdownExecutor(executor, 5, 5);
    executor = null;
  }

  /**
   * Overrides the ids and addedAfter time in the configuration.
   * @param idsToAfterDate idsToAfterDate
   */
  public void overrideIds(Map<String, DateTime> idsToAfterDate) {
    Set<IdConfig> ids = new HashSet<>();
    for (String id : idsToAfterDate.keySet()) {
      IdConfig idConfig = new IdConfig();
      idConfig.setId(id);
      idConfig.setAfterDate(idsToAfterDate.get(id));
      ids.add(idConfig);
    }
    this.configuration.setIds(ids);
  }

  @Override
  public boolean isRunning() {
    if (datums.isEmpty() && executor.isTerminated() && Futures.allAsList(futures).isDone()) {
      LOGGER.info("Completed");
      isComplete.set(true);
      LOGGER.info("Exiting");
    }
    return !isComplete.get();
  }
}