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

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.fudgemsg.FudgeMsg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;

import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.component.ComponentRepository;
import com.opengamma.component.factory.engine.RemoteEngineContextsComponentFactory;
import com.opengamma.component.tool.AbstractTool;
import com.opengamma.core.config.impl.ConfigItem;
import com.opengamma.engine.depgraph.ambiguity.FullRequirementResolution;
import com.opengamma.engine.depgraph.ambiguity.FullRequirementResolutionPrinter;
import com.opengamma.engine.depgraph.ambiguity.RequirementResolution;
import com.opengamma.engine.depgraph.ambiguity.ViewDefinitionAmbiguityTest;
import com.opengamma.engine.function.CachingFunctionRepositoryCompiler;
import com.opengamma.engine.function.CompiledFunctionDefinition;
import com.opengamma.engine.function.CompiledFunctionService;
import com.opengamma.engine.function.FunctionCompilationContext;
import com.opengamma.engine.function.config.FunctionConfigurationSource;
import com.opengamma.engine.function.exclusion.FunctionExclusionGroups;
import com.opengamma.engine.function.resolver.DefaultFunctionResolver;
import com.opengamma.engine.function.resolver.FunctionPriority;
import com.opengamma.engine.function.resolver.FunctionResolver;
import com.opengamma.engine.value.ValueSpecification;
import com.opengamma.engine.view.ViewDefinition;
import com.opengamma.financial.function.rest.RemoteFunctionConfigurationSource;
import com.opengamma.financial.tool.ToolContext;
import com.opengamma.id.VersionCorrection;
import com.opengamma.scripts.Scriptable;
import com.opengamma.transport.jaxrs.UriEndPointDescriptionProvider;
import com.opengamma.util.ClassUtils;

/**
 * Tool class that compiles a view against a function repository to identify any ambiguities.
 * <p>
 * The configuration is fetched from the server, but the function exclusion groups and priorities must be specified manually as these are not readily available
 * via REST interfaces (at the moment - there is no reason why the data couldn't be available). Any ambiguities are written to a nominated output file.
 * <p>
 * Note that this can be an expensive operation - very expensive if there are multiple deep ambiguities which will cause an incredibly large number of possible
 * terminal output resolutions (based on the cross product of all possible inputs). It is normally only necessary to run on small portfolio samples, for example
 * by setting the system property <code>SimplePortfolioNode.debugFlag</code> to <code>TRUE</code>.
 */
@Scriptable
public class FindViewAmbiguities extends AbstractTool<ToolContext> {

  /** Logger */
  private static final Logger LOGGER = LoggerFactory.getLogger(FindViewAmbiguities.class);

  private static final String VIEW_NAME_OPTION = "v";
  private static final String VIEW_NAME_OPTION_LONG = "view";
  // -xg com.opengamma.web.spring.DemoFunctionExclusionGroupsFactoryBean
  private static final String EXCLUSION_GROUPS_OPTION = "xg";
  private static final String EXCLUSION_GROUPS_OPTION_LONG = "exclusionGroups";
  // -fp com.opengamma.web.spring.DemoFunctionResolverFactoryBean$Priority
  private static final String FUNCTION_PRIORITY_OPTION = "fp";
  private static final String FUNCTION_PRIOTITY_OPTION_LONG = "functionPriority";
  private static final String OUTPUT_OPTION = "o";
  private static final String OUTPUT_OPTION_LONG = "output";
  private static final String VERBOSE_OPTION = "f";
  private static final String VERBOSE_OPTION_LONG = "full";

  private final AtomicInteger _resolutions = new AtomicInteger();
  private final AtomicInteger _ambiguities = new AtomicInteger();

  // -------------------------------------------------------------------------
  /**
   * Main method to run the tool.
   *
   * @param args
   *          the standard tool arguments, not null
   */
  public static void main(final String[] args) { // CSIGNORE
    new FindViewAmbiguities().invokeAndTerminate(args);
  }

  // -------------------------------------------------------------------------
  private final class ViewDefinitionAmbiguityTestImpl extends ViewDefinitionAmbiguityTest {

    private final PrintStream _out;
    private FunctionResolver _functionResolver;

    ViewDefinitionAmbiguityTestImpl(final PrintStream out) {
      _out = out;
    }

    @Override
    protected FunctionCompilationContext createFunctionCompilationContext() {
      final ComponentRepository repo = (ComponentRepository) getToolContext().getContextManager();
      return repo.getInstance(FunctionCompilationContext.class, "main");
    }

