package edu.washington.escience.myria.parallel;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;

import edu.washington.escience.myria.DbException;
import edu.washington.escience.myria.MyriaConstants;
import edu.washington.escience.myria.api.MyriaJsonMapperProvider;
import edu.washington.escience.myria.api.encoding.PlanFragmentEncoding;
import edu.washington.escience.myria.api.encoding.QueryConstruct;
import edu.washington.escience.myria.api.encoding.QueryConstruct.ConstructArgs;
import edu.washington.escience.myria.coordinator.CatalogException;
import edu.washington.escience.myria.operator.EOSSource;
import edu.washington.escience.myria.operator.EmptySink;

/**
 * A {@link QueryPlan} that runs a single subquery. Note that a {@link JsonSubQuery} cannot have a {@link QueryPlan} as
 * a child, but rather can only accept a physical JSON subquery as a set of fragments.
 */
public final class JsonSubQuery extends QueryPlan {
  /** The logger for this class. */
  private static final Logger LOGGER = LoggerFactory.getLogger(JsonSubQuery.class);
  /** The json query to be executed. */
  private final List<PlanFragmentEncoding> fragments;

  /**
   * @return the fragments of the query.
   */
  protected List<PlanFragmentEncoding> getFragments() {
    return ImmutableList.copyOf(fragments);
  }

  /**
   * Construct a {@link QueryPlan} that runs the given subquery. The subquery will be instantiated using
   * {@link QueryConstruct#instantiate(List, edu.washington.escience.myria.parallel.Server)}.
   *
   * @param fragments the JSON query to be executed, broken into fragments
   * @see QueryConstruct#instantiate(List, edu.washington.escience.myria.parallel.Server)
   */
  public JsonSubQuery(final List<PlanFragmentEncoding> fragments) {
    this.fragments = Objects.requireNonNull(fragments, "fragments");
  }

  @Override
  public void instantiate(
      final LinkedList<QueryPlan> planQ,
      final LinkedList<SubQuery> subQueryQ,
      final ConstructArgs args)
      throws DbException {
    QueryPlan task = planQ.peekFirst();
    Verify.verify(
        task == this,
        "this Fragment %s should be the first object on the queue, not %s!",
        this,
        task);
    planQ.removeFirst();

    Map<Integer, SubQueryPlan> allPlans;
    try {
      allPlans = QueryConstruct.instantiate(fragments, args);
    } catch (CatalogException e) {
      throw new DbException("Error instantiating JsonSubQuery", e);
    }
    SubQueryPlan serverPlan = allPlans.get(MyriaConstants.MASTER_ID);
    Map<Integer, SubQueryPlan> workerPlans;
    if (serverPlan != null) {
      workerPlans = new HashMap<>();
      for (Map.Entry<Integer, SubQueryPlan> entry : allPlans.entrySet()) {
        if (entry.getKey() != MyriaConstants.MASTER_ID) {
          workerPlans.put(entry.getKey(), entry.getValue());
        }
      }
    } else {
      workerPlans = allPlans;
      /* Create the empty server plan. TODO why do we need this? */
      serverPlan = new SubQueryPlan(new EmptySink(new EOSSource()));
    }

    String planEncoding = null;
    try {
      planEncoding = MyriaJsonMapperProvider.getMapper().writeValueAsString(fragments);
    } catch (JsonProcessingException e) {
      LOGGER.error("Unable to encode JsonSubQuery fragments", e);
    }
    subQueryQ.addFirst(new SubQuery(null, serverPlan, workerPlans, planEncoding));
  }

  @Override
  public void reset() {
    /* Do nothing. */
  }
}
