# frozen_string_literal: true
# rbs_inline: enabled

module RbWasmVdom
  # Virtual DOM Fragment Definition
  class VFragment
    attr_reader :children #: Array[VNode | VFragment | String]

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs return: void
    def initialize(children = [])
      @children = children
    end
  end
end


module RbWasmVdom
  # Virtual DOM Node Definition
  class VNode
    attr_reader :tag      #: String
    attr_reader :props    #: Hash[String, String]
    attr_reader :children #: Array[VNode | VFragment | String]

    # @rbs tag: String
    # @rbs props: Hash[String, String]
    # @rbs children: Array[VNode | VFragment | String]
    # @rbs return: void
    def initialize(tag, props = {}, children = [])
      @tag = tag
      @props = props
      @children = children
    end
  end
end


module RbWasmVdom
  # Reactive State Management
  class ReactiveState
    # @rbs initial_data: Hash[Symbol, untyped]
    # @rbs &on_change: () -> void
    # @rbs return: void
    def initialize(initial_data, &on_change)
      @data = initial_data
      @on_change = on_change
    end

    # @rbs key: Symbol
    # @rbs return: untyped
    def [](key)
      @data[key]
    end

    # @rbs return: Array[Symbol]
    def keys
      @data.keys
    end

    # @rbs key: Symbol
    # @rbs value: untyped
    # @rbs return: void
    def []=(key, value)
      return if @data[key] == value

      @data[key] = value
      @on_change.call
    end
  end
end


module RbWasmVdom
  # HTML Template Parser
  class TemplateParser
    # @rbs return: void
    def self.setup_js_parser # rubocop:disable Metrics/MethodLength
      # Add a prefix to the function name to prevent global namespace pollution in JS
      JS.eval(<<~JS)
        window.__RbWasmVdom_parseHTMLToJSON = function(html) {
          const doc = new DOMParser().parseFromString(html, "text/html");
          function walk(node) {
            if (node.nodeType === 3) {
              const text = node.textContent.trim();
              return text ? text : null;
            }
            if (node.nodeType === 1) {
              const obj = { tag: node.tagName.toLowerCase(), props: {}, children: [] };
              for (let i = 0; i < node.attributes.length; i++) {
                obj.props[node.attributes[i].name] = node.attributes[i].value;
              }
              for (let i = 0; i < node.childNodes.length; i++) {
                const childRes = walk(node.childNodes[i]);
                if (childRes !== null) obj.children.push(childRes);
              }
              return obj;
            }
            return null;
          }
          const roots = [];
          for (let i = 0; i < doc.body.childNodes.length; i++) {
            const rootRes = walk(doc.body.childNodes[i]);
            if (rootRes !== null) roots.push(rootRes);
          }

          if (roots.length === 0) return "null";
          if (roots.length === 1) return JSON.stringify(roots[0]);

          return JSON.stringify({ fragment: true, children: roots });
        }
      JS
      @setup_done = true
    end

    # @rbs html_string: String
    # @rbs return: VNode | VFragment | String | nil
    def self.parse(html_string)
      setup_js_parser unless @setup_done
      json_str = JS.global.__RbWasmVdom_parseHTMLToJSON(html_string).to_s
      return nil if json_str == "null" || json_str.empty?

      build_ast(JSON.parse(json_str))
    end

    # @rbs data: Hash[String, untyped] | String
    # @rbs return: VNode | VFragment | String
    def self.build_ast(data)
      return data if data.is_a?(String)

      children = data["children"].map { |child| build_ast(child) }
      return VFragment.new(children) if data["fragment"]

      VNode.new(data["tag"], data["props"], children)
    end
  end
end


