// package asciiscan is used to parse plaintext files into structured data
package asciiscan

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

var hashComment = []byte("#")

type Scanner struct {
	bufio  *bufio.Scanner
	closer io.Closer

	// If we've peeked, there will be items here
	buffered []string
}

func New(filename string) (*Scanner, error) {
	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}

	return &Scanner{
		bufio:  bufio.NewScanner(f),
		closer: f,
	}, nil
}

func (s *Scanner) Bufio() *bufio.Scanner {
	return s.bufio
}

func (s *Scanner) Close() error {
	return s.closer.Close()
}

func (s *Scanner) ConsumeString() (string, error) {
	if len(s.buffered) > 0 {
		out, buffered := s.buffered[0], s.buffered[1:]
		s.buffered = buffered

		return out, nil
	}

	for s.bufio.Scan() {
		line := s.bufio.Bytes()

		if len(line) == 0 || line[0] == hashComment[0] { // Most .dat files use # for comments
			continue
		}

		comment := bytes.Index(line, hashComment)
		if comment > 0 {
			line = line[0:comment]
		}

		return string(bytes.TrimRight(line, "\r\n\t ")), nil
	}

	err := s.bufio.Err()
	if err == nil {
		return "", io.EOF
	}

	return "", err
}

// It's common for properties to be specified as "foo : bar". Parse them out.
func ConsumeProperty(s string) (string, string) {
	if !IsProperty(s) {
		return "", ""
	}

	parts := strings.SplitN(s, ":", 2)

	return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
}

// Check to see if the line looks like a property (contains a colon character).
func IsProperty(s string) bool {
	return strings.Contains(s, ":")
}

// Checks if the next line might be a property, without reading it
func (s *Scanner) PeekProperty() (bool, error) {
	str, err := s.ConsumeString()
	if err != nil {
		return false, err
	}

	s.buffered = append(s.buffered, str)

	return IsProperty(str), nil
}

func (s *Scanner) ConsumeProperty() (string, string, error) {
	str, err := s.ConsumeString()
	if err != nil {
		return "", "", err
	}

	if !IsProperty(str) {
		return "", "", fmt.Errorf("Not a property: %q", str)
	}

	k, v := ConsumeProperty(str)
	return k, v, nil
}

func (s *Scanner) ConsumeInt() (int, error) {
	str, err := s.ConsumeString()
	if err != nil {
		return 0, err
	}

	return strconv.Atoi(str)
}

func (s *Scanner) ConsumeBool() (bool, error) {
	integer, err := s.ConsumeInt()
	if err != nil {
		return false, err
	}

	return (integer > 0), nil
}

// Reads a list of non-property lines, skipping any that match the given strings
func (s *Scanner) ConsumeStringList(skip ...string) ([]string, error) {
	skipper := make(map[string]bool, len(skip))
	for _, str := range skip {
		skipper[str] = true
	}

	var out []string

	for {
		isProp, err := s.PeekProperty()
		if err != nil {
			return nil, err
		}

		// The object list is terminated by the first property
		if isProp {
			break
		}

		str, err := s.ConsumeString()
		if err != nil {
			return nil, err
		}

		if !skipper[str] {
			out = append(out, str)
		}
	}

	return out, nil
}

func (s *Scanner) ConsumeIntPtr(to *int) error {
	val, err := s.ConsumeInt()
	if err != nil {
		return err
	}

	*to = val
	return nil
}

func (s *Scanner) ConsumeBoolPtr(to *bool) error {
	val, err := s.ConsumeBool()
	if err != nil {
		return err
	}

	*to = val
	return nil
}

func (s *Scanner) ConsumeIntPtrs(ptrs ...*int) error {
	for _, ptr := range ptrs {
		if err := s.ConsumeIntPtr(ptr); err != nil {
			return err
		}
	}

	return nil
}

func (s *Scanner) ConsumeBoolPtrs(ptrs ...*bool) error {
	for _, ptr := range ptrs {
		if err := s.ConsumeBoolPtr(ptr); err != nil {
			return err
		}
	}

	return nil
}
