/*
 * 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.project.structure;

import jetbrains.mps.extapi.model.GeneratableSModel;
import jetbrains.mps.generator.ModelDigestUtil;
import jetbrains.mps.project.persistence.GeneratorDescriptorPersistence;
import jetbrains.mps.smodel.BootstrapLanguages;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.SModelId.IntegerSModelId;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.smodel.SnapshotModelData;
import jetbrains.mps.smodel.TrivialModelDescriptor;
import jetbrains.mps.util.JDOMUtil;
import jetbrains.mps.util.MacrosFactory;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelId;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.module.SModule;

import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Contribute @descriptor model to Generator module.
 *
 * IMPLEMENTATION NOTE: Unlike {@link LanguageDescriptorModelProvider}, deliberately doesn't react to model changes now (module's descriptor model
 * is refreshed on model save only), waiting for {@link org.jetbrains.mps.openapi.model.SModelListener} being capable of generic modelChanged event
 * (don't want to listen to distinct node changes)
 *
 * @author Artem Tikhomirov
 * @since 3.4
 */
public class GeneratorDescriptorModelProvider extends DescriptorModelProvider {
  private final SModelId myDescriptorModelId = new IntegerSModelId(0x0f020202);

  private final Map<SModelReference, GeneratorDescriptorModel> myModels = Collections.synchronizedMap(new HashMap<SModelReference, GeneratorDescriptorModel>());


  @Override
  public boolean isApplicable(SModule module) {
    return module instanceof Generator && !module.isPackaged();
  }

  @Override
  public void refreshModule(SModule module) {
    SModelReference modelReference = getModelReference(module);
    GeneratorDescriptorModel dm = myModels.get(modelReference);
    if (dm != null) {
      dm.invalidate();
    } else {
      Generator generator = (Generator) module;
      dm = new GeneratorDescriptorModel(modelReference, generator);
      dm.addEngagedOnGenerationLanguage(BootstrapLanguages.getLanguageDescriptorLang());
      myModels.put(modelReference, dm);
      generator.registerModel(dm);
    }
  }

  @Override
  public void forgetModule(SModule module) {
    SModelReference modelReference = getModelReference(module);
    // generator.unregisterModel below triggers module changed (to be precise, modelRemoved) event, and we may
    // get into #refreshModule() above again, hence it's safe not to remove entry from the map unless all events have been sent
    // (even provided we do our best in DescriptorModelComponent not to send refresh for added/removed descriptor models)
    GeneratorDescriptorModel dm = myModels.get(modelReference);
    if (dm != null) {
      Generator generator = (Generator) module;
      assert dm.getModule() == generator;
      generator.unregisterModel(dm);
      myModels.remove(modelReference);
    }
  }

  @Override
  public void dispose() {
    ArrayList<GeneratorDescriptorModel> models = new ArrayList<GeneratorDescriptorModel>(myModels.values());
    myModels.clear();
    for (GeneratorDescriptorModel m : models) {
      SModule module = m.getModule();
      if (module != null && module instanceof Generator) {
        ((Generator) module).unregisterModel(m);
      }
    }
  }

  private SModelReference getModelReference(SModule module) {
    // Would like to keep name of the model identical to that of language descriptor, to keep qualified name of the Generator RT class the same it was before.
    // Once the name of the activator class is serialized in module.xml (or otherwise part of module descriptor), could change it as see fit.
    // Could have cast module to generator, get source language module, and use it. Don' want to cast, though, rather assume name of generator module is
    // identical to that of source language (up to # sign)
    String moduleName = module.getModuleName();
    int sharpIndex = moduleName.indexOf('#');
    if (sharpIndex != -1) {
      moduleName = moduleName.substring(0, sharpIndex);
    }
    return new jetbrains.mps.smodel.SModelReference(module.getModuleReference(), myDescriptorModelId, new SModelName(moduleName, SModelStereotype.DESCRIPTOR));
  }

  static class GeneratorDescriptorModel extends TrivialModelDescriptor implements GeneratableSModel {
    private final Generator myModule;
    private String myHash;

    GeneratorDescriptorModel(SModelReference modelReference, Generator module) {
      super(new SnapshotModelData(modelReference));
      myModule = module;
    }

    void invalidate() {
      myHash = null;
      // XXX shall fire changed event?
    }

    @Override
    public boolean isGeneratable() {
      return !myModule.isReadOnly() && myModule.generateTemplates(); // FIXME WORK IN PROGRESS, remove once templates are ready.
//      return !myModule.isReadOnly();
    }

    @Override
    public boolean isGenerateIntoModelFolder() {
      return false;
    }

    @Override
    public void setGenerateIntoModelFolder(boolean value) {
      throw new UnsupportedOperationException();
    }

    @Override
    public String getModelHash() {
      String hash = myHash;
      if (hash != null) {
        return hash;
      }
      Element element = new Element("gd");
      // FIXME can't use myModule for MacrosFactory - there's no file in generator's descriptor, hence use one of the source language.
      // Though once generator modules are standalone there's file, guess, the right way is to tolerate modules without file, and to supply
      // e.g. MacrosFactory.getGlobal() instead of null.
      GeneratorDescriptorPersistence.saveGeneratorDescriptor(element, myModule.getModuleDescriptor(), MacrosFactory.forModule(myModule.getSourceLanguage()));
      StringWriter out = new StringWriter();
      try {
        JDOMUtil.writeDocument(new Document(element), out);
      } catch (IOException ex) {
        Logger.getLogger(getClass()).warn(ex.getMessage(), ex);
      }
      hash = ModelDigestUtil.hashText(out.toString());
      BigInteger modelHash = new BigInteger(hash, Character.MAX_RADIX);
      for (SModel m : myModule.getModels()) {
        if (m instanceof GeneratableSModel && !SModelStereotype.isDescriptorModel(m)) {
          modelHash = modelHash.xor(new BigInteger(((GeneratableSModel) m).getModelHash(), Character.MAX_RADIX));
        }
      }
      myHash = hash = modelHash.toString(Character.MAX_RADIX);
      return hash;
    }

    @Override
    public Map<String, String> getGenerationHashes() {
      return Collections.singletonMap(GeneratableSModel.FILE, getModelHash());
    }

    @Override
    public void setDoNotGenerate(boolean value) {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean isDoNotGenerate() {
      return false;
    }
  }
}
