// Copyright 2018 Google Inc. All Rights Reserved.
//
// 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 com.google.api.ads.adwords.jaxws.utils.v201809;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.ads.adwords.jaxws.v201809.cm.AdGroupBidLandscape;
import com.google.api.ads.adwords.jaxws.v201809.cm.AdGroupBidLandscapePage;
import com.google.api.ads.adwords.jaxws.v201809.cm.CriterionBidLandscape;
import com.google.api.ads.adwords.jaxws.v201809.cm.CriterionBidLandscapePage;
import com.google.api.ads.adwords.jaxws.v201809.cm.Page;
import com.google.api.ads.adwords.jaxws.v201809.cm.SortOrder;
import com.google.api.ads.adwords.lib.selectorfields.EntityField;
import com.google.api.ads.adwords.lib.utils.AdWordsInternals;
import com.google.api.ads.adwords.lib.utils.ServiceQueryInterface;
import com.google.api.ads.common.lib.utils.AdsUtilityInvocationHandler;
import com.google.common.base.Objects;
import com.google.common.reflect.Reflection;
import java.lang.reflect.InvocationHandler;
import javax.annotation.Nullable;

/** A service query that embodies AWQL string for making SOAP requests. */
public final class ServiceQuery implements ServiceQueryInterface<Page> {

  private final String partialAwqlQuery;
  private final int pageSize;
  private int startIndex;

  /**
   * Constructs a service query object using the passed partial AWQL query, the start index and the
   * page size.
   *
   * <p>This constructor should be called by {@link Builder} only.
   *
   * @param partialAwqlQuery the partial AWQL query without the LIMIT clause, created by the service
   *     query builder
   * @param startIndex the start index for the LIMIT clause
   * @param pageSize the page size for the LIMIT clause
   */
  ServiceQuery(String partialAwqlQuery, int startIndex, int pageSize) {
    this.partialAwqlQuery = partialAwqlQuery;
    this.startIndex = startIndex;
    this.pageSize = pageSize;
  }

  @Override
  public ServiceQuery nextPage(@Nullable Page page) {
    return (page == null) ? this : nextPage(this.pageSize);
  }

  /**
   * Sets the LIMIT clause of the AWQL to the next page calculated by using the total number of
   * landscape points in {@link AdGroupBidLandscapePage}. If {@code page} is null, this will not
   * change the LIMIT clause.
   *
   * <p>This method is meant to be used with {@code hasNext()}. The {@code page} is necessary when
   * using DataService, as its paging mechanism is different from other services. For details, see
   * https://developers.google.com/adwords/api/docs/guides/bid-landscapes#paging_through_results.
   *
   * @param page the ad group bid landscape page whose total number of landscape points will be
   *     used to increment the index in the LIMIT clause
   * @return this service query object
   */
  public ServiceQuery nextPage(@Nullable AdGroupBidLandscapePage page) {
    if (page == null) {
      return this;
    }
    return nextPage(getTotalLandscapePointsInPage(page));
  }

  /**
   * Sets the LIMIT clause of the AWQL to the next page calculated by using the total number of
   * landscape points in {@link CriterionBidLandscapePage}. If {@code page} is null, this will not
   * change the LIMIT clause.
   *
   * <p>This method is meant to be used with {@code hasNext()}. The {@code page} is necessary when
   * using DataService, as its paging mechanism is different from other services. For details, see
   * https://developers.google.com/adwords/api/docs/guides/bid-landscapes#paging_through_results.
   *
   * @param page the criterion bid landscape page whose total number of landscape points will be
   *     used to increment the index in the LIMIT clause
   * @return this service query object
   */
  public ServiceQuery nextPage(@Nullable CriterionBidLandscapePage page) {
    if (page == null) {
      return this;
    }
    return nextPage(getTotalLandscapePointsInPage(page));
  }

  /**
   * Advances this object's {@link #startIndex} by the specified {@link #pageSize}.
   *
   * @return this service query object
   */
  private ServiceQuery nextPage(int pageSize) {
    startIndex = startIndex + pageSize;
    return this;
  }

  @Override
  public boolean hasNext(@Nullable Page page) {
    return (page == null) || startIndex + pageSize < page.getTotalNumEntries();
  }

  /**
   * Checks if there is still an ad group bid landscape page left to query.
   *
   * <p>This method is meant to be used with {@link ServiceQuery#nextPage(AdGroupBidLandscapePage)}.
   * The {@code page} is necessary when using DataService, as its paging mechanism is different from
   * other services. For details, see
   * https://developers.google.com/adwords/api/docs/guides/bid-landscapes#paging_through_results.
   *
   * @param page the ad group bid landscape page whose total number of landscape points will be used
   *     to determine if there is still a page left
   * @return true if there is still a page left
   */
  public boolean hasNext(@Nullable AdGroupBidLandscapePage page) {
    if (page == null) {
      return true;
    }
    return getTotalLandscapePointsInPage(page) >= pageSize;
  }

