module Cucumber
  module Parser
    # TIP: When you hack on the grammar, just delete feature.rb in this directory.
    # Also make sure you have uninstalled all cucumber gems (don't forget xxx-cucumber
    # github gems).
    #
    # Treetop will then generate the parser in-memory. When you're happy, just generate
    # the rb file with tt feature.tt
    grammar Feature

      rule feature_sub
        white 
        comment 
        white 
        tags 
        white 
        header:(!(scenario_outline / scenario / background) .)* 
        bg:background? 
        feature_elements
        comment? {
          def has_tags?(tag_names)
            tags.has_tags?(tag_names)
          end

          def has_all_tags?(tag_names)
            tags.has_all_tags?(tag_names)
          end

          def build(filter)
            if(filter.nil? || feature_elements.accept?(filter) || (!bg.empty? && filter.accept?(bg)))
              background = bg.respond_to?(:build) ? bg.build : nil      
              Ast::Feature.new(
                background, 
                comment.build, 
                tags.build, 
                header.text_value, 
                feature_elements.build(background, filter)
              )
            end
          end
        }
      end

      rule tags
        white ts:(tag (space/eol)+)* {
          def at_line?(line)
            ts.elements.detect{|e| e.tag.line == line}
          end

          def has_tags?(tags)
            (tag_names & tags).any?
          end

          def has_all_tags?(tags)
            (tags & tag_names) == tags
          end

          def build
            Ast::Tags.new(ts.line, tag_names)
          end
          
          def tag_names
            @tag_names ||= ts.elements.map{|e| e.tag.tag_name.text_value}
          end
        }
      end

      rule tag
        '@' tag_name:([^@\r\n\t ])+
      end

      rule comment
        (comment_line white)* {
          def build
            Ast::Comment.new(text_value)
          end
        }
      end

      rule comment_line
        '#' line_to_eol
      end

      rule background
        comment white background_keyword space* name:lines_to_keyword? (eol+ / eof) steps {
          def matches_name?(regexp_to_match)
            name.build =~ regexp_to_match
          end

          def at_line?(line)
            background_keyword.line == line ||
            steps.at_line?(line)
          end

          def has_tags?(tag_names)
            feature_tags = self.parent.tags
            feature_tags.has_tags?(tag_names)
          end

          def has_all_tags?(tag_names)
            feature_tags = self.parent.tags
            feature_tags.has_all_tags?(tag_names)
          end

          def build
            Ast::Background.new(
              comment.build, 
              background_keyword.line,
              background_keyword.text_value,
              name.build, 
              steps.build
            )
          end
        }
      end

      rule feature_elements
        (scenario / scenario_outline)* {
          def accept?(filter)
            filter.nil? || elements.empty? || elements.detect{|feature_element| filter.accept?(feature_element)}
          end
          
          def build(background, filter)
            elements.map do |feature_element|
              if filter.nil? || filter.accept?(feature_element)
                feature_element.build(background, filter)
              end
            end.compact
          end
        }
      end

      rule scenario
        comment tags white scenario_keyword space* name:lines_to_keyword white steps white {
          def at_line?(line)
            scenario_keyword.line == line ||
            steps.at_line?(line) ||
            tags.at_line?(line)
          end

          def has_tags?(tag_names)
            feature_tags = self.parent.parent.tags
            tags.has_tags?(tag_names) || feature_tags.has_tags?(tag_names)
          end

          def has_all_tags?(tag_names)
            feature_tags = self.parent.parent.tags
            tags.has_all_tags?(tag_names) || feature_tags.has_all_tags?(tag_names)
          end

          def matches_name?(regexp_to_match)
            name.build =~ regexp_to_match
          end

          def build(background, filter)
            Ast::Scenario.new(
              background,
              comment.build, 
              tags.build,
              scenario_keyword.line,
              scenario_keyword.text_value, 
              name.build, 
              steps.build
            )
          end
        }
      end

      rule scenario_outline
        comment tags white scenario_outline_keyword space* name:lines_to_keyword white steps examples_sections white {
          def at_line?(line)
            outline_at_line?(line) ||
            examples_sections.at_line?(line) ||
            tags.at_line?(line)
          end

          def outline_at_line?(line)
            scenario_outline_keyword.line == line ||
            steps.at_line?(line)
          end

          def has_tags?(tag_names)
            feature_tags = self.parent.parent.tags
            tags.has_tags?(tag_names) || feature_tags.has_tags?(tag_names)
          end

          def has_all_tags?(tag_names)
            feature_tags = self.parent.parent.tags
            tags.has_all_tags?(tag_names) || feature_tags.has_all_tags?(tag_names)
          end

          def matches_name?(regexp_to_match)
            outline_matches_name?(regexp_to_match) || examples_sections.matches_name?(regexp_to_match)
          end

          def outline_matches_name?(regexp_to_match)
            name.build =~ regexp_to_match
          end

          def build(background, filter)
            Ast::ScenarioOutline.new(
              background,
              comment.build, 
              tags.build,
              scenario_outline_keyword.line, 
              scenario_outline_keyword.text_value, 
              name.build, 
              steps.build, 
              examples_sections.build(filter, self)
            )
          end
        }
      end

      rule steps
        step* {
          def at_line?(line)
            elements.detect{|e| e.at_line?(line)}
          end

          def build
            elements.map{|e| e.build}
          end
        }
      end

      rule step
        comment space* step_keyword keyword_space name:line_to_eol (eol+ / eof) multi:multiline_arg? white {
          def at_line?(line)
            step_keyword.line == line ||
            (multi.respond_to?(:at_line?) && multi.at_line?(line))
          end

          def build
            if multi.respond_to?(:build)
              Ast::Step.new(step_keyword.line, step_keyword.text_value, name.text_value.strip, multi.build)
            else
              Ast::Step.new(step_keyword.line, step_keyword.text_value, name.text_value.strip)
            end
          end
        }
      end

      rule examples_sections
        examples* {
          def at_line?(line)
            elements.detect { |e| e.at_line?(line) }
          end

          def matches_name?(regexp_to_match)
            elements.detect { |e| e.matches_name?(regexp_to_match) }
          end

          def build(filter, scenario_outline)
            elements.map do |e|
              if(filter.nil? || filter.accept_example?(e, scenario_outline))
                e.build(filter, scenario_outline)
              end
            end.compact
          end
        }
      end

      rule examples
        space* examples_keyword space* name:lines_to_keyword? eol table white {
          def at_line?(line)
            examples_keyword.line == line ||
            table.at_line?(line)
          end

          def has_tags?(tag_names)
            true
          end

          def has_all_tags?(tag_names)
            true
          end

          def outline_at_line?(line)
            true
          end

          def matches_name?(regexp_to_match)
            name.build =~ regexp_to_match
          end

          def build(filter, scenario_outline)
            [examples_keyword.line, examples_keyword.text_value, name.build, table.raw(filter, scenario_outline)]
          end
        }
      end

      rule multiline_arg
        table / py_string
      end

      rule line_to_eol
        (!eol .)*
      end

      rule lines_to_keyword
        (!(eol space* reserved_words_and_symbols) .)* {
          def build
            self.text_value.split("\n").map{|s| s.strip}.join("\n")
          end
        }
      end

      rule reserved_words_and_symbols
        (step_keyword keyword_space) / scenario_keyword / scenario_outline_keyword / table / tag / comment_line
      end

      rule py_string
        open_py_string s:(!close_py_string .)* close_py_string {
          def at_line?(line)
            line >= open_py_string.line && line <= close_py_string.line
          end

          def build
            Ast::PyString.new(open_py_string.line, close_py_string.line, s.text_value, open_py_string.indentation)
          end
        }
      end

      rule open_py_string
        indent:space* '"""' space* eol {
          def indentation
            indent.text_value.length
          end

          def line
            indent.line
          end
        }
      end

      rule close_py_string
        eol space* quotes:'"""' white {
          def line
            quotes.line
          end
        }
      end

      rule white
        (space / eol)*
      end

    end
  end
end
