/*
 * Copyright 2003-2016 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.smodel;

import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.DevKit;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager.Deptype;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.util.CollectionUtil;
import jetbrains.mps.util.IterableUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

// todo: review usages and remove
public abstract class DefaultScope extends BaseScope {
  private final Object LOCK = new Object();

  private boolean myInitialized;
  private boolean myInitializationInProgress;

  private Set<SModule> myVisibleModules;
  private Set<Language> myUsedLanguages;
  private Set<DevKit> myUsedDevkits;

  @NotNull
  @Override
  public Iterable<SModule> getModules() {
    Set<SModule> result = new HashSet<SModule>();
    synchronized (LOCK) {
      initialize();
      result.addAll(myVisibleModules);
      result.addAll(myUsedLanguages);
      result.addAll(myUsedDevkits);
    }
    return result;
  }

  @NotNull
  @Override
  public Iterable<SModel> getModels() {
    List<SModel> result = new ArrayList<SModel>();
    synchronized (LOCK) {
      initialize();
      for (SModule module : myVisibleModules) {
        result.addAll(IterableUtil.asCollection(module.getModels()));
      }
      for (Language language : myUsedLanguages) {
        result.addAll(language.getModels()); // todo: ?
        result.addAll(language.getAccessoryModels());
      }
    }
    return result;
  }

  @Override
  public SModule resolve(SModuleReference reference) {
    if (reference == null) {
      return null;
    }

    SModule module = reference.resolve(MPSModuleRepository.getInstance());

    if (module == null) {
      return null;
    }

    synchronized (LOCK) {
      initialize();
      if (myVisibleModules.contains(module) || myUsedLanguages.contains(module) || myUsedDevkits.contains(module)) {
        return module;
      } else {
        return null;
      }
    }
  }

  @Override
  public org.jetbrains.mps.openapi.model.SModel resolve(org.jetbrains.mps.openapi.model.SModelReference reference) {
    if (reference == null) {
      return null;
    }

    SModel model = reference.resolve(MPSModuleRepository.getInstance());

    if (model == null) {
      return null;
    }

    synchronized (LOCK) {
      initialize();

      if (myVisibleModules.contains(model.getModule())) return model;

      for (Language l : myUsedLanguages) {
        if (l.getAccessoryModels().contains(model)) return model;
      }
    }

    return null;
  }

  protected Collection<Language> getInitialUsedLanguages() {
    return CollectionUtil.filter(Language.class, getInitialModules());
  }

  public void invalidateCaches() {
    synchronized (LOCK) {
      myVisibleModules = null;
      myUsedLanguages = null;
      myUsedDevkits = null;
      myInitialized = false;
    }
  }

  private void initialize() {
    synchronized (LOCK) {
      if (myInitialized) return;
      if (myInitializationInProgress) return;

      myInitializationInProgress = true;

      try {
        Set<SModule> initialModules = getInitialModules();
        fillInDevkits(initialModules);
        fillInLanguages();
        fillInVisible(initialModules);
      } finally {
        myInitializationInProgress = false;
      }

      myInitialized = true;
    }
  }

  private void fillInVisible(Set<SModule> initialModules) {
    myVisibleModules = (Set<SModule>) new GlobalModuleDependenciesManager(initialModules).getModules(Deptype.VISIBLE);
  }

  private void fillInLanguages() {
    myUsedLanguages = new HashSet<Language>();
    myUsedLanguages.addAll(getInitialUsedLanguages());
    for (DevKit dk : myUsedDevkits) {
      myUsedLanguages.addAll(dk.getAllExportedLanguages());
    }
    for (Language l : new ArrayList<Language>(myUsedLanguages)) {
      myUsedLanguages.addAll(l.getAllExtendedLanguages());
    }
  }

  private void fillInDevkits(Set<SModule> initialModules) {
    myUsedDevkits = new HashSet<DevKit>();
    for (SModule m : initialModules) {
      if (m instanceof DevKit) {
        DevKit dk = (DevKit) m;
        myUsedDevkits.add(dk);
        myUsedDevkits.addAll(dk.getAllExtendedDevkits());
      }

      ModuleDescriptor moduleDescriptor = ((AbstractModule) m).getModuleDescriptor();
      if (moduleDescriptor != null && moduleDescriptor.getUsedDevkits() != null) {
        for (SModuleReference ref : moduleDescriptor.getUsedDevkits()) {
          DevKit dk = ModuleRepositoryFacade.getInstance().getModule(ref, DevKit.class);
          if (dk == null) continue;
          myUsedDevkits.add(dk);
          myUsedDevkits.addAll(dk.getAllExtendedDevkits());
        }
      }
    }
  }

  protected abstract Set<SModule> getInitialModules();
}