  /**
   * Checks if there is still an criterion bid landscape page left to query.
   *
   * <p>This method is meant to be used with {@link
   * ServiceQuery#nextPage(CriterionBidLandscapePage)}. The {@code page} is necessary when using
   * DataService, as its paging mechanism is different from other services. For details, see
   * https://developers.google.com/adwords/api/docs/guides/bid-landscapes#paging_through_results.
   *
   * @param page the criterion bid landscape page whose total number of landscape points will be
   *     used to determine if there is still a page left
   * @return true if there is still a page left
   */
  public boolean hasNext(@Nullable CriterionBidLandscapePage page) {
    if (page == null) {
      return true;
    }
    return getTotalLandscapePointsInPage(page) >= pageSize;
  }

  @Override
  public String toString() {
    return String.format("%s LIMIT %s,%s", partialAwqlQuery, startIndex, pageSize);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof ServiceQuery) {
      ServiceQuery other = (ServiceQuery) obj;
      return Objects.equal(this.partialAwqlQuery, other.partialAwqlQuery);
    }
    return false;
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(partialAwqlQuery);
  }

  /**
   * Returns the total number of inner landscape points in the ad group bid landscape page.
   */
  private int getTotalLandscapePointsInPage(AdGroupBidLandscapePage page) {
    int totalLandscapePointsInPage = 0;
    for (AdGroupBidLandscape adGroupBidLandscape : page.getEntries()) {
      totalLandscapePointsInPage += adGroupBidLandscape.getLandscapePoints().size();
    }
    return totalLandscapePointsInPage;
  }

  /**
   * Returns the total number of inner landscape points in the criterion bid landscape page.
   */
  private int getTotalLandscapePointsInPage(CriterionBidLandscapePage page) {
    int totalLandscapePointsInPage = 0;
    for (CriterionBidLandscape criterionBidLandscape : page.getEntries()) {
      totalLandscapePointsInPage += criterionBidLandscape.getLandscapePoints().size();
    }
    return totalLandscapePointsInPage;
  }

  /**
   * Builds a {@link ServiceQuery} object.
   *
   * <p>This builder is not thread-safe.
   */
  public static class Builder implements BuilderInterface<Page, SortOrder> {

    /** The proxy whose invocation handler will handle ads utility registration. */
    private final BuilderInterface<Page, SortOrder> proxy;
    /** The underlying proxied BuilderImpl. Required for the copy constructor. */
    private final ServiceQueryBuilderImpl proxiedImpl;

    /** Constructs a new service query builder. */
    @SuppressWarnings("unchecked")
    public Builder() {
      proxiedImpl = new ServiceQueryBuilderImpl(this);
      InvocationHandler invocationHandler =
          new AdsUtilityInvocationHandler(
              proxiedImpl, AdWordsInternals.getInstance().getAdsUtilityRegistry());
      this.proxy = Reflection.newProxy(BuilderInterface.class, invocationHandler);
    }

    /**
     * Constructs a new service query builder by copying all properties from the passed service
     * query builder.
     *
     * @param builderInterface the service query builder whose properties will be copied to.
     */
    @SuppressWarnings("unchecked")
    public Builder(BuilderInterface<Page, SortOrder> builderInterface) {
      checkNotNull(builderInterface, "The service query builder cannot be null.");
      Builder builder = (Builder) builderInterface;
      proxiedImpl = new ServiceQueryBuilderImpl(builder.proxiedImpl);
      InvocationHandler invocationHandler =
          new AdsUtilityInvocationHandler(
              proxiedImpl, AdWordsInternals.getInstance().getAdsUtilityRegistry());
      this.proxy = Reflection.newProxy(BuilderInterface.class, invocationHandler);
    }

    @Override
    @SuppressWarnings("unchecked")
    public ServiceWhereBuilderInterface<Page, SortOrder, Builder> where(EntityField field) {
      return (ServiceWhereBuilderInterface<Page, SortOrder, Builder>) proxy.where(field);
    }

    @Override
    @SuppressWarnings("unchecked")
    public ServiceWhereBuilderInterface<Page, SortOrder, Builder> where(String field) {
      return (ServiceWhereBuilderInterface<Page, SortOrder, Builder>) proxy.where(field);
    }

    @Override
    public Builder fields(EntityField... fields) {
      proxy.fields(fields);
      return this;
    }

    @Override
    public Builder fields(String... fields) {
      proxy.fields(fields);
      return this;
    }

    @Override
    public Builder fields(Iterable<String> fields) {
      proxy.fields(fields);
      return this;
    }

    @Override
    public ServiceQuery build() {
      return (ServiceQuery) proxy.build();
    }

    @Override
    public Builder orderBy(EntityField field) {
      proxy.orderBy(field);
      return this;
    }

    @Override
    public Builder orderBy(EntityField field, SortOrder order) {
      proxy.orderBy(field, order);
      return this;
    }

    @Override
    public Builder orderBy(String field) {
      proxy.orderBy(field);
      return this;
    }

    @Override
    public Builder orderBy(String field, SortOrder order) {
      proxy.orderBy(field, order);
      return this;
    }

    @Override
    public Builder limit(int startIndex, int pageSize) {
      proxy.limit(startIndex, pageSize);
      return this;
    }
  }
}