require 'fog/openstack/models/model'

module Fog
  module OpenStack
    class Image
      class V2
        class Image < Fog::OpenStack::Model
          identity :id

          attribute :name
          attribute :visibility # public or private

          attribute :tags

          attribute :self
          attribute :size
          attribute :virtual_size
          attribute :disk_format
          attribute :container_format
          attribute :id
          attribute :checksum
          attribute :self
          attribute :file

          # detailed
          attribute :min_disk
          attribute :created_at
          attribute :updated_at
          attribute :protected
          attribute :status # "queued","saving","active","killed","deleted","pending_delete"
          attribute :min_ram
          attribute :owner
          attribute :properties
          attribute :metadata
          attribute :location

          # from snapshot support
          attribute :network_allocated
          attribute :base_image_ref
          attribute :image_type
          attribute :instance_uuid
          attribute :user_id

          def method_missing(method_sym, *arguments, &block)
            if attributes.key?(method_sym)
              attributes[method_sym]
            elsif attributes.key?(method_sym.to_s)
              attributes[method_sym.to_s]
            elsif method_sym.to_s.end_with?('=')
              attributes[method_sym.to_s.gsub(/=$/, '').to_sym] = arguments[0]
            else
              super
            end
          end

          def respond_to?(method_sym, include_all = false)
            if attributes.key?(method_sym)
              true
            elsif attributes.key?(method_sym.to_s)
              true
            elsif method_sym.to_s.end_with?('=')
              true
            else
              super
            end
          end

          def create
            requires :name
            merge_attributes(service.create_image(attributes).body)
            self
          end

          # Here we convert 'attributes' into a form suitable for Glance's usage of JSON Patch (RFC6902).
          #  We fetch the existing attributes from the server to compute the delta (difference)
          #  Setting value to nil will delete that attribute from the server.
          def update(attr = nil)
            requires :id
            client_attributes = attr || @attributes
            server_attributes = service.images.get(id).attributes

            json_patch = build_update_json_patch(client_attributes, server_attributes)

            merge_attributes(
              service.update_image(id, json_patch).body
            )
            self
          end

          # This overrides the behaviour of Fog::OpenStack::Model::save() which tries to be clever and
          #  assumes save=update if an ID is present - but Image V2 allows ID to be specified on creation
          def save
            if @attributes[:self].nil?
              create
            else
              update
            end
          end

          def destroy
            requires :id
            service.delete_image(id)
            true
          end

          def upload_data(io_obj)
            requires :id
            if io_obj.kind_of? Hash
              service.upload_image(id, nil, io_obj)
            else
              service.upload_image(id, io_obj)
            end
          end

          def download_data(params = {})
            requires :id
            service.download_image(id, params[:content_range], params)
          end

          def reactivate
            requires :id
            service.reactivate_image(id)
          end

          def deactivate
            requires :id
            service.deactivate_image(id)
          end

          def add_member(member_id)
            requires :id
            service.add_member_to_image(id, member_id)
          end

          def remove_member(member_id)
            requires :id
            service.remove_member_from_image(id, member_id)
          end

          def update_member(member)
            requires :id
            service.update_image_member(id, member)
          end

          def members
            requires :id
            service.get_image_members(id).body['members']
          end

          def member(member_id)
            requires :id
            service.get_member_details(id, member_id)
          end

          def add_tags(tags)
            requires :id
            tags.each { |tag| add_tag tag }
          end

          def add_tag(tag)
            requires :id
            service.add_tag_to_image(id, tag)
          end

          def remove_tags(tags)
            requires :id
            tags.each { |tag| remove_tag tag }
          end

          def remove_tag(tag)
            requires :id
            service.remove_tag_from_image(id, tag)
          end

          private

          def build_update_json_patch(client_attributes, server_attributes)
            [
              build_patch_operation('remove', patch_attributes_to_remove(client_attributes, server_attributes)),
              build_patch_operation('add', patch_attributes_to_add(client_attributes, server_attributes)),
              build_patch_operation('replace', patch_attributes_to_replace(client_attributes, server_attributes)),
            ].flatten
          end

          def patch_attributes_to_remove(client_attributes, server_attributes)
            client_attributes.select do |key, value|
              value.nil? && !server_attributes[key].nil?
            end
          end

          def patch_attributes_to_add(client_attributes, server_attributes)
            client_attributes.reject do |key, _|
              server_attributes.key?(key) || client_attributes[key].nil?
            end
          end

          def patch_attributes_to_replace(client_attributes, server_attributes)
            client_attributes.reject do |key, value|
              value.nil? || server_attributes[key] == value
            end
          end

          def build_patch_operation(op_name, attributes)
            json_patch = []
            attributes.each do |key, value|
              json_patch << {:op => op_name, :path => "/#{key}", :value => value}
            end
            json_patch
          end
        end
      end
    end
  end
end
