require 'mixlib/shellout'
require 'shellwords'

module Pacemaker
  class CIBObject
    attr_accessor :name, :definition

    @@subclasses = { } unless class_variable_defined?(:@@subclasses)

    class << self
      attr_reader :object_type

      def register_type(type_name)
        @object_type = type_name
        @@subclasses[type_name] = self
      end

      def get_definition(name)
        cmd = Mixlib::ShellOut.new("crm --display=plain configure show #{name}")
        cmd.environment['HOME'] = ENV.fetch('HOME', '/root')
        cmd.run_command
        begin
          cmd.error!
          cmd.stdout
        rescue
          nil
        end
      end

      def definition_type(definition)
        unless definition =~ /\A(\w+)\s/
          raise "Couldn't extract CIB object type from '#{definition}'"
        end
        return $1
      end

      def from_name(name)
        definition = get_definition(name)
        return nil unless definition and ! definition.empty?
        from_definition(definition)
      end

      # Make sure this works on Ruby 1.8.7 which is missing
      # Object#singleton_class.
      def singleton_class
        class << self; self; end
      end

      def from_definition(definition)
        calling_class = self.singleton_class
        this_class = method(__method__).owner
        if calling_class == this_class
          # Invoked via (this) base class
          obj_type = definition_type(definition)
          subclass = @@subclasses[obj_type]
          unless subclass
            raise "No subclass of #{self.name} was registered with type '#{obj_type}'"
          end
          return subclass.from_definition(definition)
        else
          # Invoked via subclass
          obj = new(name)
          unless name == obj.name
            raise "Name '#{obj.name}' in definition didn't match name '#{name}' used for retrieval"
          end
          obj.definition = definition
          obj.parse_definition
          obj
        end
      end

      def from_chef_resource(resource)
        new(resource.name).copy_attrs_from_chef_resource(resource,
                                                         *attrs_to_copy_from_chef)
      end

      def attrs_to_copy_from_chef
        raise NotImplementedError, "#{self.class} didn't implement attrs_to_copy_from_chef"
      end
    end

    def initialize(name)
      @name = name
      @definition = nil
    end

    def copy_attrs_from_chef_resource(resource, *attrs)
      attrs.each do |attr|
        value = resource.send(attr.to_sym)
        writer = (attr + '=').to_sym
        send(writer, value)
      end
      self
    end

    def copy_attrs_to_chef_resource(resource, *attrs)
      attrs.each do |attr|
        value = send(attr.to_sym)
        writer = attr.to_sym
        resource.send(writer, value)
      end
    end

    def load_definition
      @definition = self.class.get_definition(name)

      if @definition and ! @definition.empty? and type != self.class.object_type
        raise CIBObject::TypeMismatch, \
          "Expected #{self.class.object_type} type but loaded definition was type #{type}"
      end
    end

    def parse_definition
      raise NotImplementedError, "#{self.class} must implement #parse_definition"
    end

    def exists?
      !! (definition && ! definition.empty?)
    end

    def type
      self.class.definition_type(definition)
    end

    def to_s
      "%s '%s'" % [self.class.description, name]
    end

    def definition_indent
      ' ' * 9
    end

    def continuation_line(text)
      " \\\n#{definition_indent}#{text}"
    end

    # Returns a single-quoted shell-escaped version of the definition
    # string, suitable for use in a command like:
    #
    #     echo '...' | crm configure load update -
    def quoted_definition_string
      "'%s'" % \
      definition_string \
        .gsub('\\') { '\\\\' } \
        .gsub("'")  { "\\'" }
    end

    def escaped_definition_string
      Shellwords.join(Shellwords.split(definition_string))
    end

    def configure_command
      "crm configure " + escaped_definition_string
    end

    def reconfigure_command
      "echo #{quoted_definition_string} | crm configure load update -"
    end

    def delete_command
      "crm configure delete '#{name}'"
    end
  end

  class CIBObject::DefinitionParseError < StandardError
  end

  class CIBObject::TypeMismatch < StandardError
  end
end
