package scalar

// TODO: d.Pos check err
// TODO: fn for actual?
// TODO: dsl import .? own scalar package?
// TODO: better IOError op names

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/wader/fq/internal/bitioex"
	"github.com/wader/fq/pkg/bitio"
)

//go:generate sh -c "cat scalar_gen.go.tmpl | go run ../../dev/tmpl.go ../decode/types.json | gofmt > scalar_gen.go"

type DisplayFormat int

const (
	NumberDecimal DisplayFormat = iota
	NumberBinary
	NumberOctal
	NumberHex
)

func (df DisplayFormat) FormatBase() int {
	switch df {
	case NumberDecimal:
		return 10
	case NumberBinary:
		return 2
	case NumberOctal:
		return 8
	case NumberHex:
		return 16
	default:
		return 0
	}
}

// TODO: todos
// rename raw?
// crc
//
//
// d.FieldU2("emphasis").
//   MapSym().
//   Actual
//   Sym
//   Value()
//   Scalar()

var UintBin = UintFn(func(s Uint) (Uint, error) { s.DisplayFormat = NumberBinary; return s, nil })
var UintOct = UintFn(func(s Uint) (Uint, error) { s.DisplayFormat = NumberOctal; return s, nil })
var UintDec = UintFn(func(s Uint) (Uint, error) { s.DisplayFormat = NumberDecimal; return s, nil })
var UintHex = UintFn(func(s Uint) (Uint, error) { s.DisplayFormat = NumberHex; return s, nil })
var SintBin = SintFn(func(s Sint) (Sint, error) { s.DisplayFormat = NumberBinary; return s, nil })
var SintOct = SintFn(func(s Sint) (Sint, error) { s.DisplayFormat = NumberOctal; return s, nil })
var SintDec = SintFn(func(s Sint) (Sint, error) { s.DisplayFormat = NumberDecimal; return s, nil })
var SintHex = SintFn(func(s Sint) (Sint, error) { s.DisplayFormat = NumberHex; return s, nil })
var BigIntBin = BigIntFn(func(s BigInt) (BigInt, error) { s.DisplayFormat = NumberBinary; return s, nil })
var BigIntOct = BigIntFn(func(s BigInt) (BigInt, error) { s.DisplayFormat = NumberOctal; return s, nil })
var BigIntDec = BigIntFn(func(s BigInt) (BigInt, error) { s.DisplayFormat = NumberDecimal; return s, nil })
var BigIntHex = BigIntFn(func(s BigInt) (BigInt, error) { s.DisplayFormat = NumberHex; return s, nil })

func UintActualAdd(n int) UintActualFn {
	// TODO: use math.Add/Sub?
	return UintActualFn(func(a uint64) uint64 { return uint64(int64(a) + int64(n)) })
}

func SintActualAdd(n int) SintActualFn {
	return SintActualFn(func(a int64) int64 { return a + int64(n) })
}

func StrActualTrim(cutset string) StrActualFn {
	return StrActualFn(func(a string) string { return strings.Trim(a, cutset) })
}

var ActualTrimSpace = StrActualFn(strings.TrimSpace)

func strMapToSym(fn func(s string) (any, error), try bool) StrMapper {
	return StrFn(func(s Str) (Str, error) {
		ts := strings.TrimSpace(s.Actual)
		n, err := fn(ts)
		if err != nil {
			if try {
				return s, nil
			}
			return s, err
		}
		s.Sym = n
		return s, nil
	})
}

func TryStrSymParseUint(base int) StrMapper {
	return strMapToSym(func(s string) (any, error) { return strconv.ParseUint(s, base, 64) }, true)
}

func TryStrSymParseInt(base int) StrMapper {
	return strMapToSym(func(s string) (any, error) { return strconv.ParseInt(s, base, 64) }, true)
}

func TryStrSymParseFloat(base int) StrMapper {
	return strMapToSym(func(s string) (any, error) { return strconv.ParseFloat(s, base) }, true)
}

