/*
 * Copyright 2003-2014 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.classloading;

import jetbrains.mps.module.ReloadableModuleBase;
import jetbrains.mps.module.ReloadableModuleBase.SModuleDependenciesListener;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import org.jetbrains.mps.openapi.module.SRepositoryListener;
import org.jetbrains.mps.openapi.module.event.SModuleAddedEvent;
import org.jetbrains.mps.openapi.module.event.SModuleChangedEvent;
import org.jetbrains.mps.openapi.module.event.SModuleRemovedEvent;
import org.jetbrains.mps.openapi.module.event.SRepositoryEvent;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * This class transforms non-batched events to batch events
 * It listens to usual SRepositoryListener events and groups them into SRepositoryEvents
 * not thread-safe
 */
public class BatchEventsProcessor {
  private static Logger LOG = LogManager.getLogger(BatchEventsProcessor.class);
  private volatile boolean myBatchStarted = false;

  private static final Object LOCK = new Object();

  private final List<SRepositoryEvent> myEvents = new ArrayList<SRepositoryEvent>();

  private final SRepositoryListener myRepositoryListener = new MySRepositoryListener();

  private final SRepository myRepository;

  public BatchEventsProcessor(SRepository repository) {
    myRepository = repository;
  }

  public void startBatching() {
    if (myBatchStarted) {
      myBatchStarted = false;
      myEvents.clear();
      throw new IllegalStateException("Batching has been already started; Clearing the queue...");
    }
    if (!myEvents.isEmpty()) {
      LOG.warn("Events have not been flushed");
    }
    myBatchStarted = true;
  }

  /**
   * flushes all accumulated events
   * stops listening to SRepository, if no new events are discovered
   * @return result of batching: a list of SRepositoryEvents
   */
  public List<SRepositoryEvent> flush() {
    synchronized (LOCK) {
      if (!myBatchStarted) {
        return Collections.emptyList();
      }
      List<SRepositoryEvent> result = new ArrayList<>(myEvents);
      myEvents.clear();
      return result;
    }
  }

  public void finishBatching() {
    if (!myBatchStarted) throw new IllegalStateException("Batching has not been even started");
    myBatchStarted = false;
  }

  public void dispose() {
    myRepository.removeRepositoryListener(myRepositoryListener);
  }

  public void init() {
    myRepository.addRepositoryListener(myRepositoryListener);
  }

  /**
   *  This class listens for module's add/removal, for 'moduleChanged' event (triggered by AbstractModule)
   *  and for internal (so far) 'dependenciesChanged' event.
   */
  private class MySRepositoryListener extends SRepositoryContentAdapter implements SModuleDependenciesListener {
    private void addEventToList(@NotNull SRepositoryEvent event) {
      synchronized (LOCK) {
        myEvents.add(event);
      }
    }

    @Override
    public void moduleAdded(@NotNull SModule module) {
      if (!myBatchStarted) return;
      if (module instanceof ReloadableModuleBase) {
        module.addModuleListener(this);
        ((ReloadableModuleBase) module).addDependenciesListener(this);
      }
      addEventToList(new SModuleAddedEvent(module));
    }

    @Override
    public void beforeModuleRemoved(@NotNull SModule module) {
      if (!myBatchStarted) return;
      if (module instanceof ReloadableModuleBase) {
        ((ReloadableModuleBase) module).removeDependenciesListener(this);
        module.removeModuleListener(this);
      }
    }

    @Override
    public void moduleRemoved(@NotNull SModuleReference mRef) {
      if (!myBatchStarted) return;
      addEventToList(new SModuleRemovedEvent(mRef, myRepository));
    }

    @Override
    public void moduleChanged(@NotNull SModule module) {
      if (!myBatchStarted) return;
      addEventToList(new SModuleChangedEvent(module));
    }

    @Override
    public void dependenciesChanged(@NotNull ReloadableModuleBase module) {
      if (!myBatchStarted) return;
      moduleChanged(module);
    }
  }
}
