// Package check provides types yielding values checked to meet a condition.
package check

import (
	"encoding"
	"errors"
	"fmt"
	"path/filepath"
	"slices"
	"strings"
	"syscall"
	"unique"
)

// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
type AbsoluteError string

func (e AbsoluteError) Error() string {
	return fmt.Sprintf("path %q is not absolute", string(e))
}

func (e AbsoluteError) Is(target error) bool {
	ce, ok := errors.AsType[AbsoluteError](target)
	if !ok {
		return errors.Is(target, syscall.EINVAL)
	}
	return e == ce
}

// Absolute holds a pathname checked to be absolute.
type Absolute struct{ pathname unique.Handle[string] }

var (
	_ fmt.GoStringer = new(Absolute)

	_ encoding.TextAppender    = new(Absolute)
	_ encoding.TextMarshaler   = new(Absolute)
	_ encoding.TextUnmarshaler = new(Absolute)

	_ encoding.BinaryAppender    = new(Absolute)
	_ encoding.BinaryMarshaler   = new(Absolute)
	_ encoding.BinaryUnmarshaler = new(Absolute)
)

func (a *Absolute) GoString() string {
	return fmt.Sprintf("check.MustAbs(%q)", a.String())
}

// ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }

// unsafeAbs returns [check.Absolute] on any string value.
func unsafeAbs(pathname string) *Absolute {
	return &Absolute{unique.Make(pathname)}
}

// String returns the checked pathname.
func (a *Absolute) String() string {
	if !a.ok() {
		panic("attempted use of zero Absolute")
	}
	return a.pathname.Value()
}

// Handle returns the underlying [unique.Handle].
func (a *Absolute) Handle() unique.Handle[string] {
	return a.pathname
}

// Is efficiently compares the underlying pathname.
func (a *Absolute) Is(v *Absolute) bool {
	if a == nil && v == nil {
		return true
	}
	return a.ok() && v.ok() && a.pathname == v.pathname
}

// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
func NewAbs(pathname string) (*Absolute, error) {
	if !filepath.IsAbs(pathname) {
		return nil, AbsoluteError(pathname)
	}
	return unsafeAbs(pathname), nil
}

// MustAbs calls [NewAbs] and panics on error.
func MustAbs(pathname string) *Absolute {
	if a, err := NewAbs(pathname); err != nil {
		panic(err)
	} else {
		return a
	}
}

// Append calls [filepath.Join] with [Absolute] as the first element.
func (a *Absolute) Append(elem ...string) *Absolute {
	return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
}

// Dir calls [filepath.Dir] with [Absolute] as its argument.
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }

// AppendText appends the checked pathname.
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
	return append(data, a.String()...), nil
}

// MarshalText returns the checked pathname.
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }

// UnmarshalText stores data if it represents an absolute pathname.
func (a *Absolute) UnmarshalText(data []byte) error {
	pathname := string(data)
	if !filepath.IsAbs(pathname) {
		return AbsoluteError(pathname)
	}
	a.pathname = unique.Make(pathname)
	return nil
}

func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
func (a *Absolute) MarshalBinary() ([]byte, error)           { return a.MarshalText() }
func (a *Absolute) UnmarshalBinary(data []byte) error        { return a.UnmarshalText(data) }

// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
func SortAbs(x []*Absolute) {
	slices.SortFunc(x, func(a, b *Absolute) int {
		return strings.Compare(a.String(), b.String())
	})
}

// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
func CompactAbs(s []*Absolute) []*Absolute {
	return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool {
		return a.Is(b)
	})
}