    protected FunctionPriority createFunctionPrioritizer() {
      // TODO: The prioritizer could be exposed over the network (sending the function identifier) and cached
      final String functionPriorities = getCommandLine().getOptionValue(FUNCTION_PRIORITY_OPTION);
      if (functionPriorities != null) {
        try {
          final Class<?> functionPrioritiesClass = ClassUtils.loadClass(functionPriorities);
          Object prioritiesObject = functionPrioritiesClass.newInstance();
          if (prioritiesObject instanceof InitializingBean) {
            ((InitializingBean) prioritiesObject).afterPropertiesSet();
          }
          if (prioritiesObject instanceof FactoryBean) {
            prioritiesObject = ((FactoryBean<?>) prioritiesObject).getObject();
          }
          if (prioritiesObject instanceof FunctionPriority) {
            return (FunctionPriority) prioritiesObject;
          }
        } catch (final Exception e) {
          throw new OpenGammaRuntimeException("Error loading function priorities", e);
        }
      }
      return new FunctionPriority() {
        @Override
        public int getPriority(final CompiledFunctionDefinition function) {
          return 0;
        }
      };
    }

    @Override
    protected FunctionResolver createFunctionResolver() {
      if (_functionResolver == null) {
        final FunctionCompilationContext context = createFunctionCompilationContext();
        final FudgeMsg configMsg = RemoteEngineContextsComponentFactory.getConfiguration(context);
        final URI configUri = RemoteEngineContextsComponentFactory.getConfigurationUri(context);
        final URI functionsUri;
        final ExecutorService executor = Executors.newCachedThreadPool();
        try {
          functionsUri = UriEndPointDescriptionProvider.getAccessibleURI(executor, configUri, configMsg.getMessage("functionRepositoryConfiguration"));
        } finally {
          executor.shutdown();
        }
        LOGGER.debug("Fetching remote functions from {}", functionsUri);
        final FunctionConfigurationSource functions = new RemoteFunctionConfigurationSource(functionsUri);
        final CompiledFunctionService compiledFunctionService = new CompiledFunctionService(functions, new CachingFunctionRepositoryCompiler(), context);
        compiledFunctionService.initialize();
        _functionResolver = new DefaultFunctionResolver(compiledFunctionService, createFunctionPrioritizer());
      }
      return _functionResolver;
    }

    @Override
    protected FunctionExclusionGroups createFunctionExclusionGroups() {
      // TODO: The exclusion groups could be exposed over the network (sending the function identifier) and cached
      final String exclusionGroups = getCommandLine().getOptionValue(EXCLUSION_GROUPS_OPTION);
      if (exclusionGroups != null) {
        try {
          final Class<?> exclusionGroupsClass = ClassUtils.loadClass(exclusionGroups);
          Object groupsObject = exclusionGroupsClass.newInstance();
          if (groupsObject instanceof InitializingBean) {
            ((InitializingBean) groupsObject).afterPropertiesSet();
          }
          if (groupsObject instanceof FactoryBean) {
            groupsObject = ((FactoryBean<?>) groupsObject).getObject();
          }
          if (groupsObject instanceof FunctionExclusionGroups) {
            return (FunctionExclusionGroups) groupsObject;
          }
          throw new IllegalArgumentException("Couldn't set exclusion groups to " + exclusionGroups + " (got " + groupsObject + ")");
        } catch (final Exception e) {
          throw new OpenGammaRuntimeException("Error loading exclusion groups", e);
        }
      }
      return null;
    }

    @Override
    protected void resolved(final FullRequirementResolution resolution) {
      resolvedImpl(resolution);
      final int count = _resolutions.incrementAndGet();
      if (count % 100 == 0) {
        LOGGER.info("Checked {} resolutions", count);
      }
      if (resolution.isDeeplyAmbiguous() && getCommandLine().hasOption(VERBOSE_OPTION)) {
        synchronized (this) {
          new FullRequirementResolutionPrinter(_out).print(resolution);
        }
      }
    }

    protected void resolvedImpl(final FullRequirementResolution resolution) {
      super.resolved(resolution);
    }

    @Override
    protected synchronized void directAmbiguity(final FullRequirementResolution resolution) {
      final int count = _ambiguities.incrementAndGet();
      if (count % 10 == 0) {
        LOGGER.info("Found {} ambiguities", count);
      }
      _out.println(resolution.getRequirement());
      for (final Collection<RequirementResolution> nestedResolutions : resolution.getResolutions()) {
        final List<String> functions = new ArrayList<>();
        final List<ValueSpecification> specifications = new ArrayList<>();
        boolean failure = false;
        for (final RequirementResolution nestedResolution : nestedResolutions) {
          if (nestedResolution != null) {
            functions.add(nestedResolution.getFunction().getFunctionId());
            specifications.add(nestedResolution.getSpecification());
          } else {
            failure = true;
          }
        }
        for (final String function : functions) {
          _out.println("\t" + function);
        }
        if (failure) {
          _out.println("\t+ failure(s)");
        }
        for (final ValueSpecification specification : specifications) {
          _out.println("\t" + specification);
        }
        _out.println();
      }
    }

