/**
 * 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(FunctionCompilationContext context, ComputationTarget target) {
    return ImmutableSet.of(ValueSpecification.of(ValueRequirementNames.MERGED_OUTPUT, target.toSpecification(), ValueProperties.all()));
  }

  @Override
  public Set<ValueRequirement> getRequirements(FunctionCompilationContext context, ComputationTarget target, ValueRequirement desiredValue) {
    String mergedOutputName = desiredValue.getConstraint(ValuePropertyNames.NAME);
    ViewCalculationConfiguration calcConfig = context.getViewCalculationConfiguration();
    MergedOutput mergedOutput = calcConfig.getMergedOutput(mergedOutputName);
    if (mergedOutput == null) {
      return null;
    }
    final Set<ValueRequirement> requirements = new HashSet<ValueRequirement>();
    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(FunctionCompilationContext context, ComputationTarget target, Map<ValueSpecification, ValueRequirement> inputs) {
    ValueRequirement exampleInput = Iterables.getFirst(inputs.values(), null);
    if (exampleInput == null) {
      return null;
    }
    String mergedOutputName = exampleInput.getConstraint(ValuePropertyNames.NAME);
    ValueProperties properties = createValueProperties().with(ValuePropertyNames.NAME, mergedOutputName).get();
    return ImmutableSet.of(ValueSpecification.of(ValueRequirementNames.MERGED_OUTPUT, target.toSpecification(), properties));
  }

  @Override
  public Set<ComputedValue> execute(FunctionExecutionContext executionContext, FunctionInputs inputs, ComputationTarget target, 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. 
    ValueRequirement desiredValue = Iterables.getOnlyElement(desiredValues);
    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;
        }
      }
      String ccyCode = input.getSpecification().getProperty(ValuePropertyNames.CURRENCY);
      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) {
          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());
        }
      }
      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(Object previousSum, Object currentValue, String requirementDisplayName) {
    if (previousSum == null) {
      return currentValue;
    }
    if (previousSum instanceof DoubleCurrencyLabelledMatrix2D && currentValue instanceof DoubleCurrencyLabelledMatrix2D) {
      DoubleCurrencyLabelledMatrix2D previousSumMatrix = (DoubleCurrencyLabelledMatrix2D) previousSum;
      DoubleCurrencyLabelledMatrix2D currentValueMatrix = (DoubleCurrencyLabelledMatrix2D) currentValue;
      return previousSumMatrix.addUsingDoubleLabels(currentValueMatrix);
    }
    return SumUtils.addValue(previousSum, currentValue, requirementDisplayName);
  }

}

@@ -7369,10 +7369,90 @@
 );%0A  %7D%0A%0A
+  @Override%0A  public boolean canHandleMissingInputs() %7B%0A    return true;%0A  %7D%0A  %0A
 %7D%0A

Support aggregation with missing inputs