module Her
  module Model
    # This module adds ORM-like capabilities to the model
    module ORM
      # Initialize a new object with data received from an HTTP request
      # @private
      def initialize(single_data) # {{{
        @data = single_data
        @data = self.class.parse_relationships(@data)
      end # }}}

      # Initialize a collection of resources
      # @private
      def self.initialize_collection(name, collection_data) # {{{
        collection_data.map { |item_data| Object.const_get(name.to_s.classify).new(item_data) }
      end # }}}

      # Handles missing methods by routing them through @data
      # @private
      def method_missing(method, attrs=nil) # {{{
        assignment_method = method.to_s =~ /\=$/
        method = method.to_s.gsub(/(\?|\!|\=)$/, "").to_sym
        if attrs and assignment_method
          @data[method.to_s.gsub(/\=$/, "").to_sym] = attrs
        else
          if @data.include?(method)
            @data[method]
          else
            super
          end
        end
      end # }}}

      # Initialize a collection of resources with raw data from an HTTP request
      #
      # @param [Hash] parsed_data The raw `parsed_data` parsed from the HTTP response
      def new_collection(parsed_data) # {{{
        collection_data = parsed_data[:data]
        Her::Model::ORM.initialize_collection(self.to_s.downcase.to_sym, collection_data)
      end # }}}

      # Return `true` if a resource was not saved yet
      def new? # {{{
        !@data.include?(:id)
      end # }}}

      # Fetch a specific resource based on an ID
      #
      # @example
      #   @user = User.find(1)
      #   # Fetched via GET "/users/1"
      def find(id, params={}) # {{{
        request(params.merge(:_method => :get, :_path => "#{@her_collection_path}/#{id}")) do |parsed_data|
          new(parsed_data[:data])
        end
      end # }}}

      # Fetch a collection of resources
      #
      # @example
      #   @users = User.all
      #   # Fetched via GET "/users"
      def all(params={}) # {{{
        request(params.merge(:_method => :get, :_path => "#{@her_collection_path}")) do |parsed_data|
          new_collection(parsed_data)
        end
      end # }}}

      # Create a resource and return it
      #
      # @example
      #   @user = User.create({ :fullname => "Tobias Fünke" })
      #   # Called via POST "/users/1"
      def create(params={}) # {{{
        resource = new(params)
        perform_hook(resource, :before, :create)
        perform_hook(resource, :before, :save)
        params = resource.instance_eval { @data }
        request(params.merge(:_method => :post, :_path => "#{@her_collection_path}")) do |parsed_data|
          resource.instance_eval do
            @data = parsed_data[:data]
          end
        end
        perform_hook(resource, :after, :save)
        perform_hook(resource, :after, :create)

        resource
      end # }}}

      # Save an existing resource and return it
      #
      # @example
      #   @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
      #   # Called via PUT "/users/1"
      def save_existing(id, params) # {{{
        resource = new(params.merge(:id => id))
        resource.save
      end # }}}

      # Save a resource
      #
      # @example Save a resource after fetching it
      #   @user = User.find(1)
      #   # Fetched via GET "/users/1"
      #   @user.fullname = "Tobias Fünke"
      #   @user.save
      #   # Called via PUT "/users/1"
      #
      # @example Save a new resource by creating it
      #   @user = User.new({ :fullname => "Tobias Fünke" })
      #   @user.save
      #   # Called via POST "/users"
      def save # {{{
        params = @data.dup
        resource = self
        if @data[:id]
          self.class.class_eval do
            perform_hook(resource, :before, :update)
            perform_hook(resource, :before, :save)
          end
          self.class.request(params.merge(:_method => :put, :_path => "#{self.class.collection_path}/#{id}")) do |parsed_data|
            @data = parsed_data[:data]
          end
          self.class.class_eval do
            perform_hook(resource, :after, :save)
            perform_hook(resource, :after, :update)
          end
          self
        else
          self.class.class_eval do
            perform_hook(resource, :before, :create)
            perform_hook(resource, :before, :save)
          end
          self.class.request(params.merge(:_method => :post, :_path => "#{self.class.collection_path}")) do |parsed_data|
            @data = parsed_data[:data]
          end
          self.class.class_eval do
            perform_hook(resource, :after, :save)
            perform_hook(resource, :after, :create)
          end
        end
        self
      end # }}}

      # Destroy a resource
      #
      # @example
      #   @user = User.find(1)
      #   @user.destroy
      #   # Called via DELETE "/users/1"
      def destroy # {{{
        params = @data.dup
        resource = self
        self.class.class_eval { perform_hook(resource, :before, :destroy) }
        self.class.request(params.merge(:_method => :delete, :_path => "#{self.class.collection_path}/#{id}")) do |parsed_data|
          @data = parsed_data[:data]
        end
        self.class.class_eval { perform_hook(resource, :after, :destroy) }
        self
      end # }}}

      # Destroy an existing resource
      #
      # @example
      #   User.destroy_existing(1)
      #   # Called via DELETE "/users/1"
      def destroy_existing(id) # {{{
        params = {}
        request(params.merge(:_method => :delete, :_path => "#{collection_path}/#{id}")) do |parsed_data|
          new(parsed_data[:data])
        end
      end # }}}
    end
  end
end