// Copyright 2016 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.keywordoptimizer;

import com.google.api.ads.adwords.axis.v201809.cm.Criterion;
import com.google.api.ads.adwords.axis.v201809.cm.Keyword;
import com.google.api.ads.adwords.axis.v201809.cm.KeywordMatchType;
import com.google.api.ads.adwords.axis.v201809.cm.Language;
import com.google.api.ads.adwords.axis.v201809.cm.Location;
import com.google.api.ads.adwords.axis.v201809.cm.Money;
import com.google.api.ads.adwords.axis.v201809.o.Attribute;
import com.google.api.ads.adwords.axis.v201809.o.AttributeType;
import com.google.api.ads.adwords.axis.v201809.o.DoubleAttribute;
import com.google.api.ads.adwords.axis.v201809.o.LanguageSearchParameter;
import com.google.api.ads.adwords.axis.v201809.o.LocationSearchParameter;
import com.google.api.ads.adwords.axis.v201809.o.LongAttribute;
import com.google.api.ads.adwords.axis.v201809.o.MoneyAttribute;
import com.google.api.ads.adwords.axis.v201809.o.MonthlySearchVolume;
import com.google.api.ads.adwords.axis.v201809.o.MonthlySearchVolumeAttribute;
import com.google.api.ads.adwords.axis.v201809.o.SearchParameter;
import com.google.api.ads.adwords.axis.v201809.o.StatsEstimate;
import com.google.api.ads.adwords.axis.v201809.o.TargetingIdeaService;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Utility functions (math, strings, ...) for various other classes in this project.
 */
public class KeywordOptimizerUtil {
  // Attribute types requested from the TargetingIdeaService.
  protected static final AttributeType[] TIS_ATTRIBUTE_TYPES = new AttributeType[] {
        AttributeType.KEYWORD_TEXT,
        AttributeType.SEARCH_VOLUME,
        AttributeType.AVERAGE_CPC,
        AttributeType.COMPETITION,
        AttributeType.TARGETED_MONTHLY_SEARCHES
      };

  private static final String PLACEHOLDER_NULL = "       ---";
  private static final String FORMAT_NUMBER = "%10.3f";
  private static final String FORMAT_MONEY = "%10.2f";
  private static final int MICRO_UNITS = 1000000;

  /**
   * Calculates the mean estimated statistics based on minimum and maximum values.
   *
   * @param min the minimum estimated statistics
   * @param max the maximum estimated statistics
   * @return the mean value for all given stats
   */
  public static StatsEstimate calculateMean(StatsEstimate min, StatsEstimate max) {
    Money meanAverageCpc = calculateMean(min.getAverageCpc(), max.getAverageCpc());
    Double meanAveragePosition = calculateMean(min.getAveragePosition(), max.getAveragePosition());
    Double meanClicks = calculateMean(min.getClicksPerDay(), max.getClicksPerDay());
    Double meanImpressions = calculateMean(min.getImpressionsPerDay(), max.getImpressionsPerDay());
    Double meanCtr = calculateMean(min.getClickThroughRate(), max.getClickThroughRate());
    Money meanTotalCost = calculateMean(min.getTotalCost(), max.getTotalCost());

    StatsEstimate mean = new StatsEstimate();

    if (meanAverageCpc != null) {
      mean.setAverageCpc(meanAverageCpc);
    }

    if (meanAveragePosition != null) {
      mean.setAveragePosition(meanAveragePosition);
    }

    if (meanClicks != null) {
      mean.setClicksPerDay(meanClicks.floatValue());
    }

    if (meanImpressions != null) {
      mean.setImpressionsPerDay(meanImpressions.floatValue());
    }

    if (meanCtr != null) {
      mean.setClickThroughRate(meanCtr);
    }

    if (meanTotalCost != null) {
      mean.setTotalCost(meanTotalCost);
    }

    return mean;
  }