module RbWasmVdom
  module DomRenderer
    private

    # @rbs vnode: VNode | VFragment | String
    # @rbs return: untyped
    def create_element(vnode)
      document = JS.global[:document]
      return document.createTextNode(vnode) if vnode.is_a?(String)

      return create_fragment_element(document, vnode) if vnode.is_a?(VFragment)

      create_node_element(document, vnode)
    end

    # @rbs el: untyped
    # @rbs old_props: Hash[String, String]
    # @rbs new_props: Hash[String, String]
    # @rbs return: void
    def update_props(el, old_props, new_props)
      remove_old_props(el, old_props, new_props)
      apply_new_props(el, old_props, new_props)
    end

    # @rbs document: untyped
    # @rbs fragment: VFragment
    # @rbs return: untyped
    def create_fragment_element(document, fragment)
      element = document.createDocumentFragment
      append_children(element, fragment.children)
      element
    end

    # @rbs document: untyped
    # @rbs vnode: VNode
    # @rbs return: untyped
    def create_node_element(document, vnode)
      el = document.createElement(vnode.tag)
      update_props(el, {}, vnode.props)
      append_children(el, vnode.children)
      el
    end

    # @rbs el: untyped
    # @rbs children: Array[VNode | VFragment | String]
    # @rbs return: void
    def append_children(el, children)
      children.each do |child|
        el.appendChild(create_element(child))
      end
    end

    # @rbs el: untyped
    # @rbs old_props: Hash[String, String]
    # @rbs new_props: Hash[String, String]
    # @rbs return: void
    def remove_old_props(el, old_props, new_props)
      old_props.each_key do |key|
        el.removeAttribute(key) unless new_props.key?(key) || key.start_with?("@")
      end
    end

    # @rbs el: untyped
    # @rbs old_props: Hash[String, String]
    # @rbs new_props: Hash[String, String]
    # @rbs return: void
    def apply_new_props(el, old_props, new_props)
      new_props.each do |key, value|
        next if old_props[key] == value

        apply_prop(el, old_props, key, value)
      end
    end

    # @rbs el: untyped
    # @rbs old_props: Hash[String, String]
    # @rbs key: String
    # @rbs value: String
    # @rbs return: void
    def apply_prop(el, old_props, key, value)
      if key.start_with?("@")
        add_event_listener(el, old_props, key, value)
      elsif key == "value"
        update_value_prop(el, key, value)
      else
        el.setAttribute(key, value)
      end
    end

    # @rbs el: untyped
    # @rbs old_props: Hash[String, String]
    # @rbs key: String
    # @rbs value: String
    # @rbs return: void
    def add_event_listener(el, old_props, key, value)
      return if old_props.key?(key)

      event_name = key.sub(/^@/, "")
      el.addEventListener(event_name) do |e|
        @methods[value.to_sym].call(e, @state)
      end
    end

    # @rbs el: untyped
    # @rbs key: String
    # @rbs value: String
    # @rbs return: void
    def update_value_prop(el, key, value)
      el[:value] = value
      el.setAttribute(key, value)
    end
  end
end


