# @provides MotionKit::BaseLayout
# @requires MotionKit::BaseLayoutClassMethods
module MotionKit
  # Abstract base class, responsible for "registration" of layout classes with
  # the class `targets` method.
  #
  # Very few methods are defined on BaseLayout, and any unknown methods are
  # delegated to the 'apply' method, which accepts a method name, arguments, and
  # an optional block to set the new context.
  #
  # The TreeLayout subclass defines methods that are appropriate for adding and
  # removing views to a view hierarchy.
  class BaseLayout
    # Class methods reside in base_layout_class_methods.rb
    extend BaseLayoutClassMethods

    attr :parent

    def initialize(args={})
      # @layout is the object we look in for style methods
      @layout = nil
      # the Layout object that implements custom style methods. Leave this as nil
      # in the initializer.
      @layout_delegate = nil
      @layout_state = :initial
      # You can set a root view by using .new(root: some_view)
      # Explicit roots will not have a strong reference from
      # MotionKit, so retain one yourself from your controller
      # or other view to prevent deallocation.
      @preset_root = args[:root]
    end

    def set_parent_layout(layout)
      @layout = WeakRef.new(layout)
    end

    def parent_layout
      @layout || self
    end

    def is_parent_layout?
      @layout.nil? || @layout == self
    end

    def has_context?
      if is_parent_layout?
        !!@context
      else
        parent_layout.has_context?
      end
    end

    def target
      if is_parent_layout?
        # only the "root layout" instance is allowed to change the context.
        # if there isn't a context set, try and create a root instance; this
        # will fail if we're not in a state that allows the root to be created
        @context ||= create_default_root_context
      else
        # child layouts get the context from the root layout
        parent_layout.target
      end
    end
    def v ; target ; end

    # Runs a block of code with a new object as the 'context'. Methods from the
    # Layout classes are applied to this target object, and missing methods are
    # delegated to a new Layout instance that is created based on the new
    # context.
    #
    # This method is part of the public API, you can pass in any object to have
    # it become the 'context'.
    #
    # Example:
    #     def table_view_style
    #       content = target.contentView
    #       if content
    #         context(content) do
    #           background_color UIColor.clearColor
    #         end
    #       end
    #
    #       # usually you use 'context' automatically via method_missing, by
    #       # passing a block to a method that returns an object. That object becomes
    #       # the new context.
    #       layer do
    #         # target is now a CALayer, and methods are delegated to CALayerHelpers
    #         corner_radius 5
    #       end
    #     end
    def context(new_target, &block)
      return new_target unless block
      # this little line is incredibly important; the context is only set on
      # the top-level Layout object.

      # mp "MOTIONKIT CONTEXT is #{new_target} meta: #{new_target.motion_kit_meta}"
      return parent_layout.context(new_target, &block) unless is_parent_layout?

      if new_target.is_a?(Symbol)
        new_target = self.get_view(new_target)
      end

      context_was, parent_was, delegate_was = @context, @parent, @layout_delegate

      prev_should_run = @should_run_deferred
      if @should_run_deferred.nil?
        @should_run_deferred = true
      else
        @should_run_deferred = false
      end
      @parent = MK::Parent.new(context_was)
      @context = new_target
      @context.motion_kit_meta[:delegate] ||= Layout.layout_for(@context.class)
      @layout_delegate = @context.motion_kit_meta[:delegate]
      if @layout_delegate
        @layout_delegate.set_parent_layout(parent_layout)
      end
      yield
      @layout_delegate, @context, @parent = delegate_was, context_was, parent_was
      if @should_run_deferred
        run_deferred(new_target)
      end
      @should_run_deferred = prev_should_run

      new_target
    end

    # Blocks passed to `deferred` are run at the end of a "session", usually
    # after a call to Layout#layout.
    def deferred(context=nil, &block)
      context ||= @context
      parent_layout.add_deferred_block(context, &block)
      return self
    end

    # Only intended for private use
    def add_deferred_block(context, &block)
      context ||= @context
      if context.nil? && @assign_root
        context ||= self.create_default_root_context
      end
      raise InvalidDeferredError.new('deferred must be run inside of a context') unless context
      raise ArgumentError.new('Block required') unless block

      self.deferred_blocks << [context, block]
    end

    # Only intended for private use
    def deferred_blocks
      @deferred_blocks ||= []
    end

    # Only intended for private use
    def run_deferred(top_level_context)
      deferred_blocks = self.deferred_blocks
      @deferred_blocks = nil

      deferred_blocks.each do |target, block|
        context(target, &block)
      end

      if @deferred_blocks
        run_deferred(top_level_context)
      end
    end

    # @example
    #     def login_button_style
    #       frame [[0, 0], [100, 20]]
    #       title 'Login'
    #     end
    #
    # Methods that style the view start out as missing methods. This just calls
    # 'apply', which searches for the method in the delegate
    # (`@layout_delegate`) or using inspection (`respond_to?(:setFoo)`).
    def method_missing(method_name, *args, &block)
      self.apply(method_name, *args, &block)
    end

    def objc_version(method_name, args)
      if method_name.count(':') > 1
        objc_method_name = method_name
      elsif args.length == 2 && args[1].is_a?(Hash) && !args[1].empty?
        objc_method_name = "#{method_name}:#{args[1].keys.join(':')}:"
      else
        return nil, nil
      end

      if args[1].is_a?(Hash)
        objc_method_args = [args[0]].concat args[1].values
      else
        objc_method_args = args
      end

      return objc_method_name, objc_method_args
    end

    def ruby_version(method_name, args)
      if method_name.count(':') > 1
        parts = method_name.split(':')
        ruby_method_name = parts.first
        if args[1].is_a?(Hash)
          ruby_method_args = args
        else
          keys = parts[1..-1].map(&:to_sym)
          ruby_method_args = [args[0]] << Hash[keys.zip args[1..-1]]
        end
      elsif method_name.count(':') == 1
        ruby_method_name = method_name.split(':').first
        ruby_method_args = args
      else
        ruby_method_name = method_name
        ruby_method_args = args
      end

      return ruby_method_name, ruby_method_args
    end

    # Tries to call the setter (`foo 'value'` => `view.setFoo('value')`), or
    # assignment method (`foo 'value'` => `view.foo = 'value'`), or if a block
    # is given, then the object returned by 'method_name' is assigned as the new
    # context, and the block is executed.
    #
    # You can call this method directly, but usually it is called via
    # method_missing.
    def apply(method_name, *args, &block)
      method_name = method_name.to_s
      raise ApplyError.new("Cannot apply #{method_name.inspect} to instance of #{target.class.name}") if method_name.length == 0

      # if there is no target, than we should raise the NoMethodError; someone
      # called a method on the layout directly.
      begin
        target = self.target
      rescue NoContextError => e
        raise NoMethodError.new("undefined method `#{method_name}' for #{self}:#{self.class}", method_name)
      end

      objc_method_name, objc_method_args = objc_version(method_name, args)
      ruby_method_name, ruby_method_args = ruby_version(method_name, args)

      @layout_delegate ||= Layout.layout_for(target.class)
      if @layout_delegate
        @layout_delegate.set_parent_layout(parent_layout)
        if objc_method_name && @layout_delegate.respond_to?(objc_method_name)
          return @layout_delegate.send(objc_method_name, *objc_method_args, &block)
        elsif @layout_delegate.respond_to?(ruby_method_name)
          return @layout_delegate.send(ruby_method_name, *ruby_method_args, &block)
        end
      end

      if block
        apply_with_context(method_name, *args, &block)
      else
        apply_with_target(method_name, *args, &block)
      end
    end

    def apply_with_context(method_name, *args, &block)
      objc_method_name, objc_method_args = objc_version(method_name, args)
      ruby_method_name, ruby_method_args = ruby_version(method_name, args)

      if objc_method_name && target.respond_to?(objc_method_name)
        new_context = target.send(objc_method_name, *objc_method_args)
        self.context(new_context, &block)
      elsif target.respond_to?(ruby_method_name)
        new_context = target.send(ruby_method_name, *ruby_method_args)
        self.context(new_context, &block)
      elsif ruby_method_name.include?('_')
        objc_name = MotionKit.objective_c_method_name(ruby_method_name)
        self.apply(objc_name, *args, &block)
      else
        raise ApplyError.new("Cannot apply #{ruby_method_name.inspect} to instance of #{target.class.name}")
      end
    end

    def apply_with_target(method_name, *args, &block)
      objc_method_name, objc_method_args = objc_version(method_name, args)
      ruby_method_name, ruby_method_args = ruby_version(method_name, args)

      objc_setter = objc_method_name && MotionKit.setter(objc_method_name)
      setter = MotionKit.setter(ruby_method_name)
      assign = "#{ruby_method_name}="

      # The order is important here.
      # - unchanged method name if no args are passed (e.g. `layer`)
      # - setter (`setLayer(val)`)
      # - assign (`layer=val`)
      # - unchanged method name *again*, because many Ruby classes provide a
      #   combined getter/setter (`layer(val)`)
      # - lastly, try again after converting to camelCase
      if objc_method_name && target.respond_to?(objc_method_name)
        target.send(objc_method_name, *objc_method_args, &block)
      elsif args.empty? && target.respond_to?(ruby_method_name)
        target.send(ruby_method_name, *ruby_method_args, &block)
      elsif objc_setter && target.respond_to?(objc_setter)
        target.send(objc_setter, *objc_method_args, &block)
      elsif target.respond_to?(setter)
        target.send(setter, *args, &block)
      elsif target.respond_to?(assign)
        target.send(assign, *args, &block)
      elsif target.respond_to?(ruby_method_name)
        target.send(ruby_method_name, *ruby_method_args, &block)
      # UIAppearance classes are a whole OTHER thing; they never return 'true'
      elsif target.is_a?(MotionKit.appearance_class)
        target.send(setter, *args, &block)
      # Finally, try again with camel case if there's an underscore.
      elsif method_name.include?('_')
        objc_name = MotionKit.objective_c_method_name(method_name)
        self.apply(objc_name, *args, &block)
      else
        target.send(setter, *args, &block)
      end
    end

    class << self

      # Prevents infinite loops when methods that are defined on Object/Kernel
      # are not properly delegated to the target.
      def delegate_method_fix(method_name)
        running_name = "motion_kit_is_calling_#{method_name}"
        define_method(method_name) do |*args, &block|
          if target.motion_kit_meta[running_name]
            if block
              apply_with_context(method_name, *args, &block)
            else
              apply_with_target(method_name, *args)
            end
          else
            target.motion_kit_meta[running_name] = true
            retval = apply(method_name, *args, &block)
            target.motion_kit_meta[running_name] = false
            return retval
          end
        end
      end

    end

  protected

    def preset_root
      # Set in the initializer
      # TreeLayout.new(root: some_view)
      @preset_root
    end

  end



end
