require 'log4r'

module Vagrant
  class VM
    include Vagrant::Util

    attr_reader :uuid
    attr_reader :env
    attr_reader :name
    attr_reader :vm
    attr_reader :box
    attr_reader :config
    attr_reader :driver

    def initialize(name, env, config, opts=nil)
      @logger = Log4r::Logger.new("vagrant::vm")

      @name   = name
      @vm     = nil
      @env    = env
      @config = config
      @box    = env.boxes.find(config.vm.box)

      opts ||= {}
      if opts[:base]
        # The name is the ID we use.
        @uuid = name
      else
        # Load the UUID if its saved.
        active = env.local_data[:active] || {}
        @uuid = active[@name.to_s]
      end

      # Reload ourselves to get the state
      reload!

      # Load the associated guest.
      load_guest!
      @loaded_guest_distro = false
    end

    # Loads the guest associated with the VM. The guest class is
    # responsible for OS-specific functionality. More information
    # can be found by reading the documentation on {Vagrant::Guest::Base}.
    #
    # **This method should never be called manually.**
    def load_guest!(guest=nil)
      guest ||= config.vm.guest
      @logger.info("Loading guest: #{guest}")

      if guest.is_a?(Class)
        raise Errors::VMGuestError, :_key => :invalid_class, :guest => guest.to_s if !(guest <= Guest::Base)
        @guest = guest.new(self)
      elsif guest.is_a?(Symbol)
        guest_klass = Vagrant.guests.get(guest)
        raise Errors::VMGuestError, :_key => :unknown_type, :guest => guest.to_s if !guest_klass
        @guest = guest_klass.new(self)
      else
        raise Errors::VMGuestError, :unspecified
      end
    end

    # Returns a channel object to communicate with the virtual
    # machine.
    def channel
      @channel ||= Communication::SSH.new(self)
    end

    # Returns the guest for this VM, loading the distro of the system if
    # we can.
    def guest
      if !@loaded_guest_distro && state == :running
        # Load the guest distro for the first time
        result = @guest.distro_dispatch
        load_guest!(result)
        @loaded_guest_distro = true
      end

      @guest
    end

    # Access the {Vagrant::SSH} object associated with this VM, which
    # is used to get SSH credentials with the virtual machine.
    def ssh
      @ssh ||= SSH.new(self)
    end

    # Returns the state of the VM as a symbol.
    #
    # @return [Symbol]
    def state
      return :not_created if !@uuid
      state = @driver.read_state
      return :not_created if !state
      return state
    end

    # Returns a boolean true if the VM has been created, otherwise
    # returns false.
    #
    # @return [Boolean]
    def created?
      state != :not_created
    end

    # Sets the currently active VM for this VM. If the VM is a valid,
    # created virtual machine, then it will also update the local data
    # to persist the VM. Otherwise, it will remove itself from the
    # local data (if it exists).
    def uuid=(value)
      env.local_data[:active] ||= {}
      if value
        env.local_data[:active][name.to_s] = value
      else
        env.local_data[:active].delete(name.to_s)
      end

      # Commit the local data so that the next time vagrant is initialized,
      # it realizes the VM exists
      env.local_data.commit

      # Store the uuid and reload the instance
      @uuid = value
      reload!
    end

    def reload!
      begin
        @driver = Driver::VirtualBox.new(@uuid)
      rescue Driver::VirtualBox::VMNotFound
        # Clear the UUID since this VM doesn't exist.
        @uuid = nil

        # Reset the driver. This shouldn't raise a VMNotFound since we won't
        # feed it a UUID.
        @driver = Driver::VirtualBox.new
      end
    end

    def package(options=nil)
      run_action(:package, { "validate" => false }.merge(options || {}))
    end

    def up(options=nil)
      run_action(:up, options)
    end

    def start(options=nil)
      return if state == :running
      return resume if state == :saved

      run_action(:start, options)
    end

    def halt(options=nil)
      run_action(:halt, options)
    end

    def reload(options=nil)
      run_action(:reload, options)
    end

    def provision
      run_action(:provision)
    end

    def destroy
      run_action(:destroy)
    end

    def suspend
      run_action(:suspend)
    end

    def resume
      run_action(:resume)
    end

    def ui
      return @_ui if defined?(@_ui)
      @_ui = @env.ui.dup
      @_ui.resource = @name
      @_ui
    end

    def run_action(name, options=nil)
      options = {
        :vm => self,
        :ui => ui
      }.merge(options || {})

      env.action_runner.run(name, options)
    end
  end
end
