/*
 * Copyright 2000-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 com.intellij.openapi.vcs;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.SomeQueue;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * For exactly same refresh requests buffering:
 * <p/>
 * - refresh requests can be merged into one, but general principle is that each request should be reliably followed by refresh action
 * - at the moment only one refresh action is being done
 * - if request had been submitted while refresh action was in progress, new refresh action is initiated right after first refresh action finishes
 */
@SomeQueue
public class RequestsMerger {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.RequestsMerger");

  private final MyWorker myWorker;

  private final Object myLock = new Object();

  private MyState myState;
  private final Consumer<Runnable> myAlarm;

  private final List<Runnable> myWaitingStartListeners = new ArrayList<Runnable>();
  private final List<Runnable> myWaitingFinishListeners = new ArrayList<Runnable>();

  public RequestsMerger(final Runnable runnable, final Consumer<Runnable> alarm) {
    myAlarm = alarm;
    myWorker = new MyWorker(runnable);

    myState = MyState.empty;
  }

  public void request() {
    LOG.debug("ext: request");
    doAction(MyAction.request);
  }

  public void waitRefresh(final Runnable runnable) {
    LOG.debug("ext: wait refresh");
    synchronized (myLock) {
      myWaitingStartListeners.add(runnable);
    }
    request();
  }

  private class MyWorker implements Runnable {
    private volatile boolean myInitialized;
    private final Runnable myRunnable;

    private MyWorker(Runnable runnable) {
      myRunnable = runnable;
    }

    @Override
    public void run() {
      LOG.debug("worker: started refresh");
      try {
        doAction(MyAction.start);
        myRunnable.run();
        myInitialized = true;
      }
      finally {
        doAction(MyAction.finish);
      }
    }

    public boolean isInitialized() {
      return myInitialized;
    }
  }

  private void doAction(final MyAction action) {
    LOG.debug("doAction: START " + action.name());
    final MyExitAction[] exitActions;
    List<Runnable> toBeCalled = null;
    synchronized (myLock) {
      final MyState oldState = myState;
      myState = myState.transition(action);
      if (oldState.equals(myState)) return;
      exitActions = MyTransitionAction.getExit(oldState, myState);

      LOG.debug("doAction: oldState: " + oldState.name() + ", newState: " + myState.name());

      if (LOG.isDebugEnabled() && exitActions != null) {
        final String debugExitActions = StringUtil.join(exitActions, new Function<MyExitAction, String>() {
          @Override
          public String fun(MyExitAction exitAction) {
            return exitAction.name();
          }
        }, " ");
        LOG.debug("exit actions: " + debugExitActions);
      }
      if (exitActions != null) {
        for (MyExitAction exitAction : exitActions) {
          if (MyExitAction.markStart.equals(exitAction)) {
            myWaitingFinishListeners.addAll(myWaitingStartListeners);
            myWaitingStartListeners.clear();
          }
          else if (MyExitAction.markEnd.equals(exitAction)) {
            toBeCalled = new ArrayList<Runnable>(myWaitingFinishListeners);
            myWaitingFinishListeners.clear();
          }
        }
      }
    }
    if (exitActions != null) {
      for (MyExitAction exitAction : exitActions) {
        if (MyExitAction.submitRequestToExecutor.equals(exitAction)) {
          myAlarm.consume(myWorker);
          //myAlarm.addRequest(myWorker, ourDelay);
          //ApplicationManager.getApplication().executeOnPooledThread(myWorker);
        }
      }
    }
    if (toBeCalled != null) {
      for (Runnable runnable : toBeCalled) {
        runnable.run();
      }
    }
    LOG.debug("doAction: END " + action.name());
  }

  private static enum MyState {
    empty() {
      @Override
      @NotNull
      public MyState transition(MyAction action) {
        if (MyAction.request.equals(action)) {
          return MyState.requestSubmitted;
        }
        logWrongAction(this, action);
        return this;
      }
    },
    inProgress() {
      @Override
      @NotNull
      public MyState transition(MyAction action) {
        if (MyAction.finish.equals(action)) {
          return empty;
        }
        else if (MyAction.request.equals(action)) {
          return MyState.inProgressRequestSubmitted;
        }
        logWrongAction(this, action);
        return this;
      }
    },
    inProgressRequestSubmitted() {
      @Override
      @NotNull
      public MyState transition(MyAction action) {
        if (MyAction.finish.equals(action)) {
          return MyState.requestSubmitted;
        }
        if (MyAction.start.equals(action)) {
          logWrongAction(this, action);
        }
        return this;
      }
    },
    requestSubmitted() {
      @Override
      @NotNull
      public MyState transition(MyAction action) {
        if (MyAction.start.equals(action)) {
          return inProgress;
        }
        else if (MyAction.finish.equals(action)) {
          // to be able to be started by another request
          logWrongAction(this, action);
          return empty;
        }
        return this;
      }
    };

    // under lock
    @NotNull
    public abstract MyState transition(final MyAction action);

    private static void logWrongAction(final MyState state, final MyAction action) {
      LOG.info("Wrong action: state=" + state.name() + ", action=" + action.name());
    }
  }

  private static class MyTransitionAction {
    private static final Map<Couple<MyState>, MyExitAction[]> myMap = new HashMap<Couple<MyState>, MyExitAction[]>();

    static {
      add(MyState.empty, MyState.requestSubmitted, MyExitAction.submitRequestToExecutor);
      add(MyState.requestSubmitted, MyState.inProgress, MyExitAction.markStart);
      add(MyState.inProgress, MyState.empty, MyExitAction.markEnd);
      add(MyState.inProgressRequestSubmitted, MyState.requestSubmitted, MyExitAction.submitRequestToExecutor, MyExitAction.markEnd);

      //... and not real but to be safe:
      add(MyState.inProgressRequestSubmitted, MyState.empty, MyExitAction.markEnd);
      add(MyState.inProgress, MyState.requestSubmitted, MyExitAction.markEnd);
    }

    private static void add(final MyState from, final MyState to, final MyExitAction... action) {
      myMap.put(Couple.newOne(from, to), action);
    }

    @Nullable
    public static MyExitAction[] getExit(final MyState from, final MyState to) {
      return myMap.get(Couple.newOne(from, to));
    }
  }

  private static enum MyExitAction {
    empty,
    submitRequestToExecutor,
    markStart,
    markEnd
  }

  private static enum MyAction {
    request,
    start,
    finish
  }
}

@@ -4823,31 +4823,24 @@
 %0A%0A  private 
-static 
 enum MyState
@@ -7591,31 +7591,24 @@
 %0A%0A  private 
-static 
 enum MyExitA
@@ -7697,23 +7697,16 @@
 private 
-static 
 enum MyA

remove unnecessary static