/*
 * Copyright 2003-2017 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.generator.impl.plan;

import jetbrains.mps.PlatformMpsTest;
import jetbrains.mps.generator.GenerationFacade;
import jetbrains.mps.generator.GenerationOptions;
import jetbrains.mps.generator.GenerationOptions.OptionsBuilder;
import jetbrains.mps.generator.GenerationStatus;
import jetbrains.mps.generator.ModelGenerationPlan;
import jetbrains.mps.generator.ModelGenerationPlan.Checkpoint;
import jetbrains.mps.generator.ModelGenerationPlan.Transform;
import jetbrains.mps.generator.RigidGenerationPlan;
import jetbrains.mps.generator.TransientModelsProvider;
import jetbrains.mps.generator.impl.GenPlanTranslator;
import jetbrains.mps.generator.impl.ModelStreamProviderImpl;
import jetbrains.mps.generator.plan.CheckpointIdentity;
import jetbrains.mps.generator.plan.PlanIdentity;
import jetbrains.mps.generator.runtime.TemplateMappingConfiguration;
import jetbrains.mps.generator.runtime.TemplateModel;
import jetbrains.mps.generator.runtime.TemplateModule;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.messages.LogHandler;
import jetbrains.mps.progress.EmptyProgressMonitor;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory;
import jetbrains.mps.smodel.language.GeneratorRuntime;
import jetbrains.mps.smodel.language.LanguageRegistry;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.PathManager;
import org.apache.log4j.Logger;
import org.hamcrest.CoreMatchers;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author Artem Tikhomirov
 */
public class CheckpointModelTest extends PlatformMpsTest {
  private static Project mpsProject;

  @Rule
  public final ErrorCollector myErrors = new ErrorCollector();

  private final IMessageHandler myGeneratorMessages = new LogHandler(Logger.getLogger(CheckpointModelTest.class));

  @BeforeClass
  public static void setup() {
    mpsProject = ENV.openProject(new File(PathManager.getUserDir(), "languages/languageDesign/generator/project.xmodel.test1"));
  }

  @AfterClass
  public static void tearDown() {
    mpsProject.dispose();
  }

  /**
   * beanmodel1.mps is transformed Bean --> Class with a single checkpoint.
   * This test ensures there's checkpoint model and appropriate mapping label recorded.
   */
  @Test
  public void createModelWithOneCheckpoint() {
    final SModelReference mr = PersistenceFacade.getInstance().createModelReference(
        "r:24638668-c917-4da1-8069-8ddef862314d(jetbrains.mps.generator.crossmodel.sandbox.beanmodel1)");
    // "r:53fbbbd7-a01f-458c-a76d-a34ed2d6f25f(jetbrains.mps.generator.crossmodel.sandbox.beanmodel2)"
    final SModel m = resolve(mr);
    final PlanIdentity planIdentity = new PlanIdentity("test Plan");
    final Checkpoint cp1 = new Checkpoint(new CheckpointIdentity(planIdentity, "aaa"));
    ModelGenerationPlan plan = new ModelAccessHelper(mpsProject.getModelAccess()).runReadAction(new Computable<ModelGenerationPlan>() {
      @Override
      public ModelGenerationPlan compute() {
        final Transform step1 = new Transform(getCrossmodelPropertyGenerators());
        final Transform step2 = new Transform(getBaseLanguageGenerators());
        return new RigidGenerationPlan(planIdentity, step1, cp1, step2);
      }
    });
    GenerationOptions opt = GenerationOptions.getDefaults().customPlan(m, plan).create();
    final TransientModelsProvider tmProvider = mpsProject.getComponent(TransientModelsProvider.class);
    // need write for process(x, model) to construct transient module, and need model read to run transformation
    GenerationStatus genStatus = new ModelAccessHelper(mpsProject.getRepository()).runWriteAction(() -> {
      GenerationFacade genFacade = new GenerationFacade(mpsProject.getRepository(), opt).transients(tmProvider).messages(myGeneratorMessages);
      tmProvider.initCheckpointModule();
      GenerationStatus rv = genFacade.process(new EmptyProgressMonitor(), m);
      tmProvider.publishAll();
      return rv;
    });
    myErrors.checkThat("Generation succeeds", genStatus.isOk(), CoreMatchers.equalTo(true));
    // Now I can access CME from GenerationStatus, but keep new CME to verify the environment is capable
    // to get populated from transient models. In fact, GenerationStatus shall give access to smth directed
    // more towards serialization of changed CP models, rather than generic CME.
    CrossModelEnvironment cme = new CrossModelEnvironment(tmProvider, new ModelStreamProviderImpl());
    // XXX shall it be CME to give access to module with checkpoint models? Is there better way to find out cpModel?

    SModule checkpointModule = tmProvider.getCheckpointsModule();
    final SModelName cpModelName = CrossModelEnvironment.createCheckpointModelName(m.getReference(), cp1.getIdentity());
    SModel cpModel = null;
    for (SModel trm : checkpointModule.getModels()) {
      if (cpModelName.equals(trm.getName())) {
        cpModel = trm;
        break;
      }
    }
    myErrors.checkThat("Checkpoint model", cpModel, CoreMatchers.notNullValue());
    ModelCheckpoints modelCheckpoints = cme.getState(m);
    myErrors.checkThat("CrossModelEnvironment: state present", modelCheckpoints, CoreMatchers.notNullValue());
    CheckpointState cpState = modelCheckpoints.find(cp1);
    myErrors.checkThat("CheckpointState present", cpState, CoreMatchers.notNullValue());
    if (cpState != null) {
      Collection<String> mappingLabels = cpState.getMappingLabels();
      myErrors.checkThat("GetterMethod label present", mappingLabels.contains("GetterMethod"), CoreMatchers.equalTo(true));
    }
  }

