/*
 * Copyright 2000-2013 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.buildServer.clouds.vmware;

import com.intellij.openapi.diagnostic.Logger;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import jetbrains.buildServer.clouds.*;
import jetbrains.buildServer.clouds.vmware.settings.VMImageInfo;
import jetbrains.buildServer.clouds.vmware.vmrun.VMRun;
import jetbrains.buildServer.clouds.vmware.vmrun.VMRunParameters;
import jetbrains.buildServer.clouds.vmware.vmrun.remote.server.ClientState;
import org.jetbrains.annotations.NotNull;

/**
 * @author Eugene Petrenko
 *         Created: 20.03.2010 18:04:08
 */
public class VMRemoteHost extends ErrorHolder {
  private static final Logger LOG = Logger.getInstance(VMRemoteHost.class.getName());

  private final ScheduledExecutorService myExecutor;

  private final Map<String, VMWareImage> myImages = new HashMap<String, VMWareImage>();
  private final Map<String, VMWareImage> myImageIdToImage = new HashMap<String, VMWareImage>();

  private final VMRun myVMRun;
  private final ClientState myVMRunState;

  private final AtomicBoolean myImagesSynchronized = new AtomicBoolean(false);

  public VMRemoteHost(@NotNull final VMRun run, @NotNull final ClientState state, final ScheduledExecutorService executor) {
    myExecutor = executor;
    myVMRun = run;
    myVMRunState = state;

    myExecutor.execute(wrap("Check version", new Runnable() {
      public void run() {
        final int version = run.getVersion(VMRun.VERSION);
        if (version != VMRun.VERSION) {
          LOG.warn("Client: " + run.getHostName() + " is obsolete!");
        }
      }
    }));

    myExecutor.scheduleWithFixedDelay(wrap("Update images", new Runnable() {
      public void run() {
        if (!myVMRunState.isAlive()) {
          dispose();
          return;
        }
        final Collection<VMImageInfo> allInstances = myVMRun.getLocalImages();
        final Set<String> runningInstances = new HashSet<String>();
        for (VMImageInfo instance : myVMRun.getRunningImages()) {
          runningInstances.add(instance.getId());
        }

        updateImagesAndInstances(allInstances, runningInstances);

        myImagesSynchronized.set(true);
      }
    }), 15, 15, TimeUnit.SECONDS);
  }

  @NotNull
  public String getId() {
    return myVMRun.getLocationId();
  }

  @NotNull
  public String getHostName() {
    return myVMRun.getHostName();
  }

  private synchronized void updateImagesAndInstances(final Collection<VMImageInfo> allInstances, final Set<String> runningInstances) {
    Set<String> obsoleteImages = new HashSet<String>(myImageIdToImage.keySet());
    for (VMImageInfo info : allInstances) {
      VMWareImage image = myImageIdToImage.get(info.getId());
      obsoleteImages.remove(info.getId());
      if (image != null) {
        image.update(info);
      } else {
        image = new VMWareImage(info, this);
        myImageIdToImage.put(info.getId(), image);
      }

      if (runningInstances.contains(info.getId())) {
        image.setRunningInstanceDetected();
      } else {
        image.setNoRunningInstance();
      }
    }

    for (String obsoleteImage : obsoleteImages) {
      final VMWareImage image = myImageIdToImage.remove(obsoleteImage);
      if (image != null) {
        image.dispose();
      }
    }

    myImages.clear();
    for (VMWareImage image : myImageIdToImage.values()) {
      myImages.put(image.getId(), image);
    }
  }

  @NotNull
  public synchronized VMWareInstance startNewInstance(@NotNull final VMWareImage image, @NotNull final CloudInstanceUserData tag) {
    if (!canStartNewInstance(image)) throw new QuotaException("Failed to start more than one instance of the same type");

    final VMWareInstance instance = image.createStartingInstance();
    tag.addAgentConfigurationParameter(VMWareConstants.VMWARE_NAME_PROPOSAL, instance.getAgentName());

    myExecutor.submit(instance.wrapPermanentError("start new instnace", new Runnable() {
      public void run() {
        try {
          final VMWareImage im = instance.getImage();
          myVMRun.startMachine(new VMRunParameters(instance.getInstanceId(), instance.getImageId(), myVMRun.getLocationId(), im.getInfo()), tag);
          instance.setStatus(InstanceStatus.STARTING);
        } catch (QuotaException e) {
          LOG.info("Quota error: " + e.getMessage());
          instance.getImage().setNoRunningInstance();
          instance.dispose();
        }
      }
    }));

    return instance;
  }

  public synchronized boolean canStartNewInstance(final VMWareImage checkImage) {
    //Check if current image is running a machine
    if (checkImage.isInstanceRunning()) return false;

    int allowed = myVMRun.getAllowedMachinesToRun();
    for (VMWareImage image : getImages()) {
      if (image.isInstanceRunning()) {
        allowed--;
      }
    }
    return allowed > 0;
  }

  public synchronized void terminateInstance(@NotNull final VMWareInstance instance) {
    if (findImageById(instance.getImageId()) == null) {
      throw new CloudException("Image of instance " + instance + " was not found");
    }

    instance.setStatus(InstanceStatus.SCHEDULED_TO_STOP);
    myExecutor.submit(instance.wrapPermanentError("terminate instance", new Runnable() {
      public void run() {
        try {
          myVMRun.terminateMachine(instance.getImage().getInfo());
        } finally {
          instance.setStatus(InstanceStatus.STOPPING);
        }
      }
    }));
  }

  public synchronized void dispose() {
    for (VMWareImage image : myImages.values()) {
      image.dispose();
    }
    myImages.clear();
    myExecutor.shutdownNow();
  }

  public boolean isInitialized() {
    return myImagesSynchronized.get();
  }

  public synchronized CloudImage findImageById(@NotNull final String imageId) throws CloudException {
    return myImages.get(imageId);
  }

  @NotNull
  public synchronized Collection<? extends VMWareImage> getImages() throws CloudException {
    return new ArrayList<VMWareImage>(myImages.values());
  }
}
