# frozen_string_literal: true

module Exclaim
  class Ui
    attr_reader :implementation_map, :parsed_ui, :renderer

    def initialize(implementation_map: Exclaim::Implementations.example_implementation_map)
      @implementation_map = Exclaim::ImplementationMap.parse!(implementation_map)
    rescue Exclaim::Error
      raise
    rescue StandardError => e
      e.extend(Exclaim::InternalError)
      raise
    end

    def parse_ui!(ui_config)
      self.parsed_ui = Exclaim::UiConfiguration.parse!(@implementation_map, ui_config)
    rescue Exclaim::Error
      raise
    rescue StandardError => e
      e.extend(Exclaim::InternalError)
      raise
    end

    def render(env: {})
      if parsed_ui.nil?
        error_message = 'Cannot render without UI configured, must call Exclaim::Ui#parse_ui(ui_config) first'
        raise RenderingError.new(error_message)
      end

      renderer.call(env: env)
    rescue Exclaim::Error
      raise
    rescue StandardError => e
      e.extend(Exclaim::InternalError)
      raise
    end

    def unique_bind_paths
      if parsed_ui.nil?
        error_message = 'Cannot compute unique_bind_paths without UI configured, ' \
                        'must call Exclaim::Ui#parse_ui(ui_config) first'
        raise UiConfigurationError.new(error_message)
      end

      parsed_ui.config.reduce([]) { |all_paths, config_value| bind_paths(config_value, all_paths) }.uniq!
    end

    def each_element(element_names = :ALL_ELEMENTS, &blk)
      if parsed_ui.nil?
        error_message = 'Cannot compute each_element without UI configured, ' \
                        'must call Exclaim::Ui#parse_ui(ui_config) first'
        raise UiConfigurationError.new(error_message)
      end
      normalized_element_names = parse_element_names(element_names)

      if block_given?
        top_level_component = parsed_ui
        recurse_json_declarations(top_level_component, normalized_element_names, &blk)
      else
        to_enum(__callee__)
      end
    end

    private

    def parsed_ui=(value)
      @parsed_ui = value
      @renderer = Exclaim::Renderer.new(@parsed_ui)
    end

    def bind_paths(config_value, accumulator)
      case config_value
      in Hash => hash
        hash.values.each { |val| bind_paths(val, accumulator) }
      in Array => array
        array.each { |val| bind_paths(val, accumulator) }
      in Bind => bind
        accumulator.push(bind.path)
      in Helper | Component => element
        bind_paths(element.config, accumulator)
      else
        nil
      end

      accumulator
    end

    def parse_element_names(element_names)
      case element_names
      when :ALL_ELEMENTS
        :ALL_ELEMENTS
      when String
        [normalize_name(element_names)]
      when Array
        element_names.map { |en| normalize_name(en) }
      else
        raise UiConfigurationError.new('Exclaim::Ui#each_element: element_names argument ' \
                                       "must be a String or Array, given #{element_names.class}")
      end
    end

    def normalize_name(element_name)
      element_name.start_with?('$') ? element_name[1..] : element_name
    end

    def recurse_json_declarations(config_value, element_names, &blk)
      case config_value
      in Bind => bind
        yield bind.json_declaration if element_matches?(element_names, 'bind')
      in Component | Helper => element
        yield element.json_declaration if element_matches?(element_names, element.name)
        element.config.each_value { |val| recurse_json_declarations(val, element_names, &blk) }
      in Array => array
        array.each { |val| recurse_json_declarations(val, element_names, &blk) }
      in Hash => hash
        hash.each_value { |val| recurse_json_declarations(val, element_names, &blk) }
      else
        nil
      end
    end

    def element_matches?(requested_element_names, parsed_element_name)
      requested_element_names == :ALL_ELEMENTS || requested_element_names.include?(parsed_element_name)
    end
  end
end