require 'hyalite/transaction'
require 'hyalite/adler32'
require 'hyalite/try'
require 'hyalite/transaction'
require 'hyalite/element'
require 'hyalite/component'
require 'hyalite/instance_handles'
require 'hyalite/updates'
require 'hyalite/composite_component'

module Hyalite
  module Mount
    ID_ATTR_NAME = 'data-hyalite-id'
    CHECKSUM_ATTR_NAME = 'data-react-checksum'

    @instances_by_root_id = {}
    @containers_by_root_id = {}
    @is_batching_updates = false

    class << self
      def instances_by_root_id(root_id)
        @instances_by_root_id[root_id]
      end

      def render_subtree_into_container(next_element, container, &block)
        next_wrapped_element = ElementObject.new(TopLevelWrapper, nil, nil, nil, next_element)
        prev_component = @instances_by_root_id[root_id(container)]
        if prev_component
          prev_wrapped_element = prev_component.current_element
          prev_element = prev_wrapped_element.props;
          if Reconciler.should_update_component(prev_element, next_element)
            return update_root_component(
              prev_component,
              next_wrapped_element,
              &block
            ).rendered_component.public_instance
          else
            unmount_component_at_node(container)
          end
        end

        root_element = root_element_in_container(container)
        container_has_markup = root_element && is_rendered(root_element)
        should_reuse_markup = container_has_markup && prev_component.nil?

        component = render_new_root_component(
          next_wrapped_element,
          container,
          should_reuse_markup
        ).rendered_component.public_instance

        if block_given?
          yield component
        end

        component
      end

      def is_rendered(node)
        return false unless node.element?

        id = node_id(node)
        id ? id[0] == Hyalite::Reconciler::SEPARATOR : false
      end

      def render_new_root_component(next_element, container, should_reuse_markup)
        component_instance = Hyalite.instantiate_component(next_element)
        root_id = register_component(component_instance, container)

        Hyalite.updates.batched_updates do
          mount_component_into_node(component_instance, root_id, container, should_reuse_markup)
        end

        component_instance
      end

      def unmount_component_at_node(container)
        root_id = root_id(container)
        component = @instances_by_root_id[root_id]
        unless component
          container_has_non_root_child = has_non_root_child(container)

          container_id = internal_id(container)
          container_id &&
          container_id == InstanceHandles.root_id_from_node_id(container_id)
          return false
        end

        Hyalite.updates.batched_updates do
          unmount_component_from_node(component, container)
        end

        @instances_by_root_id.delete(root_id)
        @containers_by_root_id.delete(root_id)
        true
      end

      def has_non_root_child(node)
        root_id = root_id(container)
        root_id ? root_id != InstanceHandles.root_id_from_node_id(root_id) : false
      end

      def unmount_component_from_node(instance, container)
        Reconciler.unmount_component(instance)

        while container.children.length > 0
          container.children.last.remove
        end
      end

      def register_component(next_component, container)
        #ReactBrowserEventEmitter.ensureScrollValueMonitoring();

        root_id = register_container(container)
        @instances_by_root_id[root_id] = next_component;
        root_id
      end

      def register_container(container)
        root_id = root_id(container)
        if root_id
          root_id = InstanceHandles.root_id_from_node_id(root_id)
        end

        unless root_id
          root_id = InstanceHandles.create_root_id
        end

        @containers_by_root_id[root_id] = container
        root_id
      end

      def mount_component_into_node(component_instance, root_id, container, should_reuse_markup)
        Hyalite.updates.reconcile_transaction.perform do |transaction|
          markup = Reconciler.mount_component(component_instance, root_id, transaction.mount_ready, {})
          component_instance.rendered_component.top_level_wrapper = component_instance
          mount_image_into_node(markup, container, should_reuse_markup)
        end
      end

      def mount_image_into_node(markup, container, should_reuse_markup)
        if should_reuse_markup
          root_element = root_element_in_container(container)
          checksum = Adler32.checksum markup
          checksum_attr = root_element.attributes[CHECKSUM_ATTR_NAME]
          if checksum == checksum_attr
            return
          end

          root_element.attributes.remove(CHECKSUM_ATTR_NAME)
          root_element.attributes[CHECKSUM_ATTR_NAME] = checksum
        end

        container.inner_dom = markup
      end

      def root_element_in_container(container)
        if container.document?
          container
        else
          container.children.first
        end
      end

      def root_id(container)
        root_element = root_element_in_container(container)
        root_element && node_id(root_element)
      end

      def node_cache
        @node_cache ||= {}
      end

      def purge_id(id)
        @node_cache.delete(id)
      end

      def node_id(node)
        id = internal_id(node)
        if id
          if node_cache.has_key?(id)
            cached = node_cache[id]
            if cached != node
              #raise "Mount: Two valid but unequal nodes with the same `#{ID_ATTR_NAME}`: #{id}"
              node_cache[id] = node
            end
          else
            node_cache[id] = node
          end
        end

        id
      end

      def internal_id(node)
        if node.element?
          node.attributes[ID_ATTR_NAME]
        end
      end

      # cf. ReactMount#findReactContainerForID
      def container_for_id(id)
        root_id = InstanceHandles.root_id_from_node_id(id)
        @containers_by_root_id[root_id]
      end

      def node(id)
        node =  node_cache[id]
        unless node && is_valid(node, id)
          root_id = InstanceHandles.root_id_from_node_id(id)
          node = node_cache[id] = find_component_root(@containers_by_root_id[root_id], id)
        end
        node
      end

      def is_valid(node, id)
        if node
          container = @containers_by_root_id[id]
          if container && contains_node(container, node)
            return true
          end
        end

        false
      end

      def find_component_root(ancestor_node, target_id)
        deepest_ancestor = find_deepest_cached_ancestor(target_id) || ancestor_node

        first_children = [ deepest_ancestor.children.first ]

        while first_children.length > 0
          child = first_children.shift

          while child
            child_id = node_id(child)
            if child_id
              if target_id == child_id
                return child
              elsif InstanceHandles.is_ancestor_id_of(child_id, target_id)
                first_children = [ child.children.first ]
              end
            else
              first_children.push(child.children.first)
            end

            child = child.next_sibling
          end
        end

        raise "can't find component_root ancestor_node: #{ancestor_node}, target_id: #{target_id}"
      end

      def find_deepest_cached_ancestor(target_id)
        deepest_node_so_far = nil
        InstanceHandles.traverse_ancestors(target_id) do |ancestor_id|
          ancestor = node_cache[ancestor_id]
          if ancestor && is_valid(ancestor, ancestor_id)
            deepest_node_so_far = ancestor;
          else
            false
          end
        end

        deepest_node_so_far
      end

      def contains_node(outer_node, inner_node)
        case
        when outer_node.nil? || inner_node.nil?
          false
        when outer_node == inner_node
          true
        when outer_node.text?
          false
        else
          contains_node(outer_node, inner_node.parent)
        end
      end

      def find_first_hyalite_dom(node)
        while node && node.parent
          unless node.element? && node_id = internal_id(node)
            node = node.parent
            next
          end

          root_id = InstanceHandles.root_id_from_node_id(node_id)

          current = node
          loop do
            last_id = internal_id(current)
            return nil unless current = current.parent
            break if last_id == root_id
          end

          return node if current == @containers_by_root_id[root_id]

          node = node.parent
        end

        nil
      end

      def update_root_component(prev_component, next_element, &callback)
        UpdateQueue.enqueue_element_internal(prev_component, next_element)
        if block_given?
          UpdateQueue.enqueue_callback_internal(prev_component, &callback)
        end

        prev_component
      end
    end
  end
end
