package file

import (
	"bufio"
	"bytes"
	"os"
	"reflect"
	"regexp"
	"strings"

	"github.com/cstockton/go-conv"
	"golang.org/x/exp/slices"
)

const (
	carret  = '\r'
	newLine = '\n'
)

type scanner struct {
	f    *os.File
	scan *bufio.Scanner
}

// Lines contained in the file at the moment of the reading then cached
func (s *scanner) Scan() bool {
	return s.scan.Scan()
}

// TODO: Maybe use "github.com/vingarcia/structscanner"
func (s *scanner) Into(obj any) error {
	objv := reflect.ValueOf(obj)

	if objv.Kind() != reflect.Pointer || objv.IsNil() {
		return ErrNotReferencedObject
	}

	target := objv.Elem()
	if !target.IsValid() {
		return ErrInvalidObject
	}

	if target.Kind() != reflect.Struct {
		return ErrNonStructObject
	}

	targetType := target.Type()
	lines := s.lines()

	if len(lines) == 0 {
		return nil
	}

	for i := 0; i < targetType.NumField(); i++ {
		field := target.Field(i)

		if !field.IsValid() {
			continue
		}

		// Cannot take pointer of this field, so can't scrub it.
		if !field.CanAddr() {
			continue
		}

		// This is an unexported or private field (begins with lowercase).
		// We can't take an interface on that or scrub it.
		// UnsafeAddr(), which is unsafe.Pointer, can be used to workaround it,
		// but that is not recommended in Golang.
		if !field.Addr().CanInterface() {
			continue
		}

		// The specified format tag
		format := targetType.Field(i).Tag.Get("format")

		if format == "" {
			continue
		}

		re := regexp.MustCompile(format)
		t := targetType.Field(i).Type
		isPointer := false

		// Needed to properly assign the match later on
		if field.Kind() == reflect.Ptr {
			t = t.Elem()
			isPointer = true
		}

		val := reflect.New(t).Interface()

		for _, line := range lines {
			matches := re.FindStringSubmatch(line)

			if len(matches) == 0 {
				continue
			}

			// Ignoring the whole group match
			if len(matches) > 1 {
				matches = matches[1:]
			}

			for _, match := range matches {
				if err := conv.Infer(val, match); err == nil {
					break
				}
			}

			v := reflect.ValueOf(val)

			if !isPointer {
				v = reflect.Indirect(v)
			}

			field.Set(v)

			break
		}
	}

	return nil
}

// Text returns the most recent token generated by a call to Scan
// as a newly allocated string holding its bytes.
func (s *scanner) Text() string {
	return s.scan.Text()
}

// Bytes returns the most recent token generated by a call to Scan.
// The underlying array may point to data that will be overwritten
// by a subsequent call to Scan. It does no allocation.
func (s *scanner) Bytes() []byte {
	return s.scan.Bytes()
}

func (s *scanner) lines() []string {
	text := s.scan.Text()

	return strings.Split(text, "\n")
}

// Err returns the first non-EOF error that was encountered by the reader.
func (s *scanner) Err() error {
	return s.scan.Err()
}

// Close the reader
func (s *scanner) Close() {
	s.f.Close()
}

func NewLineScanner(name string) (*scanner, error) {
	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}

	scan := bufio.NewScanner(f)
	scan.Split(bufio.ScanLines)

	return &scanner{f: f, scan: scan}, nil
}

// NewBlockScanner that reads a file searching for blocks separated by empty lines
// Uses bufio scanner under the hood so the way of iterating thru the matches is exactly
// the same than using a bufio.Scanner
//
// <START OF FILE>
//
//	First line of block 1
//	Second line of block 1
//
//	First line of block 2
//	Second line of block 2
//
// <END OF FILE>
//
// Will return two blocks with 2 lines each
func NewBlockScanner(name string) (*scanner, error) {
	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}

	scan := bufio.NewScanner(f)
	scan.Split(ScanBlocks)

	return &scanner{f: f, scan: scan}, nil
}

// ScanBlocks represents the bufio.SplitFunc to split by blocks
// Example of buffer with blocks:
//
// block 1 line 1
// block 1 line 2
// block 1 line 3
//
// block 2 line 1
// block 2 line 2
//
// block 3 line 1
// block 3 line 2
func ScanBlocks(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF && len(data) == 0 {
		return 0, nil, nil
	}

	// We don't care about crs
	d := stripCR(data)
	start := -1

	// Let´s ignore non representative chars such as new lines and empty chars
	for i := 0; i < len(d); i++ {
		if d[i] != newLine && string(d[i]) != "" {
			start = i
			break
		}
	}

	// Nothing really to process so let´s move to the next chunk
	if start == -1 {
		return len(data), nil, nil
	}

	pos := start

	for pos < len(d) {
		next := bytes.IndexByte(d[pos:], newLine) + 1

		if next == 0 {
			return len(d), d[start:], nil
		}

		if strings.TrimSpace(string(d[pos:pos+next])) == "" {
			trimmed := strings.TrimSpace(string(d[start : pos+next-2]))
			ret := []byte(trimmed)

			if len(ret) == 0 {
				ret = nil
			}

			return pos + next, []byte(ret), nil
		}

		pos = pos + next
	}

	if pos == len(d) {
		return pos, d[:pos-1], nil
	}

	// If we're at EOF, we have a final, non-terminated line. Return it.
	if atEOF {
		return len(d), nil, nil
	}

	// Request more data.
	return 0, nil, nil
}

func stripCR(data []byte) []byte {
	for {
		i := bytes.IndexByte(data, carret)
		if i < 0 {
			break
		}

		data = slices.Delete(data, i, i+1)
	}

	return data
}
