package jetbrains.mps.checkers;

/*Generated by MPS */

import jetbrains.mps.annotations.GeneratedClass;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.progress.TaskScheduler;
import jetbrains.mps.progress.DefaultTaskScheduler;
import java.util.List;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import jetbrains.mps.internal.collections.runtime.Sequence;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import java.util.ArrayList;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.internal.collections.runtime.CollectionSequence;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.errors.item.IssueKindReportItem;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.util.Consumer;
import org.jetbrains.mps.openapi.util.ProgressMonitor;
import jetbrains.mps.progress.ProgressTask;
import org.jetbrains.mps.openapi.util.SubProgressKind;
import org.jetbrains.annotations.NotNull;
import jetbrains.mps.baseLanguage.closures.runtime._FunctionTypes;
import jetbrains.mps.errors.item.ModelReportItemBase;
import org.jetbrains.mps.openapi.model.SModelReference;
import jetbrains.mps.errors.MessageStatus;
import jetbrains.mps.errors.item.ModuleReportItemBase;
import org.jetbrains.mps.openapi.module.SModuleReference;

/**
 * Builder that is capable to create composite checker out of a collection of "specific" checkers, 
 * which can be any of the subclasses of {@link jetbrains.mps.checkers.IChecker.AbstractChecker }. 
 * The resulting checker is an instance of {@link jetbrains.mps.checkers.IAbstractChecker } and can accept {@link jetbrains.mps.checkers.ModelCheckerBuilder.ItemsToCheck }.
 * 
 */
@GeneratedClass(node = "r:ba41e9c6-15ca-4a47-95f2-6a81c2318547(jetbrains.mps.checkers)/3719390199793466458", model = "r:ba41e9c6-15ca-4a47-95f2-6a81c2318547(jetbrains.mps.checkers)")
public class ModelCheckerBuilder {
  private static final Logger LOG = Logger.getLogger(ModelCheckerBuilder.class);

  private final ModelExtractor myModelExtractor;
  private TaskScheduler myTaskScheduler = new DefaultTaskScheduler();
  public ModelCheckerBuilder(ModelExtractor modelExtractor) {
    myModelExtractor = modelExtractor;
  }
  public ModelCheckerBuilder(boolean checkStubs) {
    this(new ModelsExtractorImpl().includeStubs(checkStubs));
  }

  public static abstract class ModelExtractor {
    public final List<SModel> getModels(SModule module) {
      Iterable<SModel> models = Sequence.fromIterable(getSubModules(module)).translate((m) -> m.getModels());
      return Sequence.fromIterable(models).where((it) -> includeModel(it)).toList();
    }
    public abstract Iterable<SModule> getSubModules(SModule module);
    public abstract boolean includeModel(SModel model);
  }

  public static class ModelsExtractorImpl extends ModelExtractor {
    private boolean myIncludeStubs = false;
    private boolean myIncludeGenerators = true;
    public ModelsExtractorImpl excludeGenerators() {
      myIncludeGenerators = false;
      return this;
    }
    public ModelsExtractorImpl includeStubs(boolean checkStubs) {
      myIncludeStubs = checkStubs;
      return this;
    }
    @Override
    public Iterable<SModule> getSubModules(SModule module) {
      List<SModule> result = ListSequence.fromListAndArray(new ArrayList<SModule>(), module);
      if (myIncludeGenerators && module instanceof Language) {
        ListSequence.fromList(result).addSequence(CollectionSequence.fromCollection(((Language) module).getOwnedGenerators()));
      }
      return result;
    }
    public boolean includeModel(SModel model) {
      return myIncludeStubs || !(SModelStereotype.isStubModel(model));
    }
  }

  /**
   * Implements the "builder" pattern for collecting input data to the composite checker.
   */
  public static class ItemsToCheck {
    public static ItemsToCheck forSingleModule(SModule module) {
      ItemsToCheck result = new ItemsToCheck();
      ListSequence.fromList(result.modules).addElement(module);
      return result;
    }
    public static ItemsToCheck forSingleModel(SModel model) {
      ItemsToCheck result = new ItemsToCheck();
      ListSequence.fromList(result.models).addElement(model);
      return result;
    }
    public List<SModel> models = ListSequence.fromList(new ArrayList<SModel>());
    public List<SModule> modules = ListSequence.fromList(new ArrayList<SModule>());
  }

