module DataMapper
  module Adapters

    class ParseAdapter < AbstractAdapter
      include Parse::Conditions
      include Query::Conditions

      HOST              = "https://api.parse.com"
      VERSION           = "1"
      APP_ID_HEADER     = "X-Parse-Application-Id"
      API_KEY_HEADER    = "X-Parse-REST-API-Key"
      MASTER_KEY_HEADER = "X-Parse-Master-Key"

      attr_reader :engine

      def initialize(name, options)
        super
        master = @options[:master].nil? ? false : @options[:master]
        @engine = Parse::Engine.new @options[:app_id], @options[:api_key], master
      end

      def create(resources)
        resources.each do |resource|
          params        = attributes_as_fields(resource.attributes(:property)).except("objectId", "createdAt", "updatedAt")
          model         = resource.model
          storage_name  = model.storage_name
          result        = engine.create storage_name, params

          initialize_serial resource, result["objectId"]
          resource.created_at = resource.updated_at = result["createdAt"].to_datetime
        end.size
      end

      def read(query)
        model         = query.model
        params        = parse_params_for(query)
        storage_name  = model.storage_name
        response      = engine.read storage_name, params

        response["results"]
      end

      # Read the "count" from Parse
      # This is Parse-only
      #
      # @param [Query] query
      #   the query to match resources in the datastore
      #
      # @return [Integer]
      #   the number of records that match the query
      #
      # @api semipublic
      def read_count(query)
        model           = query.model
        params          = parse_params_for(query)
        params[:count]  = 1
        params[:limit]  = 0
        storage_name    = model.storage_name
        response        = engine.read storage_name, params

        response["count"]
      end

      # Login, which is Parse-only
      #
      # @param [String] username
      #   the username
      # @param [String] password
      #   the password
      #
      # @return [Hash]
      #   the user information
      #
      # @api semipublic
      def sign_in(username, password)
        engine.sign_in username, password
      end

      # Request a password reset email
      # Parse-only
      #
      # @param [String] email
      #   the email address
      #
      # @return [Hash]
      #   a empty Hash
      def request_password_reset(email)
        engine.request_password_reset email
      end

      # Upload a file
      # Parse-only
      #
      # @param [String] filename
      #   the filename
      #
      # @param [String] content
      #   the content
      #
      # @param [String] content_type
      #   the content type
      #
      # @return [Hash]
      #   the uploaded file information
      def upload_file(filename, content, content_type)
        engine.upload_file filename, content, content_type
      end

      def delete(resources)
        resources.each do |resource|
          storage_name = resource.model.storage_name

          engine.delete storage_name, resource.id
        end.size
      end

      def update(attributes, resources)
        resources.each do |resource|
          params        = attributes_as_fields(attributes).except("createdAt", "updatedAt")
          storage_name  = resource.model.storage_name

          engine.update storage_name, resource.id, params
        end.size
      end

      private
      def parse_params_for(query)
        result = { :limit => parse_limit_for(query) }
        if conditions = parse_conditions_for(query)
          result[:where] = conditions.to_json
        end
        if (offset = parse_offset_for(query)) > 0
          result[:skip] = offset
        end
        if orders = parse_orders_for(query)
          result[:order] = orders
        end
        result
      end

      def parse_orders_for(query)
        orders = query.order
        return nil unless orders

        # cannot use objectId as order field on Parse
        orders = orders.reject { |order| order.target.field == "objectId" }.map do |order|
          field = order.target.field
          order.operator == :desc ? "-" + field : field
        end.join(",")

        orders.blank? ? nil : orders
      end

      def parse_offset_for(query)
        query.offset
      end

      def parse_limit_for(query)
        limit = query.limit || 1000
        raise "Parse limit: only number from 0 to 1000 is valid" unless (0..1000).include?(limit)
        limit 
      end

      def parse_conditions_for(query)
        conditions  = query.conditions
        return nil if conditions.blank?

        case conditions
        when NotOperation
          parse_query = Parse::Conditions::And.new
          feed_reversely(parse_query, conditions)
        when AndOperation
          parse_query = Parse::Conditions::And.new
          feed_directly(parse_query, conditions)
        when OrOperation
          parse_query = Parse::Conditions::Or.new
          feed_or(parse_query, conditions)
        end

        parse_query.build
      end

      def feed_for(parse_query, condition, comparison_class)
        subject = condition.subject
        case subject
        when DataMapper::Property
          comparison = comparison_class.new condition.value
          parse_query.add subject.field, comparison
        when DataMapper::Associations::OneToMany::Relationship
          child_key = condition.subject.child_key.first
          parse_query.add "objectId", comparison_class.new(condition.value.map { |resource| resource.send child_key.name })
        when DataMapper::Associations::ManyToOne::Relationship
          child_key = subject.child_key.first
          parse_query.add child_key.field, comparison_class.new(condition.foreign_key_mapping.value)
        else
          raise NotImplementedError, "Condition: #{condition}"
        end
      end

      def feed_reversely(parse_query, conditions)
        conditions.each do |condition|
          case condition
          when EqualToComparison              then feed_for(parse_query, condition, Ne)
          when GreaterThanComparison          then feed_for(parse_query, condition, Lte)
          when GreaterThanOrEqualToComparison then feed_for(parse_query, condition, Lt)
          when LessThanComparison             then feed_for(parse_query, condition, Gte)
          when LessThanOrEqualToComparison    then feed_for(parse_query, condition, Gt)
          when NotOperation                   then feed_directly(parse_query, condition)
          when AndOperation                   then feed_reversely(parse_query, condition)
          when InclusionComparison            then feed_for(parse_query, condition, Nin)
          else
            raise NotImplementedError
          end
        end
      end

      def feed_directly(parse_query, conditions)
        conditions.each do |condition|
          feed_with_condition parse_query, condition
        end
      end

      def feed_or(queries, conditions)
        conditions.each do |condition|
          parse_query = Parse::Conditions::And.new
          feed_with_condition parse_query, condition
          queries.add parse_query
        end
      end

      def feed_with_condition(parse_query, condition)
        case condition
        when RegexpComparison               then feed_for(parse_query, condition, Regex)
        when EqualToComparison              then feed_for(parse_query, condition, Eql)
        when GreaterThanComparison          then feed_for(parse_query, condition, Gt)
        when GreaterThanOrEqualToComparison then feed_for(parse_query, condition, Gte)
        when LessThanComparison             then feed_for(parse_query, condition, Lt)
        when LessThanOrEqualToComparison    then feed_for(parse_query, condition, Lte)
        when InclusionComparison            then feed_for(parse_query, condition, In)
        when NotOperation                   then feed_reversely(parse_query, condition)
        when AndOperation                   then feed_directly(parse_query, condition)
        else
          raise NotImplementedError
        end
      end

    end

    const_added(:ParseAdapter)
  end
end