  /**
   * entity1.mps is transformed with two generators, Entity --> Bean --> Class. There are two checkpoints, for Beans and for Classes models.
   * Here we ensure there are mapping labels in both checkpoints, and that output discovered with the first label matches input of a label from second CP.
   */
  @Test
  public void createModelWithTwoCheckpoints() {
    final SModelReference mr = PersistenceFacade.getInstance().createModelReference(
        "r:05c2f926-57b0-4b6d-930c-1aabb187694d(jetbrains.mps.generator.crossmodel.sandbox.entrymodel1)");
    final SModel m = resolve(mr);
    final PlanIdentity planIdentity = new PlanIdentity(" test plan #2");
    final Checkpoint cp1 = new Checkpoint(new CheckpointIdentity(planIdentity, "aaa"));
    final Checkpoint cp2 = new Checkpoint(new CheckpointIdentity(planIdentity, "bbb"));
    ModelGenerationPlan plan = new ModelAccessHelper(mpsProject.getModelAccess()).runReadAction(new Computable<ModelGenerationPlan>() {
      @Override
      public ModelGenerationPlan compute() {
        final Transform step1 = new Transform(getCrossmodelEntityGenerators());
        final Transform step2 = new Transform(getCrossmodelPropertyGenerators());
        final Transform step3 = new Transform(getBaseLanguageGenerators());
        return new RigidGenerationPlan(planIdentity, step1, cp1, step2, cp2, step3);
      }
    });
    GenerationOptions opt = GenerationOptions.getDefaults().customPlan(m, plan).create();
    final TransientModelsProvider tmProvider = mpsProject.getComponent(TransientModelsProvider.class);
    // write lock - see above #createModelWithOneCheckpoint
    GenerationStatus genStatus = new ModelAccessHelper(mpsProject.getRepository()).runWriteAction(() -> {
      GenerationFacade genFacade = new GenerationFacade(mpsProject.getRepository(), opt).transients(tmProvider).messages(myGeneratorMessages);
      tmProvider.initCheckpointModule();
      GenerationStatus rv = genFacade.process(new EmptyProgressMonitor(), m);
      tmProvider.publishAll();
      return rv;
    });
    myErrors.checkThat("Generation succeeds", genStatus.isOk(), CoreMatchers.equalTo(true));
    CrossModelEnvironment cme = new CrossModelEnvironment(tmProvider, new ModelStreamProviderImpl());
    ModelCheckpoints modelCheckpoints = cme.getState(m);
    boolean crossModelCheckpointsPresent = modelCheckpoints != null;
    myErrors.checkThat("CrossModelEnvironment: state present", crossModelCheckpointsPresent, CoreMatchers.equalTo(true));
    if (!crossModelCheckpointsPresent) {
      return;
    }
    final CheckpointState cp1State = modelCheckpoints.find(cp1);
    final CheckpointState cp2State = modelCheckpoints.find(cp2);
    myErrors.checkThat("state for the first checkpoint present", cp1State, CoreMatchers.notNullValue());
    myErrors.checkThat("state for the second checkpoint present", cp2State, CoreMatchers.notNullValue());
    if (cp1State == null || cp2State == null) {
      return;
    }
    final String ml1 = "EntryOne2Property";
    final boolean ml1Present = cp1State.getMappingLabels().contains(ml1);
    final String ml2 = "GetterMethod";
    final boolean ml2Present = cp2State.getMappingLabels().contains(ml2);
    myErrors.checkThat("Entry -> BeanProperty label present", ml1Present, CoreMatchers.equalTo(true));
    myErrors.checkThat("BeanProperty -> InstanceMethodDeclaration label present", ml2Present, CoreMatchers.equalTo(true));
    if (ml1Present && ml2Present) {
      mpsProject.getModelAccess().runReadAction(new Runnable() {
        @Override
        public void run() {
          Collection<SNodeId> entryOneInputs = cp1State.getInputs(ml1);
          myErrors.checkThat("There were two Entry(kind:ONE)", entryOneInputs.size(), CoreMatchers.equalTo(2));
          for (SNodeId in : entryOneInputs) {
            SNode originalInput = m.getNode(in);
            myErrors.checkThat("Original model doesn't contain Entry we've got label recorded for", originalInput, CoreMatchers.notNullValue());
            if (originalInput == null) {
              continue;
            }
            Collection<SNode> outputAtCheckpoint1 = cp1State.getOutput(ml1, originalInput);
            myErrors.checkThat("Output at first checkpoint", outputAtCheckpoint1.isEmpty(), CoreMatchers.equalTo(false));
            for (SNode cp1Out : outputAtCheckpoint1) {
              Collection<SNode> outputAtCheckpoint2 = cp2State.getOutput(ml2, cp1Out);
              myErrors.checkThat("Output at second checkpoint", outputAtCheckpoint2.isEmpty(), CoreMatchers.equalTo(false));
            }
          }
        }
      });
    }
  }

