/*
 * Copyright 2014 Cask Data, Inc.
 *
 * 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.cdap.cdap.api.dataset.lib.cube;

import io.cdap.cdap.api.annotation.Beta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * Defines a query to perform on {@link Cube} data.
 * </p>
 * Another way to think about the query is to map it to the following statement::
 * <pre><code>
 * SELECT count('read.ops')                                         {@literal <<} measure name and aggregation function
 * FROM aggregation1.1min_resolution                                {@literal <<} aggregation and resolution
 * GROUP BY dataset,                                                {@literal <<} groupByDimensions
 * WHERE namespace='ns1' AND app='myApp' AND program='myFlow' AND   {@literal <<} dimensionValues
 *       ts>=1423370200 AND ts{@literal <}1423398198                           {@literal <<} startTs and endTs
 * LIMIT 100                                                        {@literal <<} limit
 *
 * </code>
 * </pre>
 * See also {@link Cube#query(CubeQuery)}.
 */
@Beta
public final class CubeQuery {

  // null value means auto-choose aggregation based on query todo: auto-choosing may be error prone, remove it?
  @Nullable
  private final String aggregation;
  private final long startTs;
  private final long endTs;
  private final int resolution;
  private final int limit;
  private final Map<String, AggregationFunction> measurements;
  private final Map<String, String> dimensionValues;
  private final List<String> groupByDimensions;
  private final AggregationOption aggregationOption;
  private final Interpolator interpolator;

  /**
   * Creates {@link CubeQuery} with given parameters.
   *
   * @param aggregation (optional) aggregation name to query in; if {@code null}, the
   *     aggregation will be auto-selected based on rest of query parameters
   * @param startTs start (inclusive) of the time range to query
   * @param endTs end (exclusive) of the time range to query
   * @param resolution resolution of the aggregation to query in
   * @param limit max number of returned data points
   * @param measurements map of measure name, measure type to query for, empty map means "all
   *     measures"
   * @param dimensionValues dimension values to filter by
   * @param groupByDimensions dimensions to group by
   * @param aggregationOption the aggregation option for the
   * @param interpolator {@link Interpolator} to use
   */
  public CubeQuery(@Nullable String aggregation,
      long startTs, long endTs, int resolution, int limit,
      Map<String, AggregationFunction> measurements,
      Map<String, String> dimensionValues, List<String> groupByDimensions,
      AggregationOption aggregationOption,
      @Nullable Interpolator interpolator) {
    this.aggregation = aggregation;
    this.startTs = startTs;
    this.endTs = endTs;
    this.resolution = resolution;
    this.limit = limit;
    this.measurements = measurements;
    this.dimensionValues = Collections.unmodifiableMap(new HashMap<>(dimensionValues));
    this.groupByDimensions = Collections.unmodifiableList(new ArrayList<>(groupByDimensions));
    this.aggregationOption = aggregationOption;
    this.interpolator = interpolator;
  }

  @Nullable
  public String getAggregation() {
    return aggregation;
  }

  public long getStartTs() {
    return startTs;
  }

  public long getEndTs() {
    return endTs;
  }

  public int getResolution() {
    return resolution;
  }

  public Map<String, AggregationFunction> getMeasurements() {
    return measurements;
  }

  public Map<String, String> getDimensionValues() {
    return dimensionValues;
  }

  public List<String> getGroupByDimensions() {
    return groupByDimensions;
  }

  // todo: push down limit support to Cube
  public int getLimit() {
    return limit;
  }

  public AggregationOption getAggregationOption() {
    return aggregationOption;
  }

  public Interpolator getInterpolator() {
    return interpolator;
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder();
    sb.append("CubeQuery");
    sb.append("{aggregation=").append(aggregation);
    sb.append(", startTs=").append(startTs);
    sb.append(", endTs=").append(endTs);
    sb.append(", resolution=").append(resolution);
    sb.append(", limit=").append(limit);
    sb.append(", measurements=").append(measurements);
    sb.append(", dimensionValues=").append(dimensionValues);
    sb.append(", groupByDimensions=").append(groupByDimensions);
    sb.append(", interpolator=").append(interpolator);
    sb.append('}');
    return sb.toString();
  }

  /**
   * @return {@link Builder} to build {@link CubeQuery}.
   */
  public static Builder builder() {
    return new Builder();
  }

  /**
   * Builds {@link CubeQuery}.
   */
  public static final class Builder {

    private String aggregation;
    private long startTs;
    private long endTs;
    private int resolution;
    private int limit;
    private Map<String, AggregationFunction> measurements = new HashMap<>();
    private Map<String, String> dimensionValues = new HashMap<>();
    private List<String> groupByDimensions = new ArrayList<>();
    private Interpolator interpolator;

    /**
     * @return builder for configuring {@link CubeQuery}
     */
    public Select select() {
      return new Select();
    }