module RbWasmVdom
  module Patcher
    include DomRenderer

    # @rbs parent_el: untyped
    # @rbs old_vnode: VNode | VFragment | String | nil
    # @rbs new_vnode: VNode | VFragment | String | nil
    # @rbs index: Integer
    # @rbs return: void
    def patch(parent_el, old_vnode, new_vnode, index)
      current_el = child_at(parent_el, index)

      return append_node(parent_el, new_vnode) if old_vnode.nil?
      return remove_node(parent_el, current_el) if new_vnode.nil?
      return replace_node(parent_el, current_el, new_vnode) if changed?(old_vnode, new_vnode)

      patch_existing_node(parent_el, current_el, old_vnode, new_vnode)
    end

    # @rbs parent_el: untyped
    # @rbs old_fragment: VFragment
    # @rbs new_fragment: VFragment
    # @rbs return: void
    def patch_fragment(parent_el, old_fragment, new_fragment)
      patch_children(parent_el, old_fragment.children, new_fragment.children)
    end

    private

    # @rbs parent_el: untyped
    # @rbs current_el: untyped
    # @rbs old_vnode: VNode | VFragment | String
    # @rbs new_vnode: VNode | VFragment | String
    # @rbs return: void
    def patch_existing_node(parent_el, current_el, old_vnode, new_vnode)
      if old_vnode.is_a?(VNode) && new_vnode.is_a?(VNode)
        patch_element(current_el, old_vnode, new_vnode)
      elsif old_vnode.is_a?(VFragment) && new_vnode.is_a?(VFragment)
        patch_fragment(parent_el, old_vnode, new_vnode)
      end
    end

    # @rbs parent_el: untyped
    # @rbs index: Integer
    # @rbs return: untyped
    def child_at(parent_el, index)
      parent_el[:childNodes].item(index)
    end

    # @rbs node1: VNode | VFragment | String
    # @rbs node2: VNode | VFragment | String
    # @rbs return: bool
    def changed?(node1, node2)
      return true if node1.class != node2.class
      return node1 != node2 if node1.is_a?(String)
      return false if node1.is_a?(VFragment)

      # @type var node1: VNode
      # @type var node2: VNode

      node1.tag != node2.tag
    end

    # @rbs parent_el: untyped
    # @rbs new_vnode: VNode | VFragment | String | nil
    # @rbs return: void
    def append_node(parent_el, new_vnode)
      parent_el.appendChild(create_element(new_vnode)) if new_vnode
    end

    # @rbs parent_el: untyped
    # @rbs current_el: untyped
    # @rbs return: void
    def remove_node(parent_el, current_el)
      parent_el.removeChild(current_el) unless current_el == JS::Null
    end

    # @rbs parent_el: untyped
    # @rbs current_el: untyped
    # @rbs new_vnode: VNode | VFragment | String
    # @rbs return: void
    def replace_node(parent_el, current_el, new_vnode)
      parent_el.replaceChild(create_element(new_vnode), current_el)
    end

    # @rbs current_el: untyped
    # @rbs old_vnode: VNode
    # @rbs new_vnode: VNode
    # @rbs return: void
    def patch_element(current_el, old_vnode, new_vnode)
      update_props(current_el, old_vnode.props, new_vnode.props)
      patch_children(current_el, old_vnode.children, new_vnode.children)
    end

    # @rbs current_el: untyped
    # @rbs old_children: Array[VNode | VFragment | String]
    # @rbs new_children: Array[VNode | VFragment | String]
    # @rbs return: void
    def patch_children(current_el, old_children, new_children)
      remove_extra_children(current_el, old_children, new_children)
      patch_common_children(current_el, old_children, new_children)

      return unless new_children.length > old_children.length

      append_missing_children(current_el, old_children, new_children)
    end

    # @rbs current_el: untyped
    # @rbs old_children: Array[VNode | VFragment | String]
    # @rbs new_children: Array[VNode | VFragment | String]
    # @rbs return: void
    def remove_extra_children(current_el, old_children, new_children)
      return unless old_children.length > new_children.length

      (old_children.length - 1).downto(new_children.length) do |i|
        child_to_remove = current_el[:childNodes].item(i)
        current_el.removeChild(child_to_remove) unless child_to_remove == JS::Null
      end
    end

    # @rbs current_el: untyped
    # @rbs old_children: Array[VNode | VFragment | String]
    # @rbs new_children: Array[VNode | VFragment | String]
    # @rbs return: void
    def patch_common_children(current_el, old_children, new_children)
      [old_children.length, new_children.length].min.times do |i|
        patch(current_el, old_children[i], new_children[i], i)
      end
    end

    # @rbs current_el: untyped
    # @rbs old_children: Array[VNode | VFragment | String]
    # @rbs new_children: Array[VNode | VFragment | String]
    # @rbs return: void
    def append_missing_children(current_el, old_children, new_children)
      (old_children.length...new_children.length).each do |i|
        current_el.appendChild(create_element(new_children[i]))
      end
    end
  end
end


