module JquerySortableTreeHelper
  # Ilya Zykin, zykin-ilya@ya.ru, Russia [Ivanovo, Saint Petersburg] 2009-2014
  # github.com/the-teacher
  TREE_RENDERERS = {
    tree: RenderTreeHelper,
    sortable: RenderSortableTreeHelper,
    expandable: RenderExpandableTreeHelper,
    nested_options: RenderNestedOptionsHelper,
    indented_options: RenderIndentedOptionsHelper
  }.freeze

  ###############################################
  # Common Base Methods
  ###############################################

  def define_class_of_elements_of tree
    case
      when tree.is_a?(ActiveRecord::Relation) then tree.name.to_s.underscore.downcase
      when tree.empty?                        then nil
      else tree.first.class.to_s.underscore.downcase
    end
  end

  def build_tree_html context, render_module, options = {}
    render_module::Render.new(context, options).render_node
  end

  ###############################################
  # Shortcuts
  ###############################################

  def just_tree(tree, options = {})
    build_server_tree(tree, { type: :tree }.merge!(options))
  end

  def base_data
    {
      model: params[:controller].singularize,
      title: 'title',
      max_levels: 5,
      parent_id: params[:parent_id],
      rebuild_url: send("rebuild_#{params[:controller]}_url"),
      url: url_for(controller: params[:controller], action: :show, id: ':id', format: :json)
    }
  end

  def space(height)
    content_tag(:div, ' '.html_safe, style: "height: #{height}px;")
  end

  def fake_node(options)
    OpenStruct.new(options[:title] => '', id: ':id', children: nil)
  end

  def sortable_tree(tree, options = {})
    space(20) +
    add_new_node_form(base_data.merge(options)) +
    content_tag(:ol, build_tree_html(self, TREE_RENDERERS[:sortable], base_options.merge(options).merge({ node: fake_node(options) })), class: 'fake-node hidden', style: 'display: none;') +
    content_tag(:ol, build_server_tree(tree, { type: :sortable }.merge!(options)),
                class: 'sortable_tree',
                data: base_data.merge(options.slice(:parent_id, :model, :rebuild_url, :title, :max_levels))
    )
  end

  def form_for_options(options)
    {
      html: { id: "new-#{options[:model]}-form" },
      url: send("#{options[:model].pluralize}_path", format: :json),
      method: :post,
      remote: true
    }
  end

  def add_new_node_form(options)
    capture do
      form_for(options[:model].to_sym, form_for_options(options)) do |f|
        title_field = f.text_field options[:title].to_sym, required: true, class: 'form-control', placeholder: I18n.t('sortable_tree.title_of_new_node', default: "The #{options[:title]} of new #{options[:model]}")
        concat content_tag(:div, title_field, class: 'form-group')
        concat f.hidden_field :parent_id, value: options[:parent_id]
      end
    end
  end

  def nested_options(tree, options = {})
    build_server_tree(tree, { type: :nested_options }.merge!(options))
  end

  def indented_options(tree, options = {})
    build_server_tree(tree, { type: :indented_options }.merge!(options))
  end

  def expandable_tree(tree, options = {})
    build_server_tree(tree, { type: :expandable }.merge!(options))
  end

  ###############################################
  # Server Side Render Tree Helper
  ###############################################

  def base_options
    {
      id: :id,
      title: :title,
      node: nil,
      type: :tree,
      root: false,
      level: 0,
      namespace: [],
      controller: params[:controller]
    }
  end

  def build_children(tree, opts)
    children = (opts[:boost][opts[:node].id.to_i] || [])
    children.reduce('') { |r, elem| r + build_server_tree(tree, opts.merge(node: elem, root: false, level: opts[:level].next)) }
  end

  def roots(tree)
    min_parent_id = tree.map(&:parent_id).compact.min
    tree.select { |e| e.parent_id == min_parent_id }
  end

  def merge_base_options(tree, options)
    opts = base_options.merge(options)
    opts[:namespace] = [*opts[:namespace]]
    opts[:render_module] ||= TREE_RENDERERS[opts[:type]]
    opts[:klass] ||= define_class_of_elements_of(tree)
    opts[:boost] ||= tree.group_by { |item| item.parent_id.to_i }
    opts
  end

  def build_server_tree(tree, options = {})
    tree = [*tree]
    opts = merge_base_options(tree, options)

    if opts[:node]
      raw build_tree_html(self, opts[:render_module], opts.merge(children: build_children(tree, opts)))
    else
      raw (roots(tree) || []).reduce('') { |r, root| r + build_server_tree(tree, opts.merge(node: root, root: true, level: opts[:level].next)) }
    end
  end
end