    @Override
    protected synchronized void deepAmbiguity(final FullRequirementResolution resolution) {
      for (final Collection<RequirementResolution> nestedResolutions : resolution.getResolutions()) {
        for (final RequirementResolution nestedResolution : nestedResolutions) {
          for (final FullRequirementResolution inputResolution : nestedResolution.getInputs()) {
            resolvedImpl(inputResolution);
          }
        }
      }
      _out.println(resolution.getRequirement());
      for (final Collection<RequirementResolution> nestedResolutions : resolution.getResolutions()) {
        for (final RequirementResolution nestedResolution : nestedResolutions) {
          _out.println("\t" + nestedResolution.getSpecification());
        }
      }
      _out.println();
    }

  }

  private static Option createViewNameOption() {
    final Option option = new Option(VIEW_NAME_OPTION, VIEW_NAME_OPTION_LONG, true, "the view to check, omit to check all");
    option.setArgName("view");
    option.setRequired(false);
    return option;
  }

  private static Option createFunctionExclusionGroupsBeanOption() {
    final Option option = new Option(EXCLUSION_GROUPS_OPTION, EXCLUSION_GROUPS_OPTION_LONG, true, "the exclusion groups to use");
    option.setArgName("class");
    option.setRequired(false);
    return option;
  }

  private static Option createFunctionPrioritiesOption() {
    final Option option = new Option(FUNCTION_PRIORITY_OPTION, FUNCTION_PRIOTITY_OPTION_LONG, true, "the function prioritizer to use");
    option.setArgName("class");
    option.setRequired(false);
    return option;
  }

  private static Option createOutputOption() {
    final Option option = new Option(OUTPUT_OPTION, OUTPUT_OPTION_LONG, true, "the output file to write");
    option.setArgName("filename");
    option.setRequired(false);
    return option;
  }

  private static Option createVerboseOption() {
    final Option option = new Option(VERBOSE_OPTION, VERBOSE_OPTION_LONG, false, "whether to write out ambiguities in full");
    option.setRequired(false);
    return option;
  }

  private static PrintStream openStream(final String filename) throws IOException {
    if (filename == null || "stdout".equals(filename)) {
      return System.out;
    } else if ("stderr".equals(filename)) {
      return System.err;
    } else {
      return new PrintStream(new FileOutputStream(filename));
    }
  }

  // AbstractTool

  @Override
  protected void doRun() throws Exception {
    final PrintStream out = openStream(getCommandLine().getOptionValue(OUTPUT_OPTION));
    final ViewDefinitionAmbiguityTest test = new ViewDefinitionAmbiguityTestImpl(out);
    final String viewName = getCommandLine().getOptionValue(VIEW_NAME_OPTION);
    int count = 0;
    if (viewName != null) {
      LOGGER.info("Testing {}", viewName);
      final ViewDefinition viewDefinition = getToolContext().getConfigSource().getLatestByName(ViewDefinition.class, viewName);
      if (viewDefinition == null) {
        throw new IllegalArgumentException("View definition " + viewName + " not found");
      }
      out.println("View = " + viewName);
      test.runAmbiguityTest(viewDefinition);
      count++;
    } else {
      final Collection<ConfigItem<ViewDefinition>> viewDefinitions = getToolContext().getConfigSource().getAll(ViewDefinition.class, VersionCorrection.LATEST);
      LOGGER.info("Testing {} view definition(s)", viewDefinitions.size());
      for (final ConfigItem<ViewDefinition> viewDefinitionConfig : viewDefinitions) {
        final ViewDefinition viewDefinition = viewDefinitionConfig.getValue();
        LOGGER.info("Testing {}", viewDefinition.getName());
        out.println("View = " + viewDefinition.getName());
        final int resolutions = _resolutions.get();
        final int ambiguities = _ambiguities.get();
        test.runAmbiguityTest(viewDefinition);
        count++;
        out.println("Resolutions = " + (_resolutions.get() - resolutions));
        out.println("Ambiguities = " + (_ambiguities.get() - ambiguities));
      }
    }
    LOGGER.info("{} view(s) tested", count);
    out.println("Total resolutions = " + _resolutions.get());
    out.println("Total ambiguities = " + _ambiguities.get());
    out.close();
  }

  @Override
  protected Options createOptions(final boolean mandatoryConfigResource) {
    final Options options = super.createOptions(mandatoryConfigResource);
    options.addOption(createViewNameOption());
    options.addOption(createFunctionExclusionGroupsBeanOption());
    options.addOption(createFunctionPrioritiesOption());
    options.addOption(createOutputOption());
    options.addOption(createVerboseOption());
    return options;
  }

}
