/*
 * Copyright (C) 2006 Google Inc.
 *
 * 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 com.google.inject;

import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertSame;

import java.lang.annotation.Retention;
import java.text.DecimalFormat;
import java.util.concurrent.Callable;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;

/**
 * A semi-useless microbenchmark. Spring and Guice constuct the same object graph a bunch of times,
 * and we see who can construct the most per second. As of this writing Guice is more than 50X
 * faster. Also useful for comparing pure Java configuration options.
 *
 * @author crazybob@google.com (Bob Lee)
 */
public class PerformanceComparison {

  public static void main(String[] args) throws Exception {
    // Once warm up. Takes lazy loading out of the equation and ensures we
    // created the graphs properly.
    validate(springFactory);
    validate(juiceFactory);
    validate(byHandFactory);

    for (int i2 = 0; i2 < 10; i2++) {
      iterate(springFactory, "Spring:  ");
      iterate(juiceFactory, "Guice:   ");
      iterate(byHandFactory, "By Hand: ");

      System.err.println();
    }

    System.err.println("Concurrent:");

    for (int i2 = 0; i2 < 10; i2++) {
      concurrentlyIterate(springFactory, "Spring:  ");
      concurrentlyIterate(juiceFactory, "Guice:   ");
      concurrentlyIterate(byHandFactory, "By Hand: ");

      System.err.println();
    }
  }

  static final Callable<Foo> springFactory =
      new Callable<Foo>() {

        final DefaultListableBeanFactory beanFactory;

        {
          beanFactory = new DefaultListableBeanFactory();

          RootBeanDefinition tee = new RootBeanDefinition(TeeImpl.class, true);
          tee.setLazyInit(true);
          ConstructorArgumentValues teeValues = new ConstructorArgumentValues();
          teeValues.addGenericArgumentValue("test");
          tee.setConstructorArgumentValues(teeValues);

          RootBeanDefinition bar = new RootBeanDefinition(BarImpl.class, false);
          ConstructorArgumentValues barValues = new ConstructorArgumentValues();
          barValues.addGenericArgumentValue(new RuntimeBeanReference("tee"));
          barValues.addGenericArgumentValue(5);
          bar.setConstructorArgumentValues(barValues);

          RootBeanDefinition foo = new RootBeanDefinition(Foo.class, false);
          MutablePropertyValues fooValues = new MutablePropertyValues();
          fooValues.addPropertyValue("i", 5);
          fooValues.addPropertyValue("bar", new RuntimeBeanReference("bar"));
          fooValues.addPropertyValue("copy", new RuntimeBeanReference("bar"));
          fooValues.addPropertyValue("s", "test");
          foo.setPropertyValues(fooValues);

          beanFactory.registerBeanDefinition("foo", foo);
          beanFactory.registerBeanDefinition("bar", bar);
          beanFactory.registerBeanDefinition("tee", tee);
        }

        @Override
        public Foo call() throws Exception {
          return (Foo) beanFactory.getBean("foo");
        }
      };

  static final Callable<Foo> juiceFactory =
      new Callable<Foo>() {
        final Provider<Foo> fooProvider;

        {
          Injector injector;
          try {
            injector =
                Guice.createInjector(
                    new AbstractModule() {
                      @Override
                      protected void configure() {
                        bind(Tee.class).to(TeeImpl.class);
                        bind(Bar.class).to(BarImpl.class);
                        bind(Foo.class);
                        bindConstant().annotatedWith(I.class).to(5);
                        bindConstant().annotatedWith(S.class).to("test");
                      }
                    });
          } catch (CreationException e) {
            throw new RuntimeException(e);
          }
          fooProvider = injector.getProvider(Foo.class);
        }

        @Override
        public Foo call() throws Exception {
          return fooProvider.get();
        }
      };

  static final Callable<Foo> byHandFactory =
      new Callable<Foo>() {
        final Tee tee = new TeeImpl("test");

        @Override
        public Foo call() throws Exception {
          Foo foo = new Foo();
          foo.setI(5);
          foo.setS("test");
          Bar bar = new BarImpl(tee, 5);
          Bar copy = new BarImpl(tee, 5);
          foo.setBar(bar);
          foo.setCopy(copy);
          return foo;
        }
      };

  static void validate(Callable<Foo> t) throws Exception {
    Foo foo = t.call();
    assertEquals(5, foo.i);
    assertEquals("test", foo.s);
    assertSame(foo.bar.getTee(), foo.copy.getTee());
    assertEquals(5, foo.bar.getI());
    assertEquals("test", foo.bar.getTee().getS());
  }

  static final DecimalFormat format = new DecimalFormat();

  static void iterate(Callable<Foo> callable, String label) {
    int count = 100000;

    long time = System.currentTimeMillis();

    for (int i = 0; i < count; i++) {
      try {
        callable.call();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    time = System.currentTimeMillis() - time;

    System.err.println(label + format.format(count * 1000 / time) + " creations/s");
  }

  static void concurrentlyIterate(final Callable<Foo> callable, String label) {
    int threadCount = 10;
    final int count = 10000;

    Thread[] threads = new Thread[threadCount];

    for (int i = 0; i < threadCount; i++) {
      threads[i] =
          new Thread() {
            @Override
            public void run() {
              for (int i = 0; i < count; i++) {
                try {
                  validate(callable);
                } catch (Exception e) {
                  throw new RuntimeException(e);
                }
              }
            }
          };
    }

    long time = System.currentTimeMillis();

    for (int i = 0; i < threadCount; i++) {
      threads[i].start();
    }

    for (int i = 0; i < threadCount; i++) {
      try {
        threads[i].join();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }

    time = System.currentTimeMillis() - time;

    System.err.println(label + format.format(count * 1000 / time) + " creations/s");
  }

  public static class Foo {

    Bar bar;
    Bar copy;
    String s;
    int i;

    @Inject
    public void setI(@I int i) {
      this.i = i;
    }

    @Inject
    public void setBar(Bar bar) {
      this.bar = bar;
    }

    @Inject
    public void setCopy(Bar copy) {
      this.copy = copy;
    }

    @Inject
    public void setS(@S String s) {
      this.s = s;
    }
  }

  interface Bar {

    Tee getTee();

    int getI();
  }

  public static class BarImpl implements Bar {

    final int i;
    final Tee tee;

    @Inject
    public BarImpl(Tee tee, @I int i) {
      this.tee = tee;
      this.i = i;
    }

    @Override
    public Tee getTee() {
      return tee;
    }

    @Override
    public int getI() {
      return i;
    }
  }

  interface Tee {

    String getS();
  }

  @Singleton
  public static class TeeImpl implements Tee {

    final String s;

    @Inject
    public TeeImpl(@S String s) {
      this.s = s;
    }

    @Override
    public String getS() {
      return s;
    }
  }

  @Retention(RUNTIME)
  @BindingAnnotation
  @interface I {}

  @Retention(RUNTIME)
  @BindingAnnotation
  @interface S {}
}
