# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.  The
# ASF licenses this file to you 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.

require_relative '../helpers/database_helper'

# Service objects implement the server functionality of CIMI resources; in
# particular, these objects are responsible for interacting with the
# current driver. They use the CIMI::Model objects for (de)serialization
module CIMI::Service

  class Base

    # Extend the base model with database methods
    extend Deltacloud::Helpers::Database

    attr_reader :model, :context

    class << self
      def model_class
        CIMI::Model.const_get(name.split('::').last)
      end

      def model_name
        name.split('::').last.underscore.to_sym
      end

      def collection_name
        name.split('::').last.underscore.pluralize.to_sym
      end

      def inherited(subclass)
        # Decorate all the attributes of the model class
        schema = subclass.model_class.schema
        schema.attribute_names.each do |name|
          define_method(name) { self[name] }
          define_method(:"#{name}=") { |newval| self[name] = newval }
        end
      end

      def parse(context)
        req = context.request
        model = model_class.parse(req.body, req.content_type)
        new(context, :model => model)
      end

    end

    def initialize(context, opts)
      if opts[:values]
        @model = model_class.new(opts[:values])
      elsif opts[:model]
        @model = opts[:model]
      else
        @model = model_class.new({})
      end
      @context = context
      retrieve_entity
    end

    def model_class
      self.class.model_class
    end

    # Decorate some model methods
    def []=(a, v)
      v = (@model[a] = v)
      retrieve_entity if a == :id
      v
    end

    def [](a)
      @model[a]
    end

    def to_xml
      @model.to_xml
    end

    def to_json
      @model.to_json
    end

    def select_attributes(attr_list)
      @model.select_attributes(attr_list)
    end

    # Lookup a reference and return the corresponding model
    def resolve(ref)
      self.class.resolve(ref, context)
    end

    def self.resolve(ref, ctx)
      model = nil
      if ref.href?
        name = ref.class.superclass.name.split('::').last
        service_class = CIMI::Service::const_get(name)
        id = ref.href.split('/').last
        model = service_class.find(id, ctx)
      else
        # FIXME: if ref.href? we need to overwrite
        # attributes in model with ones from ref as long as they are present
        model = ref
      end
      model
    end

    def self.list(ctx)
      id = ctx.send("#{collection_name}_url")
      entries = find(:all, ctx)
      params = {}
      params[:desc] = "#{self.name.split("::").last} Collection for the #{ctx.driver.name.capitalize} driver"
      params[:add_url] = create_url(ctx)
      if model_class == CIMI::Model::System
        params[:system] = id
        params[:import_url] = import_url(ctx)
      elsif model_class == CIMI::Model::SystemTemplate
        params[:import_url] = import_url(ctx)
      end
      model_class.list(id, entries, params).select_by(ctx.params['$select']).filter_by(ctx.params['$filter'])
    end

    def self.create_url(ctx)
      cimi_create = "create_#{model_name}_url"
      dcloud_create = ctx.deltacloud_create_method_for(model_name)
      if(ctx.respond_to?(cimi_create) &&
         ctx.driver.respond_to?(dcloud_create)) || provides?(model_name)
        ctx.send(cimi_create)
      end
    end

    # used for system and system template import
    def self.import_url(ctx)
      if ctx.driver.respond_to?("import_#{model_name}") || provides?(model_name)
        base_url = ctx.send("#{model_name}_url")
        "#{base_url}/import"
      end
    end

    #
    # Resource metadata
    #
    METADATA_TYPES = [ 'text', 'URI', 'string', 'boolean' ]

    # A hash of the attributes that need to be mentioned in the given
    # context
    def self.resource_attributes(context)
      metadata.keys.map do |k|
        a = model_class.schema.attributes.find { |a| a.name == k }
        raise "No attribute named #{k} defined" unless a
        constr = metadata[k][:constraints].call(context)
        {
          :name => a.name,
          :namespace => "http://deltacloud.org/cimi/#{model_name}/#{a.name}",
          :type => metadata[k][:type],
          :required => a.required? ? 'true' : 'false',
          :constraints => constr.map { |v| { :value => v } }
        }
      end
    end

    def self.resource_capabilities(context)
      cimi_object = model_name.to_s.pluralize.to_sym
      driver_class = context.driver.class
      (driver_class.features[cimi_object] || []).map do |cur|
        feat = CIMI::FakeCollection.feature(cur)
        values = driver_class.constraints[cimi_object][feat.name][:values] || []
        { :name => feat.name.to_s.camelize,
          :uri => CMWG_NAMESPACE+"/capability/#{cimi_object.to_s.camelize.singularize}/#{feat.name.to_s.camelize}",
          :description => feat.description,
          :value => values.join(",")
        }
      end
    end

    # Define the metadata for an attribute; +opts+ must be a Hash that can
    # contain the following entries:
    #   :type : one of METADATA_TYPES
    #   :constraints : a proc that is passed the current context and
    #                  must return a list of values
    def self.metadata(attr_name = nil, opts = nil)
      @metadata ||= {}
      return @metadata if attr_name.nil? && opts.nil?

      opts[:type] ||= 'text'
      opts[:type] = opts[:type].to_s
      opts[:constraints] ||= lambda { |_| [] }
      unless METADATA_TYPES.include?(opts[:type])
        raise "Metadata type must be one of #{METADATA_TYPES.join(",")}"
      end
      metadata[attr_name] = opts
    end

    #
    # Database interactions
    #

    # Save the common attributes name, description, and properties to the
    # database
    def save
      if @entity
        before_save
        @entity.save
      end
      self
    end

    # Destroy the database attributes for this model
    def destroy
      @entity.destroy
      self
    end

    # FIXME: Kludge around the fact that we do not have proper *Create
    # objects that deserialize properties by themselves
    def extract_properties!(data)
      h = {}
      if data['property']
        # Data came from XML
        h = data['property'].inject({}) do |r,v|
          r[v['key']] = v['content']
          r
        end
      elsif data['properties']
        h = data['properties']
      end
      property ||= {}
      property.merge!(h)
    end

    def ref_id(ref_url)
      ref_url.split('/').last if ref_url
    end

    protected

    def attributes_to_copy
      [:name, :description]
    end

    def before_save
      attributes_to_copy.each { |a| @entity[a] = @model[a] }
      @entity.properties = @model.property
    end

    def after_retrieve
      attributes_to_copy.each { |a| @model[a] = @entity[a] }
      @model.property ||= {}
      @model.property.merge!(@entity.properties)
    end

    private

    # Load an existing database entity for this object, or create a new one
    def retrieve_entity
      if self.id
        @entity = Deltacloud::Database::Entity::retrieve(@model)
        if @entity.exists?
          after_retrieve
        end
      else
        @entity = nil
      end
    end

  end
end
