/**
 * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.financial.analytics.model.pnl;

import static com.opengamma.engine.value.ValuePropertyNames.CURRENCY;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.threeten.bp.Instant;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.core.position.Position;
import com.opengamma.core.security.Security;
import com.opengamma.engine.ComputationTarget;
import com.opengamma.engine.ComputationTargetSpecification;
import com.opengamma.engine.function.AbstractFunction;
import com.opengamma.engine.function.CompiledFunctionDefinition;
import com.opengamma.engine.function.FunctionCompilationContext;
import com.opengamma.engine.function.FunctionExecutionContext;
import com.opengamma.engine.function.FunctionInputs;
import com.opengamma.engine.target.ComputationTargetType;
import com.opengamma.engine.value.ComputedValue;
import com.opengamma.engine.value.ValueProperties;
import com.opengamma.engine.value.ValuePropertyNames;
import com.opengamma.engine.value.ValueRequirement;
import com.opengamma.engine.value.ValueRequirementNames;
import com.opengamma.engine.value.ValueSpecification;
import com.opengamma.financial.OpenGammaCompilationContext;
import com.opengamma.financial.analytics.model.forex.ForexVisitors;
import com.opengamma.financial.currency.CurrencyPair;
import com.opengamma.financial.currency.CurrencyPairs;
import com.opengamma.financial.security.FinancialSecurity;
import com.opengamma.financial.security.fx.FXForwardSecurity;
import com.opengamma.financial.security.fx.NonDeliverableFXForwardSecurity;
import com.opengamma.timeseries.DoubleTimeSeries;
import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeries;
import com.opengamma.util.async.AsynchronousExecution;
import com.opengamma.util.money.Currency;

/**
 * Produces the aggregated P&amp;L series across both curves for an FX Forward.
 */
public class FXForwardYieldCurvesPnLFunction extends AbstractFunction {

  @Override
  public CompiledFunctionDefinition compile(final FunctionCompilationContext context, final Instant atInstant) {
    final CurrencyPairs currencyPairs = OpenGammaCompilationContext.getCurrencyPairsSource(context).getCurrencyPairs(CurrencyPairs.DEFAULT_CURRENCY_PAIRS);
    return new Compiled(currencyPairs);
  }

  /**
   * The compiled form.
   */
  protected class Compiled extends AbstractInvokingCompiledFunction {

    private final CurrencyPairs _currencyPairs;

    public Compiled(final CurrencyPairs currencyPairs) {
      _currencyPairs = currencyPairs;
    }

    // CompiledFunctionDefinition

    @Override
    public ComputationTargetType getTargetType() {
      return ComputationTargetType.POSITION;
    }

    @Override
    public boolean canApplyTo(final FunctionCompilationContext context, final ComputationTarget target) {
      final Security security = target.getPosition().getSecurity();
      return security instanceof FXForwardSecurity || security instanceof NonDeliverableFXForwardSecurity;
    }

    @Override
    public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target) {
      return Sets.newHashSet(new ValueSpecification(ValueRequirementNames.PNL_SERIES, target.toSpecification(),
          ValueProperties.all().withoutAny(ValuePropertyNames.CURVE_CURRENCY)));
    }

    @Override
    public Set<ValueRequirement> getRequirements(final FunctionCompilationContext context, final ComputationTarget target,
        final ValueRequirement desiredValue) {
      final FXForwardSecurity security = (FXForwardSecurity) target.getPosition().getSecurity();
      final ValueProperties constraints = desiredValue.getConstraints();
      final Set<String> resultCurrencies = constraints.getValues(CURRENCY);
      final Currency payCurrency = security.accept(ForexVisitors.getPayCurrencyVisitor());
      final Currency receiveCurrency = security.accept(ForexVisitors.getReceiveCurrencyVisitor());
      String resultCurrency;
      if (resultCurrencies == null || resultCurrencies.size() != 1) {
        final CurrencyPair baseQuotePair = _currencyPairs.getCurrencyPair(payCurrency, receiveCurrency);
        final Currency baseCurrency = baseQuotePair.getBase();
        resultCurrency = baseCurrency.getCode();
      } else {
        resultCurrency = Iterables.getOnlyElement(resultCurrencies);
      }
      final ComputationTargetSpecification targetSpec = target.toSpecification();
      final ValueRequirement payPnLSeriesRequirement = getPnLSeriesRequirement(payCurrency, targetSpec, constraints);
      final ValueRequirement receivePnLSeriesRequirement = getPnLSeriesRequirement(receiveCurrency, targetSpec, constraints);
      return ImmutableSet.of(payPnLSeriesRequirement, receivePnLSeriesRequirement);
    }