  @Test
  public void testTwoModelsIndividually() {
    final SModelReference mr1 = PersistenceFacade.getInstance().createModelReference("r:a2bc1c51-b81b-4f90-a208-04e6bd08c9c2(jetbrains.mps.generator.xmodel.test.m1)");
    final SModelReference mr2 = PersistenceFacade.getInstance().createModelReference("r:1ae0d5a3-32c6-406d-9a53-f40b122309f5(jetbrains.mps.generator.xmodel.test.m2)");
    GenerationStatus[] genStatus = new GenerationStatus[2];
    mpsProject.getModelAccess().runWriteAction(() -> {
      SLanguage lang1 = MetaAdapterFactory.getLanguage(0xb2d9d19b9a4747a4L, 0x93f40c9390001bf2L, "jetbrains.mps.generator.test.xmodel.lang1");
      final Transform step1 = new Transform(getGenerators(lang1));
      final Transform step2 = new Transform(getBaseLanguageGenerators());
      final PlanIdentity planIdentity = new PlanIdentity("test.plan.3");
      final Checkpoint cp1 = new Checkpoint(new CheckpointIdentity(planIdentity, "aaa"));
      ModelGenerationPlan plan =  new RigidGenerationPlan(planIdentity, step1, cp1, step2);
      SModel m1 = resolve(mr1);
      OptionsBuilder optBuilder = GenerationOptions.getDefaults();
      GenerationOptions opt = optBuilder.customPlan(m1, plan).create();
      final TransientModelsProvider tmProvider = mpsProject.getComponent(TransientModelsProvider.class);
      GenerationFacade genFacade = new GenerationFacade(mpsProject.getRepository(), opt).transients(tmProvider).messages(myGeneratorMessages);
      tmProvider.initCheckpointModule();
      genStatus[0] = genFacade.process(new EmptyProgressMonitor(), m1);
      tmProvider.publishAll();
      SModel m2 = resolve(mr2);
      // although could, don't want to put plan for m2 right along with plan for m1, want to have them separate
      opt = optBuilder.customPlan(m2, plan).create();
      genFacade = new GenerationFacade(mpsProject.getRepository(), opt).transients(tmProvider).messages(myGeneratorMessages);
      tmProvider.initCheckpointModule();
      genStatus[1] = genFacade.process(new EmptyProgressMonitor(), m2);
      tmProvider.publishAll();
    });
    myErrors.checkThat("m1 generation succeeds", genStatus[0].isOk(), CoreMatchers.equalTo(true));
    myErrors.checkThat("m2 generation succeeds", genStatus[1].isOk(), CoreMatchers.equalTo(true));
  }