  /**
   * Returns the mean of the two {@link Money} values if neither is null, else returns null.
   *
   * @param value1 first value
   * @param value2 second value
   * @return the mean of the two {@link Money} values
   */
  private static Money calculateMean(Money value1, Money value2) {
    if (value1 == null || value2 == null) {
      return null;
    }

    Double meanAmount = calculateMean(value1.getMicroAmount(), value2.getMicroAmount());
    if (meanAmount == null) {
      return null;
    }

    Money mean = new Money();
    mean.setMicroAmount(meanAmount.longValue());

    return mean;
  }

  /**
   * Returns the mean of the two {@link Number} values if neither is null, else returns null.
   *
   * @param value1 first value
   * @param value2 second value
   * @return the mean of the two {@link Money} values
   */
  private static Double calculateMean(Number value1, Number value2) {
    if (value1 == null || value2 == null) {
      return null;
    }
    return (value1.doubleValue() + value2.doubleValue()) / 2;
  }


  /**
   * Returns a string representation of the given {@link Keyword}. Please note, as not all classes
   * belong to the project itself, toString methods are bundled here.
   * 
   * @param keyword the keyword
   * @return a string representation of the keyword
   */
  public static String toString(Keyword keyword) {
    return keyword.getText() + "[" + keyword.getMatchType().getValue() + "]";
  }

  /**
   * Returns a string representation of the given {@link StatsEstimate}. Please note, as not all
   * classes belong to the project itself, toString methods are bundled here.
   *
   * @param estimate the estimate
   * @return a string representation of the estimate
   */
  public static String toString(StatsEstimate estimate) {
    return String.format(
        "Imp: %s Cli: %s Ctr: %s Pos: %s Cpc: %s Cos: %s",
        format(estimate.getImpressionsPerDay()),
        format(estimate.getClicksPerDay()),
        format(estimate.getClickThroughRate()),
        format(estimate.getAveragePosition()),
        format(estimate.getAverageCpc()),
        format(estimate.getTotalCost()));
  }

  /**
   * Convenience method for creating a new keyword.
   *
   * @param text the keyword text
   * @param matchType the match type (BROAD, PHRASE, EXACT)
   * @return the newly created {@link Keyword}
   */
  public static Keyword createKeyword(String text, KeywordMatchType matchType) {
    Keyword keyword = new Keyword();
    keyword.setMatchType(matchType);
    keyword.setText(text);
    return keyword;
  }

  /**
   * Convenience method for creating a money object.
   *
   * @param microAmount the amount in micros
   * @return the newly created {@link Money} object
   */
  public static Money createMoney(long microAmount) {
    Money money = new Money();
    money.setMicroAmount(microAmount);
    return money;
  }

  /**
   * Formats a given number in a default format (3 decimals, padded left to 10 characters).
   *
   * @param nr a number
   * @return a string version of the number
   */
  private static String format(Float nr) {
    if (nr == null) {
      return PLACEHOLDER_NULL;
    }

    return String.format(FORMAT_NUMBER, nr);
  }

  /**
   * Formats a given number in a default format (3 decimals, padded left to 10 characters).
   *
   * @param nr a number
   * @return a string version of the number
   */
  public static String format(Double nr) {
    if (nr == null) {
      return PLACEHOLDER_NULL;
    }

    return String.format(FORMAT_NUMBER, nr);
  }

  /**
   * Formats a given monetary value in a default format (2 decimals, padded left to 10 characters).
   *
   * @param money a monetary value
   * @return a string version of the monetary value
   */
  public static String format(Money money) {
    long microAmount;
    if (money != null) {
      microAmount = money.getMicroAmount();
    } else {
      return PLACEHOLDER_NULL;
    }

    double amount = (double) microAmount / MICRO_UNITS;
    return String.format(FORMAT_MONEY, amount);
  }

  /**
   * Formats a given number for CSV output (effectively handles null values).
   *
   * @param number a number or null
   * @return a string version of the number or the empty string if number is null.
   */
  public static String formatCsv(@Nullable Number number) {
    return null == number ? "" : number.toString();
  }

