content: /*
   * Copyright 2012 Andrew C. Dvorak.
   *
   * 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 net.andydvorak.intellij.lessc.fs;
  
  import com.asual.lesscss.LessEngine;
  import com.asual.lesscss.LessException;
  import com.intellij.openapi.diagnostic.Logger;
  import net.andydvorak.intellij.lessc.observer.CompileEvent;
  import net.andydvorak.intellij.lessc.observer.CompileObservable;
  import net.andydvorak.intellij.lessc.observer.CompileObserver;
  import net.andydvorak.intellij.lessc.state.LessProfile;
  import org.jetbrains.annotations.NotNull;
  
  import java.io.IOException;
  import java.util.LinkedHashSet;
  import java.util.Set;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.concurrent.atomic.AtomicLong;
  
  public class LessCompileJob implements CompileObservable {
  
      private static final long WAIT_INTERVAL_MS = 250;
      private static final Logger LOG = Logger.getInstance("#" + LessCompileJob.class.getName());
  
      private final AtomicBoolean compiled = new AtomicBoolean();
      private final AtomicBoolean running = new AtomicBoolean();
  
      private final AtomicLong finished = new AtomicLong(0);
  
      private final LessFile sourceLessFile;
      private final LessProfile lessProfile;
  
      private final Set<LessFile> sourceAndDependents = new LinkedHashSet<LessFile>();
      private final AtomicInteger curLessFileIndex = new AtomicInteger(-1);
  
      private final Set<LessFile> updatedLessFiles = new LinkedHashSet<LessFile>();
      private final Set<CompileObserver> observers = new LinkedHashSet<CompileObserver>();
  
      /*
       * Constructors
       */
  
      public LessCompileJob(final LessFile sourceLessFile, final LessProfile lessProfile) {
          this.sourceLessFile = sourceLessFile;
          this.lessProfile = lessProfile;
      }
  
      /*
       * Public instance methods
       */
  
      public LessFile getSourceLessFile() {
          return sourceLessFile;
      }
  
      public Set<LessFile> getSourceAndDependents() {
          return sourceAndDependents;
      }
  
      public LessFile getCurLessFile() {
          final int index = curLessFileIndex.get();
          if (!sourceAndDependents.isEmpty() && index > -1) {
              final LessFile[] lessFiles = new LessFile[sourceAndDependents.size()];
              sourceAndDependents.toArray(lessFiles);
              return lessFiles[index];
          } else {
              return null;
          }
      }
  
      public void addUpdatedLessFile(final LessFile lessFile) {
          updatedLessFiles.add(lessFile);
      }
  
      /**
       * @return set of LESS {@code File}s that produced new or updated CSS files after being compiled
       */
      public Set<LessFile> getUpdatedLessFiles() {
          return new LinkedHashSet<LessFile>(updatedLessFiles);
      }
  
      public int getNumUpdated() {
          return getUpdatedLessFiles().size();
      }
  
      @Override
      public void addObserver(@NotNull final CompileObserver observer) {
          observers.add(observer);
      }
  
      @Override
      public void removeObserver(@NotNull final CompileObserver observer) {
          observers.remove(observer);
      }
  
      @Override
      public void notifyObservers(@NotNull final CompileEvent event) {
          for (final CompileObserver observer : observers) {
              event.notify(observer);
          }
      }
  
      public void compile() throws IOException, LessException, IllegalStateException {
          preventConcurrency();
          try {
              start();
          } finally {
              finish();
          }
      }
  
      public void refreshVFS() {
          if (lessProfile != null)
              VirtualFileLocationChange.refresh(lessProfile.getCssDirectories());
      }
  
      public boolean isRunning() {
          return running.get();
      }
  
      public boolean isFinished() {
          return compiled.get();
      }
  
      public boolean needsToWait() {
          return isRunning() || (isFinished() && System.currentTimeMillis() - finished.get() <= WAIT_INTERVAL_MS);
      }
  
      /*
      * Private instance methods
      */
  
      /**
       * Throws an {@code IllegalStateException} if this job is run more than once.
       * @throws IllegalStateException
       */
      private void preventConcurrency() throws IllegalStateException {
          if (compiled.get() || running.get()) {
              final String message = "LessCompileJob for \"" + sourceLessFile.getName() + "\" can only be compiled once.";
              LOG.error(message);
              throw new IllegalStateException(message);
          }
      }
  
      private void findSourceAndDependents() throws IOException {
          if (sourceAndDependents.isEmpty()) {
              sourceAndDependents.add(sourceLessFile);
              if (lessProfile != null && lessProfile.hasCssDirectories()) {
                  final Set<LessFile> dependents = sourceLessFile.getDependentsRecursive(lessProfile);
                  sourceAndDependents.addAll(dependents);
              }
          }
      }
  
      private void start() throws IOException, LessException {
          running.set(true);
  
          findSourceAndDependents();
  
          notifyObservers(new CompileEvent() {
              @Override
              public void notify(@NotNull final CompileObserver observer) {
                  observer.compileStarted(sourceAndDependents);
              }
          });
  
          for (final LessFile lessFile : sourceAndDependents) {
              compile(lessFile);
          }
      }
  
      private void compile(@NotNull final LessFile lessFile) throws IOException, LessException {
          final boolean cssChanged;
  
          curLessFileIndex.incrementAndGet();
  
          if (lessFile.shouldCompile(lessProfile)) {
              lessFile.compile(LessEngine.getInstance(), lessProfile);
              cssChanged = lessFile.hasCssChanged();
          } else {
              cssChanged = false;
          }
  
          final CompileEvent event;
  
          if (cssChanged) {
              addUpdatedLessFile(lessFile);
              event = new CompileEvent() {
                  @Override
                  public void notify(@NotNull final CompileObserver observer) {
                      observer.outputFileChanged(lessFile);
                  }
              };
              refreshVFS();
          } else {
              event = new CompileEvent() {
                  @Override
                  public void notify(@NotNull final CompileObserver observer) {
                      observer.outputFileUnchanged(lessFile);
                  }
              };
          }
  
          notifyObservers(event);
      }
  
      private void finish() {
          finished.set(System.currentTimeMillis());
          compiled.set(true);
          running.set(false);
  
          final int numCompiled = getNumUpdated();
  
          notifyObservers(new CompileEvent() {
              @Override
              public void notify(@NotNull final CompileObserver observer) {
                  observer.compileFinished(numCompiled);
              }
          });
      }
  }
  