func StrSymParseUint(base int) StrMapper {
	return strMapToSym(func(s string) (any, error) { return strconv.ParseUint(s, base, 64) }, false)
}

func StrSymParseInt(base int) StrMapper {
	return strMapToSym(func(s string) (any, error) { return strconv.ParseInt(s, base, 64) }, false)
}

func StrSymParseFloat(base int) StrMapper {
	return strMapToSym(func(s string) (any, error) { return strconv.ParseFloat(s, base) }, false)
}

type URangeEntry struct {
	Range [2]uint64
	S     Uint
}

// UintRangeToScalar maps uint64 ranges to a scalar, first in range is chosen
type UintRangeToScalar []URangeEntry

func (rs UintRangeToScalar) MapUint(s Uint) (Uint, error) {
	n := s.Actual
	for _, re := range rs {
		if n >= re.Range[0] && n <= re.Range[1] {
			ns := re.S
			ns.Actual = s.Actual
			s = ns
			break
		}
	}
	return s, nil
}

// SRangeToScalar maps ranges to a scalar, first in range is chosen
type SRangeEntry struct {
	Range [2]int64
	S     Sint
}

// SRangeToScalar maps sint64 ranges to a scalar, first in range is chosen
type SRangeToScalar []SRangeEntry

func (rs SRangeToScalar) MapSint(s Sint) (Sint, error) {
	n := s.Actual
	for _, re := range rs {
		if n >= re.Range[0] && n <= re.Range[1] {
			ns := re.S
			ns.Actual = s.Actual
			s = ns
			break
		}
	}
	return s, nil
}

func RawSym(s BitBuf, nBytes int, fn func(b []byte) string) (BitBuf, error) {
	br := s.Actual
	brLen, err := bitioex.Len(br)
	if err != nil {
		return BitBuf{}, err
	}
	if nBytes < 0 {
		nBytes = int(brLen) / 8
		if brLen%8 != 0 {
			nBytes++
		}
	}
	if brLen < int64(nBytes)*8 {
		return s, nil
	}
	// TODO: shared somehow?
	b := make([]byte, nBytes)
	if _, err := br.ReadBitsAt(b, int64(nBytes)*8, 0); err != nil {
		return s, err
	}

	s.Sym = fn(b[0:nBytes])

	return s, nil
}

var RawUUID = BitBufFn(func(s BitBuf) (BitBuf, error) {
	return RawSym(s, -1, func(b []byte) string {
		return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
	})
})

var RawHex = BitBufFn(func(s BitBuf) (BitBuf, error) {
	return RawSym(s, -1, func(b []byte) string { return fmt.Sprintf("%x", b) })
})

type RawBytesMap []struct {
	Bytes  []byte
	Scalar BitBuf
}

func (m RawBytesMap) MapBitBuf(s BitBuf) (BitBuf, error) {
	rc, err := bitio.CloneReader(s.Actual)
	if err != nil {
		return s, err
	}
	bb := &bytes.Buffer{}
	if _, err := bitioex.CopyBits(bb, rc); err != nil {
		return s, err
	}
	for _, bs := range m {
		if bytes.Equal(bb.Bytes(), bs.Bytes) {
			ns := bs.Scalar
			ns.Actual = s.Actual
			s = ns
			break
		}
	}
	return s, nil
}

var unixTimeEpochDate = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)

func UintActualDate(epoch time.Time, format string) UintFn {
	return UintFn(func(s Uint) (Uint, error) {
		s.Description = epoch.Add(time.Duration(s.Actual) * time.Second).Format(format)
		return s, nil
	})
}

func UintActualUnixTime(format string) UintFn {
	return UintActualDate(unixTimeEpochDate, format)
}

func FltActualDate(epoch time.Time, format string) FltFn {
	return FltFn(func(s Flt) (Flt, error) {
		s.Description = epoch.Add(time.Duration(s.Actual) * time.Second).Format(format)
		return s, nil
	})
}

func FltActualUnixTime(format string) FltFn {
	return FltActualDate(unixTimeEpochDate, format)
}
