/* 
 * $Id$
 */
package zielu.svntoolbox.async;

import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.DumbService.DumbModeListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import zielu.svntoolbox.FileStatus;
import zielu.svntoolbox.FileStatusCalculator;
import zielu.svntoolbox.SvnToolBoxApp;
import zielu.svntoolbox.SvnToolBoxProject;
import zielu.svntoolbox.projectView.ProjectViewManager;
import zielu.svntoolbox.projectView.ProjectViewStatus;
import zielu.svntoolbox.projectView.ProjectViewStatusCache;
import zielu.svntoolbox.util.LogStopwatch;
import zielu.svntoolbox.util.MfSupplier;

/**
 * <p></p>
 * <br/>
 * <p>Created on 05.12.13</p>
 *
 * @author Lukasz Zielinski
 */
public class AsyncFileStatusCalculator extends AbstractProjectComponent implements AsyncStatusCalc {
    private final Logger LOG = Logger.getInstance(getClass());

    private final SvnToolBoxApp app;
    
    private final FileStatusCalculator myStatusCalc = new FileStatusCalculator();
    private final BlockingQueue<StatusRequest> myRequestQueue = new LinkedBlockingQueue<StatusRequest>();
  private final Set<VirtualFile> myPendingFiles = ContainerUtil.newConcurrentSet();

    private final AtomicBoolean myActive = new AtomicBoolean();
    private final AtomicBoolean myCalculationInProgress = new AtomicBoolean();
    private final AtomicBoolean myCalculationAllowed = new AtomicBoolean(true);

    private ProjectViewManager myProjectViewManager;
    private Supplier<Integer> PV_SEQ;
    private MessageBusConnection myConnection;

    public AsyncFileStatusCalculator(Project project, SvnToolBoxApp app) {
        super(project);
        this.app = app;        
    }

    public static AsyncFileStatusCalculator getInstance(@NotNull Project project) {
        return project.getComponent(AsyncFileStatusCalculator.class);
    }