    @Override
    public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target,
        final Map<ValueSpecification, ValueRequirement> inputs) {
      final ValueProperties.Builder builder = createValueProperties();
      for (final ValueSpecification inputSpec : inputs.keySet()) {
        for (final String propertyName : inputSpec.getProperties().getProperties()) {
          if (ValuePropertyNames.FUNCTION.equals(propertyName)) {
            continue;
          }
          final Set<String> values = inputSpec.getProperties().getValues(propertyName);
          if (values == null || values.isEmpty()) {
            builder.withAny(propertyName);
          } else {
            builder.with(propertyName, values);
          }
        }
      }
      return ImmutableSet.of(new ValueSpecification(ValueRequirementNames.PNL_SERIES, target.toSpecification(), builder.get()));
    }

    @Override
    public Set<ComputedValue> execute(final FunctionExecutionContext executionContext, final FunctionInputs inputs, final ComputationTarget target,
        final Set<ValueRequirement> desiredValues) throws AsynchronousExecution {
      final Position position = target.getPosition();
      final FinancialSecurity security = (FinancialSecurity) position.getSecurity();
      final Currency payCurrency = security.accept(ForexVisitors.getPayCurrencyVisitor());
      final Currency receiveCurrency = security.accept(ForexVisitors.getReceiveCurrencyVisitor());

      LocalDateDoubleTimeSeries payPnLSeries = null;
      LocalDateDoubleTimeSeries receivePnLSeries = null;
      for (final ComputedValue input : inputs.getAllValues()) {
        if (!ValueRequirementNames.PNL_SERIES.equals(input.getSpecification().getValueName())) {
          continue;
        }
        final LocalDateDoubleTimeSeries pnlSeries = (LocalDateDoubleTimeSeries) input.getValue();
        final Currency pnlSeriesCurveCurrency = Currency.of(input.getSpecification().getProperty(ValuePropertyNames.CURVE_CURRENCY));
        if (pnlSeriesCurveCurrency.equals(payCurrency)) {
          payPnLSeries = pnlSeries;
        } else if (pnlSeriesCurveCurrency.equals(receiveCurrency)) {
          receivePnLSeries = pnlSeries;
        }
      }
      if (payPnLSeries == null || receivePnLSeries == null) {
        throw new OpenGammaRuntimeException("Unable to find both pay and receive curve P&L series in inputs");
      }
      final DoubleTimeSeries<?> result = payPnLSeries.add(receivePnLSeries);
      final ValueRequirement desiredValue = desiredValues.iterator().next();
      final ValueSpecification resultSpec = new ValueSpecification(ValueRequirementNames.PNL_SERIES, target.toSpecification(), desiredValue.getConstraints());
      return Collections.singleton(new ComputedValue(resultSpec, result));
    }

    private ValueRequirement getPnLSeriesRequirement(final Currency currency, final ComputationTargetSpecification targetSpec,
        final ValueProperties constraints) {
      final ValueProperties newConstraints = constraints.copy().with(ValuePropertyNames.CURVE_CURRENCY, currency.getCode()).get();
      return new ValueRequirement(ValueRequirementNames.PNL_SERIES, targetSpec, newConstraints);
    }
  }

}