    /**
     * @return instance of {@link CubeQuery}
     */
    private CubeQuery build() {
      return new CubeQuery(aggregation, startTs, endTs, resolution, limit,
          measurements, dimensionValues, groupByDimensions, null, interpolator);
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class Select {

      private Select() {
      }

      /**
       * Adds measurement to be included in selection of {@link CubeQuery}.
       *
       * @param name name of the measurement
       * @param aggFunc function to be used if aggregation of measurement value is needed
       * @return builder for configuring {@link CubeQuery}
       */
      public Measurement measurement(String name, AggregationFunction aggFunc) {
        Builder.this.measurements.put(name, aggFunc);
        return new Measurement();
      }

      /**
       * Adds measurements to be included in selection of {@link CubeQuery}.
       *
       * @param measurements map of measurement name, agg function to include
       * @return builder for configuring {@link CubeQuery}
       */
      public Measurement measurements(Map<String, AggregationFunction> measurements) {
        Builder.this.measurements.putAll(measurements);
        return new Measurement();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class Measurement {

      private Measurement() {
      }

      /**
       * Adds measurement to be included in selection of {@link CubeQuery}.
       *
       * @param name name of the measurement
       * @param aggFunc function to be used if aggregation of measurement value is needed
       * @return builder for configuring {@link CubeQuery}
       */
      public Measurement measurement(String name, AggregationFunction aggFunc) {
        Builder.this.measurements.put(name, aggFunc);
        return this;
      }

      /**
       * Adds measurements to be included in selection of {@link CubeQuery}.
       *
       * @param measurements map of measurement name, agg function to include
       * @return builder for configuring {@link CubeQuery}
       */
      public Measurement measurements(Map<String, AggregationFunction> measurements) {
        Builder.this.measurements.putAll(measurements);
        return new Measurement();
      }

      /**
       * Defines aggregation view to query from.
       *
       * @param aggregation name of the aggregation view
       * @return builder for configuring {@link CubeQuery}
       */
      public From from(String aggregation) {
        Builder.this.aggregation = aggregation;
        return new From();
      }

      /**
       * Sets aggregation view to query from to be auto-selected based on other parameters of the
       * query.
       *
       * @return builder for configuring {@link CubeQuery}
       */
      public From from() {
        Builder.this.aggregation = null;
        return new From();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class From {

      private From() {
      }

      /**
       * Sets resolution for {@link CubeQuery}.
       *
       * @param amount amount of units
       * @param timeUnit unit type
       * @return builder for configuring {@link CubeQuery}
       */
      public Where resolution(long amount, TimeUnit timeUnit) {
        Builder.this.resolution = (int) timeUnit.convert(amount, TimeUnit.SECONDS);
        return new Where();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class Where {

      private Where() {
      }

      /**
       * @return builder for configuring {@link CubeQuery}
       */
      public Dimension where() {
        return new Dimension();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class Dimension {

      private Dimension() {
      }

      /**
       * Adds dimension value to filter by.
       *
       * @param name name of dimension
       * @param value value of dimension
       * @return builder for configuring {@link CubeQuery}
       */
      public Dimension dimension(String name, String value) {
        Builder.this.dimensionValues.put(name, value);
        return this;
      }

      /**
       * Adds dimension values to filter by.
       *
       * @param dimValues dimension name, dimension value pairs to filter by
       * @return builder for configuring {@link CubeQuery}
       */
      public Dimension dimensions(Map<String, String> dimValues) {
        Builder.this.dimensionValues.putAll(dimValues);
        return this;
      }

      /**
       * Defines time range for {@link CubeQuery}.
       *
       * @param startTsInSec start time inclusive (epoch in seconds)
       * @param endTsInSec end time exclusive (epoch in seconds)
       * @return builder for configuring {@link CubeQuery}
       */
      public GroupBy timeRange(long startTsInSec, long endTsInSec) {
        Builder.this.startTs = startTsInSec;
        Builder.this.endTs = endTsInSec;
        return new GroupBy();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class GroupBy {

      private GroupBy() {
      }

      /**
       * @return builder for configuring {@link CubeQuery}
       */
      public GroupByDimension groupBy() {
        return new GroupByDimension();
      }

      /**
       * Sets a limit on returned data points per time series
       *
       * @param limit limit value
       * @return builder for configuring {@link CubeQuery}
       */
      public Limit limit(int limit) {
        Builder.this.limit = limit;
        return new Limit();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class GroupByDimension {

      private GroupByDimension() {
      }

      /**
       * Adds dimension to use for grouping results into time series.
       *
       * @param name name of the dimension
       * @return builder for configuring {@link CubeQuery}
       */
      public GroupByDimension dimension(String name) {
        Builder.this.groupByDimensions.add(name);
        return this;
      }

      /**
       * Adds dimensions to use for grouping results into time series.
       *
       * @param names names of the dimensions
       * @return builder for configuring {@link CubeQuery}
       */
      public GroupByDimension dimensions(List<String> names) {
        Builder.this.groupByDimensions.addAll(names);
        return this;
      }

      /**
       * Sets a limit on returned data points per time series
       *
       * @param limit limit value
       * @return builder for configuring {@link CubeQuery}
       */
      public Limit limit(int limit) {
        Builder.this.limit = limit;
        return new Limit();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class Limit {

      private Limit() {
      }

      /**
       * Sets {@link Interpolator} to use for {@link CubeQuery}.
       *
       * @param interpolator interpolator to use
       * @return builder for configuring {@link CubeQuery}
       */
      public Build interpolator(Interpolator interpolator) {
        Builder.this.interpolator = interpolator;
        return new Build();
      }

      /**
       * @return {@link CubeQuery}
       */
      public CubeQuery build() {
        return Builder.this.build();
      }
    }

    /**
     * Builder for configuring {@link CubeQuery}.
     */
    public final class Build {

      private Build() {
      }

      /**
       * @return {@link CubeQuery}
       */
      public CubeQuery build() {
        return Builder.this.build();
      }
    }
  }

}
