/*
 * Copyright 2003-2015 JetBrains s.r.o.
 *
 * 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 jetbrains.mps.java.stub;

import jetbrains.mps.extapi.persistence.FolderSetDataSource;
import jetbrains.mps.persistence.java.library.JavaClassStubModelDescriptor;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.ModuleId;
import jetbrains.mps.project.structure.modules.ModuleReference;
import jetbrains.mps.reloading.CommonPaths;
import jetbrains.mps.smodel.SNodeId.Foreign;
import jetbrains.mps.smodel.loading.ModelLoadingState;
import jetbrains.mps.tool.environment.Environment;
import jetbrains.mps.tool.environment.EnvironmentConfig;
import jetbrains.mps.tool.environment.MpsEnvironment;
import jetbrains.mps.vfs.impl.IoFileSystem;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

/**
 * Stub models from classes are loaded lazily. To check there's no
 * race condition when model is loaded, do the stress testing.
 * There were few causes:
 *  - initial model construction set state from NOT_LOADED to INTERFACE_LOADED outside of synchronized code, so condition state == INTERFACE_LOADED in load()
 *    to move to FULLY_LOADED was not triggered - threads might have seen model != null but state still == NOT_LOADED.
 *  - similarly FULLY_LOADED was dispatched outside of model update lock, and the chances were to treat model alrady full with nodes as loaded partially,
 *    so that another thread starts to update it and to remove nodes other thread assumes are in the node map already.
 *
 * Since model under test is not attached to a repository, no model access control is in place.
 * @author Artem Tikhomirov
 */
public class StubModelLazyLoadStressTest {
  private static Environment ourPlatform;
  private static final boolean DEBUG = Boolean.FALSE.booleanValue();

  @BeforeClass
  public static void setUp() {
    ourPlatform = MpsEnvironment.getOrCreate(EnvironmentConfig.defaultConfig());
  }

  @AfterClass
  public static void tearDown() {
    ourPlatform.release();
    ourPlatform = null;
  }

  private static void trace(String message) {
    if (DEBUG) {
      System.out.println(message);
    }
  }

  @Test
  public void testParallelLoad() throws InterruptedException {
    final ModuleReference moduleRef = new ModuleReference("fake", ModuleId.regular());
    SModelReference modelRef = new JavaPackageNameStub("java.util.regex").asModelReference(moduleRef);
    FolderSetDataSource dataSource = new FolderSetDataSource();
    for (String path : CommonPaths.getJDKPath()) {
      if (new File(path).isFile() && path.endsWith(".jar")) {
        path += "!/java/util/regex/";
      }
      dataSource.addPath(IoFileSystem.INSTANCE.getFile(path), null);
    }
    JavaClassStubModelDescriptor model = new JavaClassStubModelDescriptor(modelRef, dataSource) {
      @Override
      protected void fireModelStateChanged(ModelLoadingState oldState, ModelLoadingState newState) {
        Thread.yield();
        try {
          Thread.sleep(10);
        } catch (InterruptedException ex) {
          // ignore
        }
        super.fireModelStateChanged(oldState, newState);
        trace("JCSMD.marked as " + newState + " from " + Thread.currentThread().getName());
      }
    };
    model.setModule(new AbstractModule() {
      {
        setModuleReference(moduleRef);
      }
    });
    SNodeId nodeId = new Foreign("~Pattern.compile(java.lang.String):java.util.regex.Pattern");
    FindNodeRunnable[] runners = new FindNodeRunnable[10];
    LatchCountAction latch = new LatchCountAction(new CountDownLatch(2));
    CyclicBarrier barrier = new CyclicBarrier(runners.length, latch);
    for (int i = 0; i < runners.length; i++) {
      runners[i] = new FindNodeRunnable(model, nodeId);
      if (i+1 == runners.length) {
        trace("about to start latest thread...");
      }
      new Thread(new BarrierRunnable(barrier, runners[i]), "FindNodeThread" + i).start();
    }
    latch.await(10);
    StringBuilder msg = new StringBuilder();
    boolean anyNull = false;
    for (int i = 0; i < runners.length; i++) {
      msg.append("Thread");
      msg.append(i);
      msg.append(':');
      if (runners[i].getNode() == null) {
        anyNull = true;
        msg.append("NULL");
      } else {
        msg.append("OK");
      }
      msg.append(',');
    }
    Assert.assertFalse(msg.toString(), anyNull);
  }

  private static class FindNodeRunnable implements Runnable {
    private final SModel myModel;
    private final SNodeId myNodeId;
    private SNode myNode;

    public FindNodeRunnable(SModel model, SNodeId nodeId) {
      myModel = model;
      myNodeId = nodeId;
    }

    @Override
    public void run() {
      myModel.load(); // let all threads block at load()'s guard
      myNode = myModel.getNode(myNodeId);
      trace(Thread.currentThread().getName() + " is over");
    }

    public SNode getNode() {
      return myNode;
    }
  }

  final static class BarrierRunnable implements Runnable {
    private final CyclicBarrier myBarrier;
    private final Runnable myDelegate;

    public BarrierRunnable(CyclicBarrier barrier, Runnable delegate) {
      myBarrier = barrier;
      myDelegate = delegate;
    }

    @Override
    public void run() {
      try {
        myBarrier.await();
        myDelegate.run();
        myBarrier.await();
      } catch (Exception ex) {
        ex.printStackTrace();
        Assert.fail(ex.getMessage());
      }
    }
  }

  private static class LatchCountAction implements Runnable {
    private final CountDownLatch myLatch;

    public LatchCountAction(CountDownLatch latch) {
      myLatch = latch;
    }
    @Override
    public void run() {
      myLatch.countDown();
    }

    public void await(int seconds) throws InterruptedException {
      myLatch.await(seconds, TimeUnit.SECONDS);
    }
  }
}