    @Override
    public void initComponent() {
        super.initComponent();
        if (myActive.compareAndSet(false, true)) {
            myProjectViewManager = ProjectViewManager.getInstance(myProject);
            PV_SEQ = SvnToolBoxProject.getInstance(myProject).sequence();
            myConnection = myProject.getMessageBus().connect();
            myConnection.subscribe(DumbService.DUMB_MODE, new DumbModeListener() {
                @Override
                public void enteredDumbMode() {
                    myCalculationAllowed.compareAndSet(true, false);
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("[" + PV_SEQ.get() + "] Entered Dumb-Mode");
                    }
                }

                @Override
                public void exitDumbMode() {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("[" + PV_SEQ.get() + "] Exit Dumb-Mode");
                    }
                    myCalculationAllowed.compareAndSet(false, true);
                    calculateStatus();
                }
            });
        }
    }

    @Override
    public void refreshView() {
        if (LOG.isDebugEnabled()) {
            LOG.debug("[" + PV_SEQ.get() + "] Requesting Project View refresh");
        }
        myProjectViewManager.refreshProjectView(myProject);
    }

    public void calculateStatus() {
        final boolean DEBUG = LOG.isDebugEnabled();
        if (myActive.get()) {
            if (myCalculationAllowed.get()) {
                if (!myCalculationInProgress.get()) {
                    app.submit(new Task());
                } else {
                    if (DEBUG) {
                        LOG.debug("[" + PV_SEQ.get() + "] Another status calculation in progress");
                    }
                }
            } else {
                if (DEBUG) {
                    LOG.debug("[" + PV_SEQ.get() + "] Calculation not available at this moment");
                }
            }
        } else {
            if (DEBUG) {
                LOG.debug("[" + PV_SEQ.get() + "] Component inactive - status calculation cancelled");
            }
        }
    }

    @Override
    public void projectClosed() {
        if (myActive.compareAndSet(true, false)) {
            int pendingFiles = myPendingFiles.size();
            myPendingFiles.clear();
            if (LOG.isDebugEnabled()) {
                LOG.debug("[" + PV_SEQ.get() + "] Project closed. Pending: files=" + pendingFiles + ", requests=" + myRequestQueue.size());
            }
            myRequestQueue.clear();
            myConnection.disconnect();
        }
        super.projectClosed();
    }

    @Override
    public void disposeComponent() {
        myRequestQueue.clear();
        int pendingFiles = myPendingFiles.size();
        myPendingFiles.clear();
        if (LOG.isDebugEnabled()) {
            LOG.debug("[" + PV_SEQ.get() + "] Component disposed. Pending: files=" + pendingFiles + ", requests=" + myRequestQueue.size());
        }        
        super.disposeComponent();
    }

    public Optional<FileStatus> scheduleStatusFor(@Nullable Project project, @NotNull VirtualFile vFile) {
        if (myActive.get()) {
            if (project == null) {
                return FileStatus.EMPTY_OPTIONAL;
            } else {
                if (myPendingFiles.add(vFile)) {
                    myRequestQueue.add(new StatusRequest(project, vFile));
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("[" + PV_SEQ.get() + "] Queued request for " + vFile.getPath());
                        LOG.debug("[" + PV_SEQ.get() + "] Scheduling on-queued status calculation - " + myRequestQueue.size() + " requests pending");
                    }
                    calculateStatus();
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("[" + PV_SEQ.get() + "] " + vFile.getPath() + " already awaits calculation");
                    }
                }
            }
        }
      return Optional.empty();
    }

    private class Task implements Runnable {

        @Override
        public void run() {
            final boolean DEBUG = LOG.isDebugEnabled();
            if (myCalculationInProgress.compareAndSet(false, true)) {
                boolean exhausted = false;
                try {
                    StatusRequest request = myRequestQueue.poll(150, TimeUnit.MILLISECONDS);
                    if (request != null) {
                        if (myPendingFiles.remove(request.file)) {                                                        
                            AccessToken token = ApplicationManager.getApplication().acquireReadActionLock();
                            LogStopwatch watch = LogStopwatch.debugStopwatch(PV_SEQ,
                                    new MfSupplier("Status calculation for {0}", request.file)).start();
                            FileStatus status;
                            try {                                                                                                 
                                status = myStatusCalc.statusFor(request.project, request.file);                                                                                                
                            } finally {
                                token.finish();
                                watch.stop();
                            }
                            ProjectViewStatus viewStatus;
                            if (status.isUnderVcs()) {
                                if (status.getBranchName().isPresent()) {
                                    viewStatus = new ProjectViewStatus(status.getBranchName().get());
                                } else {
                                    viewStatus = ProjectViewStatus.NOT_CONFIGURED;
                                }
                            } else {
                                viewStatus = ProjectViewStatus.EMPTY;    
                            }
                            ProjectViewStatusCache cache = myProjectViewManager.getStatusCache();
                            cache.add(request.file, viewStatus);
                        } else {
                            if (DEBUG) {
                                LOG.debug("[" + PV_SEQ.get() + "] " + request.file.getPath() + " was already calculated");
                            }
                        }
                    } else {
                        if (DEBUG) {
                            LOG.debug("[" + PV_SEQ.get() + "] Requests exhausted");
                        }
                        exhausted = true;
                    }
                } catch (InterruptedException e) {
                    LOG.error(e);
                } finally {
                    if (myCalculationInProgress.compareAndSet(true, false)) {
                        if (exhausted) {
                            refreshView();
                        } else {
                            if (DEBUG) {
                                LOG.debug("[" + PV_SEQ.get() + "] Scheduling next status calculation - " + myRequestQueue.size() + " requests pending");
                            }
                            calculateStatus();
                        }
                    }
                }
            } else {
                if (DEBUG) {
                    LOG.debug("[" + PV_SEQ.get() + "] Another status calculation in progress");
                }
            }
        }
    }
}