  public ModelCheckerBuilder withTaskScheduler(TaskScheduler taskExecutor) {
    this.myTaskScheduler = taskExecutor;
    return this;
  }

  /**
   * Creates a composite checker from all "specific" checkers that is intended to process objects 
   * of type {@link jetbrains.mps.checkers.ModelCheckerBuilder.ItemsToCheck }.
   */
  public IAbstractChecker<ItemsToCheck, IssueKindReportItem> createChecker(final List<? extends IChecker<?, ? extends IssueKindReportItem>> specificCheckers) {
    List<IChecker<SModel, ? extends IssueKindReportItem>> modelCheckers = ListSequence.fromList(new ArrayList<IChecker<SModel, ? extends IssueKindReportItem>>());
    List<IChecker<SModule, ? extends IssueKindReportItem>> moduleCheckers = ListSequence.fromList(new ArrayList<IChecker<SModule, ? extends IssueKindReportItem>>());

    for (IChecker<?, ? extends IssueKindReportItem> it : ListSequence.fromList(specificCheckers)) {
      IChecker<?, ? extends IssueKindReportItem> checker = it;
      if (checker instanceof IChecker.AbstractModuleChecker) {
        ListSequence.fromList(moduleCheckers).addElement((IChecker.AbstractModuleChecker<? extends IssueKindReportItem>) checker);
      } else if (checker instanceof IChecker.AbstractModelChecker) {
        ListSequence.fromList(modelCheckers).addElement((IChecker.AbstractModelChecker<?>) checker);
      } else if (checker instanceof IChecker.AbstractRootChecker) {
        ListSequence.fromList(modelCheckers).addElement(((IChecker.AbstractRootChecker<?>) checker).asModelChecker());
      } else if (checker instanceof IChecker.AbstractNodeChecker) {
        ListSequence.fromList(modelCheckers).addElement(((IChecker.AbstractNodeChecker<?>) checker).asModelChecker());
      } else {
        if (LOG.isErrorLevel()) {
          LOG.error("IChecker implementor doesn't extend none of expected base classes: " + checker.getClass().getName());
        }
      }
    }

    return createChecker(modelCheckers, moduleCheckers);
  }

  private IAbstractChecker<ItemsToCheck, IssueKindReportItem> createChecker(final List<IChecker<SModel, ? extends IssueKindReportItem>> specificModelCheckers, final List<IChecker<SModule, ? extends IssueKindReportItem>> specificModuleCheckers) {
    return new IAbstractChecker<ItemsToCheck, IssueKindReportItem>() {
      public void check(ItemsToCheck itemsToCheck, SRepository repository, Consumer<? super IssueKindReportItem> errorCollector, ProgressMonitor monitor) {
        ProgressTask checkTask = checkTask(itemsToCheck, repository, errorCollector);
        myTaskScheduler.schedule(checkTask, monitor).finish();
      }

      @Override
      public ProgressTask checkTask(final ItemsToCheck itemsToCheck, final SRepository repository, final Consumer<? super IssueKindReportItem> errorCollector) {
        List<SModule> modules = ListSequence.fromList(itemsToCheck.modules).translate((it) -> myModelExtractor.getSubModules(it)).toList();
        List<SModel> models = itemsToCheck.models;
        final int work = ListSequence.fromList(models).count() + ListSequence.fromList(modules).count() + ListSequence.fromList(modules).translate((it) -> myModelExtractor.getModels(it)).count();

        ProgressTask.Builder taskBuilder = new ProgressTask.Builder();
        taskBuilder.addTask(new ProgressTask.SimpleTask("checking") {
          @Override
          protected void run() {
            myMonitor.start("Checking", work);
          }
        });

        ProgressTask.Builder builder = new ProgressTask.Builder();
        IAbstractChecker<SModule, ? extends IssueKindReportItem> generalModuleChecker = aggreagateSpecificCheckers(specificModuleCheckers, (SModule m) -> m.getModuleName());
        IAbstractChecker<SModel, ? extends IssueKindReportItem> generalModelChecker = skipNullModules(aggreagateSpecificCheckers(specificModelCheckers, (SModel m) -> m.getName().getLongName()));

        for (SModel model : ListSequence.fromList(models)) {
          builder.addTask(new DelegatingSubtask(generalModelChecker.checkTask(model, repository, errorCollector)) {
            @Override
            public void onThrowable(Throwable t) {
              if (LOG.isErrorLevel()) {
                LOG.error("Failed to check model " + model.getName(), t);
              }
              IssueKindReportItem ri = new ExceptionForModel(model.getReference(), new RuntimeException(t));
              errorCollector.accept(ri);
            }
          });
        }

        for (SModule module : ListSequence.fromList(modules)) {
          builder.addTask(new DelegatingSubtask(generalModuleChecker.checkTask(module, repository, errorCollector)) {
            @Override
            public void onThrowable(Throwable t) {
              if (LOG.isErrorLevel()) {
                LOG.error("Failed to check module " + module.getModuleName(), t);
              }
              IssueKindReportItem ri = new ExceptionForModule(module.getModuleReference(), new RuntimeException(t));
              errorCollector.accept(ri);
            }
          });

          for (SModel model : ListSequence.fromList(myModelExtractor.getModels(module))) {
            builder.addTask(new DelegatingSubtask(generalModelChecker.checkTask(model, repository, errorCollector)) {
              @Override
              public void onThrowable(Throwable t) {
                if (LOG.isErrorLevel()) {
                  LOG.error("Failed to check model " + model.getName(), t);
                }
                IssueKindReportItem ri = new ExceptionForModel(model.getReference(), new RuntimeException(t));
                errorCollector.accept(ri);
              }
            });
          }
        }
        taskBuilder.addTask(builder.asParallel());

        taskBuilder.addTask(new ProgressTask.SimpleTask("checking finished") {
          @Override
          public void onFinished() {
            myMonitor.done();
          }
        });
        return taskBuilder.asSequential();
      }
    };
  }