  /**
   * Returns all objects in the given list that are instances of the given class.
   *
   * @param input list of objects to look through
   * @param typeClass class of the objects to filter
   * @return an array of all objects in the given list that are instances of the given class
   */
  @SuppressWarnings(value = "unchecked")
  private static <T> T[] getAllOfType(List<?> input, Class<T> typeClass) {
    List<T> allEntriesOfType = new ArrayList<T>();

    for (Object o : input) {
      if (typeClass.isInstance(o)) {
        allEntriesOfType.add((T) o);
      }
    }

    return allEntriesOfType.toArray((T[]) Array.newInstance(typeClass, 0));
  }

  /**
   * Converts a list of given trigger criteria to according {@link SearchParameter}s for the
   * TargetingIdeaService.
   *
   * @param criteria the criteria to be converted
   * @return a list of according {@link SearchParameter}s
   */
  public static List<SearchParameter> toSearchParameters(List<Criterion> criteria) {
    List<SearchParameter> parameters = new ArrayList<>();

    // Take all location criteria and add as one searchParameter.
    Location[] allLocations = KeywordOptimizerUtil.getAllOfType(criteria, Location.class);
    if (allLocations.length > 0) {
      LocationSearchParameter locationParameter = new LocationSearchParameter();
      locationParameter.setLocations(allLocations);
      parameters.add(locationParameter);
    }

    // Take all language criteria and add as one searchParameter.
    Language[] allLanguages = KeywordOptimizerUtil.getAllOfType(criteria, Language.class);
    if (allLanguages.length > 0) {
      LanguageSearchParameter languageParameter = new LanguageSearchParameter();
      languageParameter.setLanguages(allLanguages);
      parameters.add(languageParameter);
    }

    // Any others are not supported right now.
    return parameters;
  }
  
  /**
   * Converts a given map of attribute data from the {@link TargetingIdeaService} to {@link
   * IdeaEstimate} object.
   *
   * @param attributeData map of attribute data as returned by the {@link TargetingIdeaService}
   * @return a {@link IdeaEstimate} object containing typed data
   */
  public static IdeaEstimate toSearchEstimate(Map<AttributeType, Attribute> attributeData) {
    LongAttribute searchVolumeAttribute =
        (LongAttribute) attributeData.get(AttributeType.SEARCH_VOLUME);
    MoneyAttribute averageCpcAttribute =
        (MoneyAttribute) attributeData.get(AttributeType.AVERAGE_CPC);
    DoubleAttribute competitionAttribute =
        (DoubleAttribute) attributeData.get(AttributeType.COMPETITION);
    MonthlySearchVolumeAttribute targetedMonthlySearchesAttribute =
        (MonthlySearchVolumeAttribute) attributeData.get(AttributeType.TARGETED_MONTHLY_SEARCHES);

    double competition = 0D;
    if (competitionAttribute != null && competitionAttribute.getValue() != null) {
      competition = competitionAttribute.getValue();
    }
    long searchVolume = 0L;
    if (searchVolumeAttribute != null && searchVolumeAttribute.getValue() != null) {
      searchVolume = searchVolumeAttribute.getValue();
    }
    Money averageCpc = KeywordOptimizerUtil.createMoney(0L);
    if (averageCpcAttribute != null && averageCpcAttribute.getValue() != null) {
      averageCpc = averageCpcAttribute.getValue();
    }
    MonthlySearchVolume[] targetedMonthlySearches = new MonthlySearchVolume[] {};
    if (targetedMonthlySearchesAttribute != null
        && targetedMonthlySearchesAttribute.getValue() != null) {
      targetedMonthlySearches = targetedMonthlySearchesAttribute.getValue();
    }

    return new IdeaEstimate(competition, searchVolume, averageCpc, targetedMonthlySearches);
  }
}