package filterlist

import (
	"bufio"
	"io"

	"github.com/AdguardTeam/urlfilter/rules"
)

// RuleScanner implements an interface for reading filtering rules.
type RuleScanner struct {
	// reader reads the source.
	reader *bufio.Reader

	// currentRule is the last read rule.
	currentRule rules.Rule

	// listID is the filter list ID.
	listID rules.ListID

	// currentPos is the current position in the reader.
	currentPos int64

	// currentRuleIndex is the index of the beginning of the current rule.
	currentRuleIndex int64

	// ignoreCosmetic tells whether to ignore cosmetic rules or not.
	ignoreCosmetic bool
}

// NewRuleScanner returns a new RuleScanner to read from the given reader.
func NewRuleScanner(r io.Reader, id rules.ListID, ignoreCosmetic bool) (s *RuleScanner) {
	return &RuleScanner{
		listID:         id,
		ignoreCosmetic: ignoreCosmetic,
		reader:         bufio.NewReaderSize(r, int(readerBufferSize)),
	}
}

// Scan advances the RuleScanner to the next rule, which will then be available
// through the Rule method. It returns false when the scan stops, either by
// reaching the end of the input or an error.
func (s *RuleScanner) Scan() (ok bool) {
	for {
		line, idx, err := s.readNextLine()
		if err != nil {
			return false
		}

		rule, err := rules.NewRule(line, s.listID)
		// TODO(a.garipov):  Ignore cosmetic earlier in the process when they're
		// still in []byte.
		if rule != nil && err == nil && !s.isIgnored(rule) {
			s.currentRule = rule
			s.currentRuleIndex = idx

			return true
		}
	}
}

// Rule returns the most recent rule generated by a call to Scan, and the index
// of this rule's text.
func (s *RuleScanner) Rule() (r rules.Rule, idx int64) {
	return s.currentRule, s.currentRuleIndex
}

// readNextLine reads the next line and returns it and the index of the
// beginning of the string.
func (s *RuleScanner) readNextLine() (line string, idx int64, err error) {
	lineIndex := s.currentPos

	for {
		var bytes []byte
		bytes, err = s.reader.ReadBytes('\n')
		if len(bytes) > 0 {
			s.currentPos += int64(len(bytes))

			return string(bytes), lineIndex, nil
		}

		// TODO(d.kolyshev):  Do not overwrite error?
		if err != nil {
			return "", lineIndex, io.EOF
		}
	}
}

// isIgnored checks if the rule should be ignored by this scanner.
func (s *RuleScanner) isIgnored(f rules.Rule) (ignored bool) {
	if !s.ignoreCosmetic {
		return false
	}

	if _, ok := f.(*rules.CosmeticRule); ok {
		return true
	}

	return false
}