  @Test
  public void testPlanTransformer() {
    final SModelReference planModelRef = PersistenceFacade.getInstance().createModelReference("r:85a0bc80-fc68-485e-a9a1-926c3cc284af(jetbrains.mps.generator.xmodel.test.plan1@genplan)");
    ModelGenerationPlan plan = new ModelAccessHelper(mpsProject.getModelAccess()).runReadAction(new Computable<ModelGenerationPlan>() {
      @Override
      public ModelGenerationPlan compute() {
        SModel planModel = resolve(planModelRef);
        RigidPlanBuilder planBuilder = new RigidPlanBuilder(LanguageRegistry.getInstance(mpsProject.getRepository()));
        GenPlanTranslator planTranslator = new GenPlanTranslator(planModel.getRootNodes().iterator().next());
        planTranslator.feed(planBuilder);
        return planBuilder.wrapUp(planTranslator.getPlanIdentity());
      }
    });
    Assert.assertNotNull(plan);
    Assert.assertEquals(3, plan.getSteps().size());
    myErrors.checkThat(plan.getSteps().get(0), CoreMatchers.instanceOf(Transform.class));
    myErrors.checkThat(plan.getSteps().get(1), CoreMatchers.instanceOf(Checkpoint.class));
    myErrors.checkThat(plan.getSteps().get(2), CoreMatchers.instanceOf(Transform.class));
    Transform s1 = (Transform) plan.getSteps().get(0);
    Checkpoint s2 = (Checkpoint) plan.getSteps().get(1);
    Transform s3 = (Transform) plan.getSteps().get(2);
    myErrors.checkThat(s1.getTransformations().isEmpty(), CoreMatchers.equalTo(false));
    myErrors.checkThat(s3.getTransformations().isEmpty(), CoreMatchers.equalTo(false));
    myErrors.checkThat(s2.getName(), CoreMatchers.equalTo("first"));
    // XXX mgp.getIdentity, provided there's CP.getIdentity()?
    myErrors.checkThat(s2.getIdentity().getPlan().getPersistenceValue(), CoreMatchers.equalTo("plan_a"));
  }

  @Test
  public void testPlanBuilder() {
//    final SModuleReference smodelGenerator = PersistenceFacade.getInstance().createModuleReference("2bdcefec-ba49-4b32-ab50-ebc7a41d5090()");
    final SModuleReference collectionsGenerator = PersistenceFacade.getInstance().createModuleReference("5f9babc9-8d5d-4825-8e61-17b241ee6272()");
    final SModuleReference closuresGenerator = PersistenceFacade.getInstance().createModuleReference("857d0a79-6f44-4f46-84ed-9c5b42632011()");
    final SModuleReference blInternalGenerator = PersistenceFacade.getInstance().createModuleReference("46ef3033-ce72-4166-b19e-6ceed23b6844()");
    final SModuleReference baselangGenerator = PersistenceFacade.getInstance().createModuleReference("985c8c6a-64b4-486d-a91e-7d4112742556()");
    final PlanIdentity pi1 = new PlanIdentity("p1");
    final PlanIdentity pi2 = new PlanIdentity("p2");
    final CheckpointIdentity cp1 = new CheckpointIdentity(pi1, "cp1");
    final CheckpointIdentity cp2 = new CheckpointIdentity(pi2, "cp2");

    ModelGenerationPlan plan = new ModelAccessHelper(mpsProject.getModelAccess()).runReadAction(new Computable<ModelGenerationPlan>() {
      @Override
      public ModelGenerationPlan compute() {
        Generator closuresGeneratorInstance = (Generator) closuresGenerator.resolve(mpsProject.getRepository());
        Generator collectionsGeneratorInstance = (Generator) collectionsGenerator.resolve(mpsProject.getRepository());
        Generator blInternalGeneratorInstance = (Generator) blInternalGenerator.resolve(mpsProject.getRepository());
        Generator baselangGeneratorInstance = (Generator) baselangGenerator.resolve(mpsProject.getRepository());
        final LanguageRegistry languageRegistry = LanguageRegistry.getInstance(mpsProject.getRepository());
        Collection<TemplateModule> engagedGenerators = new ArrayList<>();
        engagedGenerators.add(((TemplateModule) languageRegistry.getGenerator(closuresGeneratorInstance)));
        engagedGenerators.add(((TemplateModule) languageRegistry.getGenerator(collectionsGeneratorInstance)));
        engagedGenerators.add(((TemplateModule) languageRegistry.getGenerator(baselangGeneratorInstance)));
        engagedGenerators.add(((TemplateModule) languageRegistry.getGenerator(blInternalGeneratorInstance)));
        RegularPlanBuilder planBuilder = new RegularPlanBuilder(languageRegistry, engagedGenerators);
        planBuilder.recordCheckpoint(cp1);
        planBuilder.applyGeneratorWithExtended(closuresGeneratorInstance);
        planBuilder.synchronizeWithCheckpoint(cp2);
        planBuilder.applyGeneratorWithExtended(blInternalGeneratorInstance);
        planBuilder.applyGeneratorWithExtended(baselangGeneratorInstance);
        return planBuilder.wrapUp(pi1);
      }
    });
    Assert.assertNotNull(plan);
    Assert.assertEquals(5, plan.getSteps().size());
    myErrors.checkThat(plan.getSteps().get(0), CoreMatchers.instanceOf(Checkpoint.class));
    myErrors.checkThat(plan.getSteps().get(1), CoreMatchers.instanceOf(Transform.class));
    myErrors.checkThat(plan.getSteps().get(2), CoreMatchers.instanceOf(Checkpoint.class));
    myErrors.checkThat(plan.getSteps().get(3), CoreMatchers.instanceOf(Transform.class));
    myErrors.checkThat(plan.getSteps().get(4), CoreMatchers.instanceOf(Transform.class));
    //
    Checkpoint p1 = (Checkpoint) plan.getSteps().get(0);
    Checkpoint p2 = (Checkpoint) plan.getSteps().get(2);
    myErrors.checkThat(p1.isPersisted(), CoreMatchers.equalTo(true));
    myErrors.checkThat(p2.isPersisted(), CoreMatchers.equalTo(false));
    Transform t1 = (Transform) plan.getSteps().get(1); // closures + extensions
    Transform t2 = (Transform) plan.getSteps().get(3); // blInternal + extensions
    Transform t3 = (Transform) plan.getSteps().get(4); // bl + extensions
    myErrors.checkThat(t1.getTransformations().size(), CoreMatchers.equalTo(7)); // 2 from closures + 5 from collections
    myErrors.checkThat(t2.getTransformations().size(), CoreMatchers.equalTo(1)); // 1 MC in blInternal.
    myErrors.checkThat(t3.getTransformations().size(), CoreMatchers.equalTo(8)); // 3 from BL + 5 from collections
  }

