module CustomLandingPage
  class Denormalizer

    def initialize(link_resolvers: {})
      @link_resolvers = link_resolvers
    end

    def to_tree(normalized_data, root:)
      root = normalized_data[root]

      deep_map(root) { |k, v|
        case v
        when Hash
          type, id = v.values_at("type", "id")

          new_v =
            if type.nil?
              # Not a link
              v
            elsif id.nil?
              # Looks like link, but no ID. That's an error.
              raise ArgumentError.new("Invalid link: #{v.inspect} has a 'type' key but no 'id'")
            else
              # Is a link
              resolve_link(type, id, normalized_data)
            end

          [k, new_v]
        else
          [k, v]
        end
      }
    end

    # Recursively walks through nested hash and performs `map` operation.
    #
    # The tree is traversed in pre-order manner.
    #
    # In each node, calls the block with two arguments: key and value.
    # The block needs to return a tuple of [key, value].
    #
    # Example (double all values):
    #
    # deep_map(a: { b: { c: 1}, d: [{ e: 1, f: 2 }]}) { |k, v|
    #   [k, v * 2]
    # }
    #
    #
    # Example (stringify keys):
    #
    # deep_map(a: 1, b: 2) { |k, v|
    #   [k.to_s, v]
    # }
    #
    # Unlike Ruby's Hash#map, this method returns a Hash, not an Array.
    #
    def deep_map(obj, &block)
      case obj
      when Hash
        obj.map { |k, v|
          deep_map(block.call(k, v), &block)
        }.to_h
      when Array
        obj.map { |x| deep_map(x, &block) }
      else
        obj
      end
    end

    def find_link(type, id, normalized_data)
      normalized_data[type].find { |item| item["id"] == id }
    end

    private

    def resolve_link(type, id, normalized_data)
      if @link_resolvers[type].respond_to? :call
        @link_resolvers[type].call(type, id, normalized_data)
      else
        find_link(type, id, normalized_data)
      end
    end
  end
end