module RbWasmVdom
  class Interpolator
    IDENTIFIER_PATTERN = /^[a-z_]\w*$/

    # @rbs state: ReactiveState
    def initialize(state)
      @state = state
    end

    # @rbs text: String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: String
    def call(text, locals = {})
      interpolate_text(text.to_s, locals)
    end

    private

    # @rbs text: String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: String
    def interpolate_text(text, locals)
      result = +""
      index = 0

      while index < text.length
        range = interpolation_range(text, index)
        return result << text[index..].to_s unless range

        result << text[index...range.begin].to_s
        result << interpolate_range(text, range, locals)

        index = range.end + 1
      end

      result
    end

    # @rbs text: String
    # @rbs index: Integer
    # @rbs return: Range[Integer]?
    def interpolation_range(text, index)
      start_index = text.index("{{", index)
      return nil unless start_index

      end_index = text.index("}}", start_index + 2)
      return nil unless end_index

      start_index..(end_index + 1)
    end

    # @rbs text: String
    # @rbs range: Range[Integer]
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: String
    def interpolate_range(text, range, locals)
      placeholder = text[range].to_s
      expression = text[(range.begin + 2)...(range.end - 1)].to_s.strip

      interpolate_expression(expression, placeholder, locals)
    end

    # @rbs expression: String
    # @rbs placeholder: String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: String
    def interpolate_expression(expression, placeholder, locals)
      return placeholder if expression.empty?

      value = EvaluationContext.new(@state, locals).evaluate(expression)
      value.to_s
    rescue Exception => e # rubocop:disable Lint/RescueException
      JSConsole.print_error(e)

      placeholder
    end

    class EvaluationContext
      # @rbs state: ReactiveState
      # @rbs locals: Hash[Symbol, untyped]
      # @rbs return: void
      def initialize(state, locals)
        @state = state
        @locals = locals
      end

      # @rbs expression: String
      # @rbs return: untyped
      def evaluate(expression)
        eval(evaluation_code(expression)) # rubocop:disable Security/Eval
      end

      # @rbs key: Symbol
      # @rbs return: untyped
      def fetch_value(key)
        return @locals[key] if @locals.key?(key)

        @state[key]
      end

      private

      # @rbs expression: String
      # @rbs return: String
      def evaluation_code(expression)
        "#{local_variable_assignments}\n#{expression}"
      end

      # @rbs return: String
      def local_variable_assignments
        visible_keys.map do |key|
          "#{key} = fetch_value(:#{key})"
        end.join("\n")
      end

      # @rbs return: Array[Symbol]
      def visible_keys
        (@locals.keys + state_keys).uniq.select do |key|
          valid_identifier?(key)
        end
      end

      # @rbs return: Array[Symbol]
      def state_keys
        @state.respond_to?(:keys) ? @state.keys : []
      end

      # @rbs key: Symbol
      # @rbs return: bool
      def valid_identifier?(key)
        key.to_s.match?(IDENTIFIER_PATTERN)
      end
    end
  end
end


module RbWasmVdom
  module ConditionalRenderer
    CONDITIONAL_DIRECTIVES = ["#if", "#elsif", "#else"].freeze

    private

    # @rbs ast_node: VNode
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: bool
    def conditional_node_renderable?(ast_node, locals)
      return false if ast_node.props.key?("#elsif") || ast_node.props.key?("#else")
      return true unless ast_node.props.key?("#if")

      truthy_expression?(ast_node.props["#if"], locals)
    end

    # @rbs child: VNode | VFragment | String
    # @rbs return: bool
    def conditional_start_node?(child)
      child.is_a?(VNode) && child.props.key?("#if")
    end

    # @rbs child: VNode | VFragment | String
    # @rbs return: bool
    def conditional_continuation_node?(child)
      child.is_a?(VNode) && (child.props.key?("#elsif") || child.props.key?("#else"))
    end

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs start_index: Integer
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: [VNode?, Integer]
    def find_conditional_node(children, start_index, locals)
      conditional_index = find_renderable_conditional_index(children, start_index, locals)
      next_index = next_conditional_index(children, start_index + 1)

      return [nil, next_index] unless conditional_index

      child = children[conditional_index]
      return [nil, next_index] unless child.is_a?(VNode)

      [child, next_index]
    end

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs start_index: Integer
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Integer?
    def find_renderable_conditional_index(children, start_index, locals)
      index = start_index

      while index < children.length
        child = children[index]
        break unless child.is_a?(VNode)
        return index if render_conditional_node?(child, locals)

        index += 1
        break unless index < children.length && conditional_continuation_node?(children[index])
      end

      nil
    end

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs index: Integer
    # @rbs return: Integer
    def next_conditional_index(children, index)
      index += 1 while index < children.length && conditional_continuation_node?(children[index])

      index
    end

    # @rbs ast_node: VNode
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: bool
    def render_conditional_node?(ast_node, locals)
      if ast_node.props.key?("#if")
        truthy_expression?(ast_node.props["#if"], locals)
      elsif ast_node.props.key?("#elsif")
        truthy_expression?(ast_node.props["#elsif"], locals)
      else
        ast_node.props.key?("#else")
      end
    end

    # @rbs expression: String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: bool
    def truthy_expression?(expression, locals)
      !!Interpolator::EvaluationContext.new(@state, locals).evaluate(expression)
    rescue Exception => e # rubocop:disable Lint/RescueException
      JSConsole.print_error(e)

      false
    end
  end
