/*
 * Copyright 2010 the original author or authors.
 *
 * 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 org.spockframework.runtime;

import java.io.File;
import java.security.AccessControlException;
import java.util.*;

import org.junit.runner.notification.RunNotifier;

import org.spockframework.builder.DelegatingScript;
import org.spockframework.runtime.condition.*;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.util.*;

import spock.config.RunnerConfiguration;

public class RunContext {
  private static final ThreadLocal<LinkedList<RunContext>> contextStacks =
      new ThreadLocal<LinkedList<RunContext>>() {
        protected LinkedList<RunContext> initialValue() {
          return new LinkedList<RunContext>();
        }
      };

  private final String name;
  private final File spockUserHome;
  private final DelegatingScript configurationScript;
  private final List<Class<?>> globalExtensionClasses;
  private final GlobalExtensionRegistry globalExtensionRegistry;

  private final IObjectRenderer<Object> diffedObjectRenderer = createDiffedObjectRenderer();

  private RunContext(String name, File spockUserHome,
      @Nullable DelegatingScript configurationScript, List<Class<?>> globalExtensionClasses) {
    this.name = name;
    this.spockUserHome = spockUserHome;
    this.configurationScript = configurationScript;
    this.globalExtensionClasses = globalExtensionClasses;
    globalExtensionRegistry = new GlobalExtensionRegistry(globalExtensionClasses, Arrays.asList(new RunnerConfiguration()));
  }

  private void start() {
    globalExtensionRegistry.initializeGlobalExtensions();

    if (configurationScript != null) {
      ConfigurationBuilder builder = new ConfigurationBuilder();
      builder.build(globalExtensionRegistry, configurationScript);
    }

    globalExtensionRegistry.startGlobalExtensions();
  }

  private void stop() {
    globalExtensionRegistry.stopGlobalExtensions();
  }

  public String getName() {
    return name;
  }

  // TODO: make it possible for configuration class to get at spock user home (w/o causing StackOverflowError)
  public File getSpockUserHome() {
    return spockUserHome;
  }

  public ExtensionRunner createExtensionRunner(SpecInfo spec) {
    return new ExtensionRunner(spec, globalExtensionRegistry, globalExtensionRegistry);
  }

  public ParameterizedSpecRunner createSpecRunner(SpecInfo spec, RunNotifier notifier) {
    return new ParameterizedSpecRunner(spec,
        new JUnitSupervisor(spec, notifier, createStackTraceFilter(spec), diffedObjectRenderer));
  }

  @Nullable
  public <T> T getConfiguration(Class<T> type) {
    return globalExtensionRegistry.getConfigurationByType(type);
  }

  private IStackTraceFilter createStackTraceFilter(SpecInfo spec) {
    RunnerConfiguration runnerConfig = globalExtensionRegistry.getConfigurationByType(RunnerConfiguration.class);
    return runnerConfig.filterStackTrace ? new StackTraceFilter(spec) : new DummyStackTraceFilter();
  }

  private IObjectRenderer<Object> createDiffedObjectRenderer() {
    IObjectRendererService service = new ObjectRendererService();

    service.addRenderer(Object.class, new DiffedObjectAsBeanRenderer());

    DiffedObjectAsStringRenderer asStringRenderer = new DiffedObjectAsStringRenderer();
    service.addRenderer(CharSequence.class, asStringRenderer);
    service.addRenderer(Number.class, asStringRenderer);
    service.addRenderer(Character.class, asStringRenderer);
    service.addRenderer(Boolean.class, asStringRenderer);

    service.addRenderer(Collection.class, new DiffedCollectionRenderer());
    service.addRenderer(Set.class, new DiffedSetRenderer(true));
    service.addRenderer(SortedSet.class, new DiffedSetRenderer(false));
    service.addRenderer(Map.class, new DiffedMapRenderer(true));
    service.addRenderer(SortedMap.class, new DiffedMapRenderer(false));

    DiffedArrayRenderer arrayRenderer = new DiffedArrayRenderer();
    service.addRenderer(Object[].class, arrayRenderer);
    service.addRenderer(byte[].class, arrayRenderer);
    service.addRenderer(short[].class, arrayRenderer);
    service.addRenderer(int[].class, arrayRenderer);
    service.addRenderer(long[].class, arrayRenderer);
    service.addRenderer(float[].class, arrayRenderer);
    service.addRenderer(double[].class, arrayRenderer);
    service.addRenderer(char[].class, arrayRenderer);
    service.addRenderer(boolean[].class, arrayRenderer);

    return service;
  }

  public static <T, U extends Throwable> T withNewContext(String name, File spockUserHome,
      @Nullable DelegatingScript configurationScript,
      List<Class<?>> extensionClasses, boolean inheritParentExtensions,
      IThrowableFunction<RunContext, T, U> command) throws U {
    List<Class<?>> allExtensionClasses = new ArrayList<Class<?>>(extensionClasses);
    if (inheritParentExtensions) allExtensionClasses.addAll(getCurrentExtensions());
    
    RunContext context = new RunContext(name, spockUserHome, configurationScript, allExtensionClasses);
    LinkedList<RunContext> contextStack = contextStacks.get();
    contextStack.addFirst(context);
    try {
      context.start();
      return command.apply(context);
    } finally {
      contextStack.removeFirst();
      context.stop();
    }
  }

  public static RunContext get() {
    LinkedList<RunContext> contextStack = contextStacks.get();
    RunContext context = contextStack.peek();
    if (context == null) {
      context = createBottomContext();
      contextStack.addFirst(context);
      final RunContext bottomContext = context;
      try {
        Runtime.getRuntime().addShutdownHook(new Thread("org.spockframework.runtime.RunContext.stop()") {
          @Override
          public void run() {
            bottomContext.stop();
          }
        });
      } catch (AccessControlException ignored) {
        // GAE doesn't support creating a new thread
      }
      context.start();
    }
    return context;
  }

  private static List<Class<?>> getCurrentExtensions() {
    RunContext context = contextStacks.get().peek();
    if (context == null) return Collections.emptyList();
    return context.globalExtensionClasses;
  }
  
  // This context will stay around until the thread dies.
  // It would be more accurate to remove the context once the test run
  // has finished, but the JUnit Runner SPI doesn't provide an adequate hook.
  // That said, since most environments fork a new JVM for each test run,
  // this shouldn't be much of a problem in practice.
  private static RunContext createBottomContext() {
    File spockUserHome = new File(env("spock.user.home", "SPOCK_USER_HOME", System.getProperty("user.home") + "/.spock"));
    DelegatingScript script = new ConfigurationScriptLoader(spockUserHome).loadAutoDetectedScript();
    List<Class<?>> classes = new ExtensionClassesLoader().loadClassesFromDefaultLocation();
    return new RunContext("default", spockUserHome, script, classes);
  }

  private static String env(String systemPropertyKey, String envVariableKey, String defaultValue) {
    String value = System.getProperty(systemPropertyKey);
    if (value != null) return value;

    value = System.getenv(envVariableKey);
    if (value != null) return value;

    return defaultValue;
  }
}
