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

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

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.core.position.PortfolioNode;
import com.opengamma.core.position.Position;
import com.opengamma.engine.ComputationTarget;
import com.opengamma.engine.function.AbstractFunction;
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.engine.view.ViewCalculationConfiguration;
import com.opengamma.engine.view.ViewCalculationConfiguration.MergedOutput;
import com.opengamma.financial.analytics.DoubleCurrencyLabelledMatrix2D;
import com.opengamma.financial.analytics.DoubleLabelledMatrix1D;
import com.opengamma.financial.analytics.SumUtils;
import com.opengamma.util.async.AsynchronousExecution;
import com.opengamma.util.money.Currency;
import com.opengamma.util.money.CurrencyAmount;

/**
 * Calculates simple, linear aggregates at the portfolio node level over merged outputs.
 */
public class MergedOutputLinearAggregationPortfolioNodeFunction extends AbstractFunction.NonCompiledInvoker {

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

  @Override
  public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target) {
    return ImmutableSet.of(ValueSpecification.of(ValueRequirementNames.MERGED_OUTPUT, target.toSpecification(), ValueProperties.all()));
  }

  @Override
  public Set<ValueRequirement> getRequirements(final FunctionCompilationContext context, final ComputationTarget target, final ValueRequirement desiredValue) {
    final String mergedOutputName = desiredValue.getConstraint(ValuePropertyNames.NAME);
    final ViewCalculationConfiguration calcConfig = context.getViewCalculationConfiguration();
    final MergedOutput mergedOutput = calcConfig.getMergedOutput(mergedOutputName);
    if (mergedOutput == null) {
      return null;
    }
    final Set<ValueRequirement> requirements = new HashSet<>();
    final PortfolioNode node = target.getPortfolioNode();
    for (final Position position : node.getPositions()) {
      requirements.add(
          new ValueRequirement(ValueRequirementNames.MERGED_OUTPUT, ComputationTargetType.POSITION, position.getUniqueId(), desiredValue.getConstraints()));
    }
    for (final PortfolioNode childNode : node.getChildNodes()) {
      requirements.add(new ValueRequirement(ValueRequirementNames.MERGED_OUTPUT, ComputationTargetType.PORTFOLIO_NODE, childNode.getUniqueId(),
          desiredValue.getConstraints()));
    }
    return requirements;
  }

  @Override
  public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target,
      final Map<ValueSpecification, ValueRequirement> inputs) {
    final ValueRequirement exampleInput = Iterables.getFirst(inputs.values(), null);
    if (exampleInput == null) {
      return null;
    }
    final String mergedOutputName = exampleInput.getConstraint(ValuePropertyNames.NAME);
    final ValueProperties properties = createValueProperties().with(ValuePropertyNames.NAME, mergedOutputName).get();
    return ImmutableSet.of(ValueSpecification.of(ValueRequirementNames.MERGED_OUTPUT, target.toSpecification(), properties));
  }

  @Override
  public Set<ComputedValue> execute(final FunctionExecutionContext executionContext, final FunctionInputs inputs, final ComputationTarget target,
      final Set<ValueRequirement> desiredValues) throws AsynchronousExecution {
    // TODO jonathan 2014-01-13 -- as a proof-of-concept this supports aggregating very specific types. It should be
    // extended as required.
    final ValueRequirement desiredValue = Iterables.getOnlyElement(desiredValues);
    final String mergedOutputName = desiredValue.getConstraint(ValuePropertyNames.NAME);
    Object value = null;
    for (final ComputedValue input : inputs.getAllValues()) {
      Object inputValue = input.getValue();
      if (inputValue instanceof String) {
        if (((String) inputValue).length() == 0) {
          continue;
        }
      }
      final String ccyCode = input.getSpecification().getProperty(ValuePropertyNames.CURRENCY);
      final Currency ccy = ccyCode != null ? Currency.parse(ccyCode) : null;
      if (ccy != null) {
        // When merging outputs we want to allow aggregation across currencies, so we add another dimension to known
        // data structures to include the currency
        if (inputValue instanceof Double) {
          inputValue = CurrencyAmount.of(ccy, (double) inputValue);
        } else if (inputValue instanceof DoubleLabelledMatrix1D) {
          final DoubleLabelledMatrix1D inputMatrix = (DoubleLabelledMatrix1D) inputValue;
          // TODO jonathan 2014-01-08 -- an optional constraint should control whether we aggregate by key or label
          // We default to this since we usually want to see the matrix aggregated across currencies by label (e.g. 7D)
          // rather than repeating labels where the keys are slightly different (e.g. due to holiday differences)
          if (value != null && !(value instanceof DoubleCurrencyLabelledMatrix2D)) {
            throw new OpenGammaRuntimeException("Unable to aggregate " + value.getClass() + " with " + DoubleCurrencyLabelledMatrix2D.class);
          }
          inputValue = new DoubleCurrencyLabelledMatrix2D(
              inputMatrix.getKeys(), inputMatrix.getLabels(), inputMatrix.getLabelsTitle(),
              new Currency[] { ccy }, new Currency[] { ccy }, ValuePropertyNames.CURRENCY,
              new double[][] { inputMatrix.getValues() }, inputMatrix.getValuesTitle());
        }
      }
      final String requirementDisplayName = ValueRequirementNames.MERGED_OUTPUT + " (" + mergedOutputName + ")";
      value = addValue(value, inputValue, requirementDisplayName);
    }
    if (value == null) {
      value = "";
    }
    return Collections.singleton(
        new ComputedValue(new ValueSpecification(ValueRequirementNames.MERGED_OUTPUT, target.toSpecification(), desiredValue.getConstraints()), value));
  }

  protected Object addValue(final Object previousSum, final Object currentValue, final String requirementDisplayName) {
    if (previousSum == null) {
      return currentValue;
    }
    if (previousSum instanceof DoubleCurrencyLabelledMatrix2D && currentValue instanceof DoubleCurrencyLabelledMatrix2D) {
      final DoubleCurrencyLabelledMatrix2D previousSumMatrix = (DoubleCurrencyLabelledMatrix2D) previousSum;
      final DoubleCurrencyLabelledMatrix2D currentValueMatrix = (DoubleCurrencyLabelledMatrix2D) currentValue;
      return previousSumMatrix.addUsingDoubleLabels(currentValueMatrix);
    }
    return SumUtils.addValue(previousSum, currentValue, requirementDisplayName);
  }

  @Override
  public boolean canHandleMissingInputs() {
    return true;
  }

}