end


module RbWasmVdom
  module DirectiveRenderer # rubocop:disable Metrics/ModuleLength
    include ConditionalRenderer

    private

    # @rbs ast_node: VNode | VFragment | String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | VFragment | String]
    def build_vdom_nodes(ast_node, locals = {})
      return [@interpolator.call(ast_node, locals)] if ast_node.is_a?(String)

      return [VFragment.new(build_child_nodes(ast_node.children, locals))] if ast_node.is_a?(VFragment)

      return [] unless conditional_node_renderable?(ast_node, locals)

      build_vdom_nodes_without_condition(ast_node, locals)
    end

    # @rbs ast_node: VNode
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: VNode
    def build_single_vnode(ast_node, locals)
      new_props = {} #: Hash[String, String]
      ast_node.props.each do |key, value|
        next if key == "#each"
        next if CONDITIONAL_DIRECTIVES.include?(key)

        new_props[key] = @interpolator.call(value, locals)
      end

      new_children = build_child_nodes(ast_node.children, locals)
      VNode.new(ast_node.tag, new_props, new_children)
    end

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | VFragment | String]
    def build_child_nodes(children, locals)
      new_children = [] #: Array[VNode | VFragment | String]
      index = 0

      while index < children.length
        rendered_nodes, index = build_child_nodes_at(children, index, locals)
        new_children.concat(rendered_nodes)
      end

      new_children
    end

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs index: Integer
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: [Array[VNode | VFragment | String], Integer]
    def build_child_nodes_at(children, index, locals)
      child = children[index]

      return build_conditional_child_nodes(children, index, locals) if conditional_start_node?(child)

      [build_vdom_nodes(child, locals), index + 1]
    end

    # @rbs children: Array[VNode | VFragment | String]
    # @rbs index: Integer
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: [Array[VNode | VFragment | String], Integer]
    def build_conditional_child_nodes(children, index, locals)
      conditional_node, next_index = find_conditional_node(children, index, locals)
      return [[], next_index] unless conditional_node

      [build_vdom_nodes_without_condition(conditional_node, locals), next_index]
    end

    # @rbs ast_node: VNode
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | String]
    def build_vdom_nodes_without_condition(ast_node, locals)
      each_expression = ast_node.props["#each"]
      return build_each_nodes(ast_node, each_expression, locals) if each_expression

      [build_single_vnode(ast_node, locals)]
    end

    # @rbs ast_node: VNode
    # @rbs expression: String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | String]
    def build_each_nodes(ast_node, expression, locals)
      parsed = parse_each_expression(expression)
      return [build_single_vnode(ast_node, locals)] unless parsed

      first_name, second_name, collection_expression = parsed
      collection = evaluate_each_collection(collection_expression, locals)

      build_collection_nodes(ast_node, collection, first_name, second_name, locals)
    end

    # @rbs expression: String
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: untyped
    def evaluate_each_collection(expression, locals)
      Interpolator::EvaluationContext.new(@state, locals).evaluate(expression)
    rescue Exception => e # rubocop:disable Lint/RescueException
      JSConsole.print_error(e)

      nil
    end

    # @rbs expression: String
    # @rbs return: [String, String?, String]?
    def parse_each_expression(expression)
      parse_each_single_value_expression(expression) ||
        parse_each_pair_expression(expression)
    end

    # @rbs expression: String
    # @rbs return: [String, nil, String]?
    def parse_each_single_value_expression(expression)
      match = expression.match(/^\s*(\w+)\s+in\s+(.+?)\s*$/)
      return nil unless match

      value_name = match[1]
      collection_expression = match[2]
      return nil unless value_name && collection_expression

      [value_name, nil, collection_expression]
    end

    # @rbs expression: String
    # @rbs return: [String, String, String]?
    def parse_each_pair_expression(expression)
      match = expression.match(/^\s*(\w+)\s*,\s*(\w+)\s+in\s+(.+?)\s*$/)
      return nil unless match

      first_name = match[1]
      second_name = match[2]
      collection_expression = match[3]
      return nil unless first_name && second_name && collection_expression

      [first_name, second_name, collection_expression]
    end

    # @rbs ast_node: VNode
    # @rbs collection: untyped
    # @rbs first_name: String
    # @rbs second_name: String?
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | String]
    def build_collection_nodes(ast_node, collection, first_name, second_name, locals)
      case collection
      when Hash
        build_hash_nodes(ast_node, collection, first_name, second_name, locals)
      when JS::Object
        build_js_object_nodes(ast_node, collection, first_name, second_name, locals)
      when Enumerable
        build_array_nodes(ast_node, collection, first_name, second_name, locals)
      else
        []
      end
    end

    # @rbs ast_node: VNode
    # @rbs collection: Enumerable[untyped]
    # @rbs item_name: String
    # @rbs index_name: String?
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | String]
    def build_array_nodes(ast_node, collection, item_name, index_name, locals)
      nodes = [] #: Array[VNode | String]
      index = 0

      collection.each do |item|
        item_locals = locals.merge(item_name.to_sym => item)
        item_locals[index_name.to_sym] = index if index_name

        nodes << build_single_vnode(ast_node, item_locals)
        index += 1
      end

      nodes
    end

    # @rbs ast_node: VNode
    # @rbs collection: Hash[untyped, untyped]
    # @rbs key_name: String
    # @rbs value_name: String?
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | String]
    def build_hash_nodes(ast_node, collection, key_name, value_name, locals)
      nodes = [] #: Array[VNode | String]

      collection.each do |key, value|
        item_locals = locals.merge(key_name.to_sym => key)
        item_locals[value_name.to_sym] = value if value_name

        nodes << build_single_vnode(ast_node, item_locals)
      end

      nodes
    end

    # @rbs ast_node: VNode
    # @rbs collection: JS::Object
    # @rbs item_name: String
    # @rbs index_name: String?
    # @rbs locals: Hash[Symbol, untyped]
    # @rbs return: Array[VNode | String]
    def build_js_object_nodes(ast_node, collection, item_name, index_name, locals)
      nodes = [] #: Array[VNode | String]

      0.upto(collection[:length].to_i - 1) do |index|
        item = collection[index]
        item_locals = locals.merge(item_name.to_sym => item)
        item_locals[index_name.to_sym] = index if index_name

        nodes << build_single_vnode(ast_node, item_locals)
      end

      nodes
    end
  end