  private static class DelegatingSubtask extends ProgressTask.DelegatingTask {
    public DelegatingSubtask(ProgressTask delegate) {
      super(delegate);
    }
    @Override
    public void initMonitor(ProgressMonitor parentMonitor) {
      myMonitor = parentMonitor.subTask(1, SubProgressKind.REPLACING);
    }
    @Override
    public boolean isReady() {
      return !(myMonitor.isCanceled());
    }
    @Override
    public void onFinished() {
      myMonitor.done();
    }
  }

  public static IAbstractChecker<SModel, IssueKindReportItem> skipNullModules(IAbstractChecker<SModel, IssueKindReportItem> checker) {
    return new SkippingChecker<SModel, IssueKindReportItem>(checker, (SModel model, SRepository repository) -> {
      SModule module = model.getModule();
      if (module == null) {
        if (LOG.isWarningLevel()) {
          LOG.warning("Module is null for " + model.getName() + " model");
        }
        return false;
      }
      return true;
    });
  }

  /*package*/ static <O> IAbstractChecker<O, IssueKindReportItem> aggreagateSpecificCheckers(@NotNull List<IChecker<O, ? extends IssueKindReportItem>> specificCheckers, _FunctionTypes._return_P1_E0<? extends String, ? super O> getFqName) {
    return new AggregatingChecker<O>(specificCheckers, getFqName);
  }

  private static String asMessage(Exception ex) {
    String m = ex.getMessage();
    if (m != null && !(m.isEmpty())) {
      return m;
    }
    return String.format("Exception %s; check aborted", ex.getClass().getName());
  }

  private static class ExceptionForModel extends ModelReportItemBase {
    /*package*/ ExceptionForModel(SModelReference model, Exception ex) {
      super(MessageStatus.ERROR, model, asMessage(ex));
    }

    @Override
    public IssueKindReportItem.ItemKind getIssueKind() {
      // no idea how to construct ItemKind (and no proper documentation, despite MPS-26043 marked as fixed),
      // just took other similar classes for inspiration
      return IssueKindReportItem.MODEL_PROPERTIES.deriveItemKind("exception");
    }
  }
  private static class ExceptionForModule extends ModuleReportItemBase {
    /*package*/ ExceptionForModule(SModuleReference module, Exception ex) {
      super(MessageStatus.ERROR, module, asMessage(ex));
    }

    @Override
    public IssueKindReportItem.ItemKind getIssueKind() {
      // see CancelForModel#getIssueKind for whine and frustration
      return IssueKindReportItem.MODULE_PROPERTIES.deriveItemKind("exception");
    }
  }
}