  // utility to obtain generators of j.m.g.test.crossmodel.property language
  private static List<TemplateMappingConfiguration> getCrossmodelPropertyGenerators() {
    return getGenerators(MetaAdapterFactory.getLanguage(0xdc1cc9486f434687L, 0x90cb17dd5cb27219L, "jetbrains.mps.generator.test.crossmodel.property"));
  }

  // utility to obtain generators of j.m.g.test.crossmodel.entity language
  private static List<TemplateMappingConfiguration> getCrossmodelEntityGenerators() {
    return getGenerators(MetaAdapterFactory.getLanguage(0x4d14758c3ecb486dL, 0xb8c8ea5beb8ae408L, "jetbrains.mps.generator.test.crossmodel.entity"));
  }

  private static List<TemplateMappingConfiguration> getBaseLanguageGenerators() {
    return getGenerators(MetaAdapterFactory.getLanguage(0xf3061a5392264cc5L, 0xa443f952ceaf5816L, "jetbrains.mps.baseLanguage"));
  }


  private static List<TemplateMappingConfiguration> getGenerators(SLanguage language) {
    final LanguageRegistry lr = LanguageRegistry.getInstance(mpsProject.getRepository());
    final GeneratorRuntime g1 = lr.getLanguage(language).getGenerators().iterator().next();
    return getGenerators(g1);

  }

  private static List<TemplateMappingConfiguration> getGenerators(GeneratorRuntime gr) {
    ArrayList<TemplateMappingConfiguration> rv = new ArrayList<TemplateMappingConfiguration>();
    if (gr instanceof TemplateModule) {
      for (TemplateModel tm : ((TemplateModule) gr).getModels()) {
        rv.addAll(tm.getConfigurations());
      }
    }
    return rv;
  }

  private SModel resolve(final SModelReference mr) {
    Computable<SModel> c = new Computable<SModel>() {
      @Override
      public SModel compute() {
        return mr.resolve(mpsProject.getRepository());
      }
    };
    ModelAccess ma = mpsProject.getModelAccess();
    if (ma.canRead()) {
      return c.compute();
    }
    return new ModelAccessHelper(ma).runReadAction(c);
  }
}