end


module RbWasmVdom
  # Framework Core
  class App
    include Patcher
    include DirectiveRenderer

    # @rbs selector: String
    # @rbs template: String
    # @rbs state: Hash[Symbol, untyped]
    # @rbs methods: Hash[Symbol, Proc]
    # @rbs return: void
    def initialize(selector, template:, state:, methods:)
      @el = JS.global[:document].querySelector(selector)
      @template_ast = TemplateParser.parse(template)
      @methods = methods
      @current_vnode = nil

      @state = ReactiveState.new(state) do
        render_cycle
      end

      @interpolator = Interpolator.new(@state)

      render_cycle
    end

    private

    # @rbs return: void
    def render_cycle
      new_vnode = build_vdom(@template_ast)

      if @current_vnode.nil?
        @el[:innerHTML] = ""
        @el.appendChild(create_element(new_vnode))
      elsif @current_vnode.is_a?(VFragment) && new_vnode.is_a?(VFragment)
        patch_fragment(@el, @current_vnode, new_vnode)
      else
        patch(@el, @current_vnode, new_vnode, 0)
      end

      @current_vnode = new_vnode
    end

    # @rbs ast_node: VNode | VFragment | String
    # @rbs return: VNode | VFragment | String
    def build_vdom(ast_node)
      build_vdom_nodes(ast_node).first
    end
  end
end


module RbWasmVdom
  module JSConsole
    # Print error to console.error
    # @rbs error: Exception
    def self.print_error(error)
      lines = ["#{error.class}: #{error.message}"]
      backtrace = error.backtrace || []
      lines.concat(backtrace) unless backtrace.empty?
      JS.global[:console].error(lines.join("\n"))
    end
  end
end


require "js"
require "json"

module RbWasmVdom
  # @rbs selector: String
  # @rbs template: String
  # @rbs state: Hash[Symbol, untyped]
  # @rbs methods: Hash[Symbol, Proc]
  # @rbs return: App
  def self.create_app(selector, template: "", state: {}, methods: {})
    App.new(selector, template: template, state: state, methods: methods)
  end
end
