module CDQ

  class CDQContextManager

    include CDQ::Deprecation

    BACKGROUND_SAVE_NOTIFICATION = 'com.infinitered.cdq.context.background_save_completed'
    DID_FINISH_IMPORT_NOTIFICATION = 'com.infinitered.cdq.context.did_finish_import'

    def initialize(opts = {})
      @store_manager = opts[:store_manager]
    end

    def dealloc
      NSNotificationCenter.defaultCenter.removeObserver(self) if @observed_context
      super
    end

    # Push a new context onto the stack for the current thread, making that context the
    # default. If a block is supplied, push for the duration of the block and then
    # return to the previous state.
    #
    def push(context, options = {}, &block)
      @has_been_set_up = true

      if !context.is_a?(NSManagedObjectContext)
        context = create(context, options)
      elsif options[:named]
        assign_name(options[:named], context)
      end

      if block_given?
        save_stack do
          context = push_to_stack(context)
          block.call
          context
        end
      else
        push_to_stack(context)
      end
    end

    # Pop the top context off the stack.  If a block is supplied, pop for the
    # duration of the block and then return to the previous state.
    #
    def pop(&block)
      if block_given?
        save_stack do
          rval = pop_from_stack
          block.call
          rval
        end
      else
        pop_from_stack
      end
    end

    # The current context at the top of the stack.
    #
    def current
      if stack.empty? && !@has_been_set_up
        push(NSMainQueueConcurrencyType)
      end
      stack.last
    end

    # An array of all contexts, from bottom to top of the stack.
    #
    def all
      stack.dup
    end

    # Remove all contexts.
    #
    def reset!
      self.stack = []
    end

    # Create a new context by type, setting upstream to the topmost context if available,
    # or to the persistent store coordinator if not.  Return the context but do NOT push it
    # onto the stack.
    #
    # Options:
    #
    #   :named - Assign the context a name, making it available as cdq.contexts.<name>.  The
    #     name is permanent, and should only be used for contexts that are intended to be global,
    #     since the object will never get released.
    #
    def create(concurrency_type, options = {}, &block)
      @has_been_set_up = true

      case concurrency_type
      when :main
        context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSMainQueueConcurrencyType)
        options[:named] = :main unless options.has_key?(:named)
      when :private_queue, :private
        context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSPrivateQueueConcurrencyType)
      when :root
        context = NSManagedObjectContext.alloc.initWithConcurrencyType(NSPrivateQueueConcurrencyType)
        options[:named] = :root unless options.has_key?(:named)
      else
        context = NSManagedObjectContext.alloc.initWithConcurrencyType(concurrency_type)
      end

      if stack.empty?
        if @store_manager.invalid?
          raise "store coordinator not found. Cannot create the first context without one."
        else
          context.mergePolicy = NSMergePolicy.alloc.initWithMergeType(NSMergeByPropertyObjectTrumpMergePolicyType)
          context.performBlockAndWait ->{
            coordinator = @store_manager.current
            context.persistentStoreCoordinator = coordinator

            NSNotificationCenter.defaultCenter.addObserver(self, selector:"did_finish_import:", name:NSPersistentStoreDidImportUbiquitousContentChangesNotification, object:nil)
            @observed_context = context
          }
        end
      else
        context.parentContext = stack.last
      end

      if options[:named]
        assign_name(options[:named], context)
      end
      context
    end

    # Save all passed contexts in order.  If none are supplied, save all
    # contexts in the stack, starting with the current and working down.  If
    # you pass a symbol instead of a context, it will look up context with
    # that name.
    #
    # Options:
    #
    #   always_wait: If true, force use of performBlockAndWait for synchronous
    #     saves.  By default, private queue saves are performed asynchronously.
    #     Main queue saves are always synchronous if performed from the main
    #     queue.
    #
    def save(*contexts_and_options)

      if contexts_and_options.last.is_a? Hash
        options = contexts_and_options.pop
      else
        options = {}
      end

      if contexts_and_options.empty?
        contexts = stack.reverse
      else
        # resolve named contexts
        contexts = contexts_and_options.map do |c|
          if c.is_a? Symbol
            send(c)
          else
            c
          end
        end
      end

      set_timestamps
      always_wait = options[:always_wait]
      contexts.each do |context|
        if context.concurrencyType == NSMainQueueConcurrencyType && NSThread.isMainThread
          with_error_object do |error|
            context.save(error)
          end
        elsif always_wait
          context.performBlockAndWait( -> {

            with_error_object do |error|
              context.save(error)
            end

          } )
        elsif context.concurrencyType == NSPrivateQueueConcurrencyType
          task_id = UIApplication.sharedApplication.beginBackgroundTaskWithExpirationHandler( -> { NSLog "CDQ Save Timed Out" } )

          if task_id == UIBackgroundTaskInvalid
            context.performBlockAndWait( -> {

              with_error_object do |error|
                context.save(error)
              end

            } )
          else
            context.performBlock( -> {

              # Let the application know we're doing something important
              with_error_object do |error|
                context.save(error)
              end

              UIApplication.sharedApplication.endBackgroundTask(task_id)

              NSNotificationCenter.defaultCenter.postNotificationName(BACKGROUND_SAVE_NOTIFICATION, object: context)

            } )
          end
        else
          with_error_object do |error|
            context.save(error)
          end
        end
      end
      true
    end

    # Run the supplied block in a new context with a private queue.  Once the
    # block exits, the context will be forgotten, so any changes made must be
    # saved within the block.
    #
    # Note that the CDQ context stack, which is used when deciding what to save
    # with `cdq.save` is stored per-thread, so the stack inside the block is
    # different from the stack outside the block. If you push any more contexts
    # inside, they will also disappear when the thread terminates.
    #
    # The thread is also unique.  If you call `background` multiple times, it will
    # be a different thread each time with no persisted state.
    #
    # Options:
    #   wait: If true, run the block synchronously
    #
    def background(options = {}, &block)
      # Create a new private queue context with the main context as its parent
      context = create(NSPrivateQueueConcurrencyType)

      on(context, options) do
        push(context, {}, &block)
      end

    end

    # Run a block on the supplied context using performBlock.  If context is a
    # symbol, it will look up the corresponding named context and use that
    # instead.
    #
    # Options:
    #   wait: If true, run the block synchronously
    #
    def on(context, options = {}, &block)

      if context.is_a? Symbol
        context = send(context)
      end

      if options[:wait]
        context.performBlockAndWait(block)
      else
        context.performBlock(block)
      end
    end

    def did_finish_import(notification)
      @observed_context.performBlockAndWait ->{
        @observed_context.mergeChangesFromContextDidSaveNotification(notification)
        NSNotificationCenter.defaultCenter.postNotificationName(DID_FINISH_IMPORT_NOTIFICATION, object:self, userInfo:{context: @observed_context})
      }
    end


    private

    def assign_name(name, context)
      if respond_to?(name)
        raise "Cannot name a context '#{name}': conflicts with existing method"
      end
      self.class.send(:define_method, name) do
        context
      end
    end

    def push_to_stack(value)
      lstack = stack
      lstack << value
      self.stack = lstack
      value
    end

    def pop_from_stack
      lstack = stack
      value = lstack.pop
      self.stack = lstack
      value
    end

    def save_stack(&block)
      begin
        saved_stack = all
        block.call
      ensure
        self.stack = saved_stack
      end
    end

    def stack
      Thread.current[:"cdq.context.stack.#{object_id}"] || []
    end

    def stack=(value)
      Thread.current[:"cdq.context.stack.#{object_id}"] = value
    end

    def with_error_object(default = nil, &block)
      error = Pointer.new(:object)
      result = block.call(error)
      if error[0]
        print_error("Error while fetching", error[0])
        raise "Error while fetching: #{error[0].debugDescription}"
      end
      result || default
    end

    def print_error(message, error, indent = "")
      puts indent + message + error.localizedDescription
      if error.userInfo['reason']
        puts indent + error.userInfo['reason']
      end
      if error.userInfo['metadata']
        error.userInfo['metadata'].each do |key, value|
          puts indent + "#{key}: #{value}"
        end
      end
      if !error.userInfo[NSDetailedErrorsKey].nil?
        error.userInfo[NSDetailedErrorsKey].each do |key, value|
          if key.instance_of? NSError
            print_error("Sub-Error: ", key, indent + "   ")
          else
            puts indent + "#{key}: #{value}"
          end
        end
      end
    end

    def set_timestamps
      now = Time.now

      current.insertedObjects.allObjects.each do |e|
        e.created_at = now if e.respond_to? :created_at=
        e.updated_at = now if e.respond_to? :updated_at=
      end

      current.updatedObjects.allObjects.each do |e|
        e.updated_at = now if e.respond_to? :updated_at=
      end

    end

  end

end
