module Liquid
  class BlockBody
    FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
    ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
    TAGSTART = "{%".freeze
    VARSTART = "{{".freeze

    attr_reader :nodelist

    def initialize
      @nodelist = []
      @blank = true
    end

    def parse(tokens, options)
      while token = tokens.shift
        begin
          unless token.empty?
            case
            when token.start_with?(TAGSTART)
              if token =~ FullToken
                tag_name = $1
                markup = $2
                # fetch the tag from registered blocks
                if tag = Template.tags[tag_name]
                  markup = token.child(markup) if token.is_a?(Token)
                  new_tag = tag.parse(tag_name, markup, tokens, options)
                  new_tag.line_number = token.line_number if token.is_a?(Token)
                  @blank &&= new_tag.blank?
                  @nodelist << new_tag
                else
                  # end parsing if we reach an unknown tag and let the caller decide
                  # determine how to proceed
                  return yield tag_name, markup
                end
              else
                raise_missing_tag_terminator(token, options)
              end
            when token.start_with?(VARSTART)
              new_var = create_variable(token, options)
              new_var.line_number = token.line_number if token.is_a?(Token)
              @nodelist << new_var
              @blank = false
            else
              @nodelist << token
              @blank &&= !!(token =~ /\A\s*\z/)
            end
          end
        rescue SyntaxError => e
          e.set_line_number_from_token(token)
          raise
        end
      end

      yield nil, nil
    end

    def blank?
      @blank
    end

    def warnings
      all_warnings = []
      nodelist.each do |node|
        all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
      end
      all_warnings
    end

    def render(context)
      output = []
      context.resource_limits.render_score += @nodelist.length

      @nodelist.each do |token|
        # Break out if we have any unhanded interrupts.
        break if context.has_interrupt?

        begin
          # If we get an Interrupt that means the block must stop processing. An
          # Interrupt is any command that stops block execution such as {% break %}
          # or {% continue %}
          if token.is_a?(Continue) or token.is_a?(Break)
            context.push_interrupt(token.interrupt)
            break
          end

          token_output = render_token(token, context)

          unless token.is_a?(Block) && token.blank?
            output << token_output
          end
        rescue MemoryError => e
          raise e
        rescue ::StandardError => e
          output << context.handle_error(e, token)
        end
      end

      output.join
    end

    private

    def render_token(token, context)
      token_output = (token.respond_to?(:render) ? token.render(context) : token)
      token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s

      context.resource_limits.render_length += token_str.length
      if context.resource_limits.reached?
        raise MemoryError.new("Memory limits exceeded".freeze)
      end
      token_str
    end

    def create_variable(token, options)
      token.scan(ContentOfVariable) do |content|
        markup = token.is_a?(Token) ? token.child(content.first) : content.first
        return Variable.new(markup, options)
      end
      raise_missing_variable_terminator(token, options)
    end

    def raise_missing_tag_terminator(token, options)
      raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
    end

    def raise_missing_variable_terminator(token, options)
      raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
    end
  end
end