package fn

import "testing"

// Option[A] represents a value which may or may not be there. This is very
// often preferable to nil-able pointers.
type Option[A any] struct {
	isSome bool
	some   A
}

// Some trivially injects a value into an optional context.
//
// Some : A -> Option[A].
func Some[A any](a A) Option[A] {
	return Option[A]{
		isSome: true,
		some:   a,
	}
}

// None trivially constructs an empty option
//
// None : Option[A].
func None[A any]() Option[A] {
	return Option[A]{}
}

// ElimOption is the universal Option eliminator. It can be used to safely
// handle all possible values inside the Option by supplying two continuations.
//
// ElimOption : (Option[A], () -> B, A -> B) -> B.
func ElimOption[A, B any](o Option[A], b func() B, f func(A) B) B {
	if o.isSome {
		return f(o.some)
	}

	return b()
}

// UnwrapOr is used to extract a value from an option, and we supply the default
// value in the case when the Option is empty.
//
// UnwrapOr : (Option[A], A) -> A.
func (o Option[A]) UnwrapOr(a A) A {
	if o.isSome {
		return o.some
	}

	return a
}

// UnwrapOrFunc is used to extract a value from an option, and we supply a
// thunk to be evaluated in the case when the Option is empty.
func (o Option[A]) UnwrapOrFunc(f func() A) A {
	return ElimOption(o, f, func(a A) A { return a })
}

// UnwrapOrFail is used to extract a value from an option within a test
// context. If the option is None, then the test fails.
func (o Option[A]) UnwrapOrFail(t *testing.T) A {
	t.Helper()

	if o.isSome {
		return o.some
	}

	t.Fatalf("Option[%T] was None()", o.some)

	var zero A
	return zero
}

// UnwrapOrErr is used to extract a value from an option, if the option is
// empty, then the specified error is returned directly.
func (o Option[A]) UnwrapOrErr(err error) (A, error) {
	if !o.isSome {
		var zero A
		return zero, err
	}

	return o.some, nil
}

// UnwrapOrFuncErr is used to extract a value from an option, and we supply a
// thunk to be evaluated in the case when the Option is empty.
func (o Option[A]) UnwrapOrFuncErr(f func() (A, error)) (A, error) {
	if o.isSome {
		return o.some, nil
	}

	return f()
}

// WhenSome is used to conditionally perform a side-effecting function that
// accepts a value of the type that parameterizes the option. If this function
// performs no side effects, WhenSome is useless.
//
// WhenSome : (Option[A], A -> ()) -> ().
func (o Option[A]) WhenSome(f func(A)) {
	if o.isSome {
		f(o.some)
	}
}

// IsSome returns true if the Option contains a value
//
// IsSome : Option[A] -> bool.
func (o Option[A]) IsSome() bool {
	return o.isSome
}

// IsNone returns true if the Option is empty
//
// IsNone : Option[A] -> bool.
func (o Option[A]) IsNone() bool {
	return !o.isSome
}

// FlattenOption joins multiple layers of Options together such that if any of
// the layers is None, then the joined value is None. Otherwise the innermost
// Some value is returned.
//
// FlattenOption : Option[Option[A]] -> Option[A].
func FlattenOption[A any](oo Option[Option[A]]) Option[A] {
	if oo.IsNone() {
		return None[A]()
	}
	if oo.some.IsNone() {
		return None[A]()
	}

	return oo.some
}

// ChainOption transforms a function A -> Option[B] into one that accepts an
// Option[A] as an argument.
//
// ChainOption : (A -> Option[B]) -> Option[A] -> Option[B].
func ChainOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] {
	return func(o Option[A]) Option[B] {
		if o.isSome {
			return f(o.some)
		}

		return None[B]()
	}
}

// MapOption transforms a pure function A -> B into one that will operate
// inside the Option context.
//
// MapOption : (A -> B) -> Option[A] -> Option[B].
func MapOption[A, B any](f func(A) B) func(Option[A]) Option[B] {
	return func(o Option[A]) Option[B] {
		if o.isSome {
			return Some(f(o.some))
		}

		return None[B]()
	}
}

// MapOptionZ transforms a pure function A -> B into one that will operate
// inside the Option context. Unlike MapOption, this function will return the
// default/zero argument of the return type if the Option is empty.
func MapOptionZ[A, B any](o Option[A], f func(A) B) B {
	var zero B

	if o.IsNone() {
		return zero
	}

	return f(o.some)
}

// LiftA2Option transforms a pure function (A, B) -> C into one that will
// operate in an Option context. For the returned function, if either of its
// arguments are None, then the result will be None.
//
// LiftA2Option : ((A, B) -> C) -> (Option[A], Option[B]) -> Option[C].
func LiftA2Option[A, B, C any](
	f func(A, B) C,
) func(Option[A], Option[B]) Option[C] {

	return func(o1 Option[A], o2 Option[B]) Option[C] {
		if o1.isSome && o2.isSome {
			return Some(f(o1.some, o2.some))
		}

		return None[C]()
	}
}

// Alt chooses the left Option if it is full, otherwise it chooses the right
// option. This can be useful in a long chain if you want to choose between
// many different ways of producing the needed value.
//
// Alt : Option[A] -> Option[A] -> Option[A].
func (o Option[A]) Alt(o2 Option[A]) Option[A] {
	if o.isSome {
		return o
	}

	return o2
}

// UnsafeFromSome can be used to extract the internal value. This will panic
// if the value is None() though.
func (o Option[A]) UnsafeFromSome() A {
	if o.isSome {
		return o.some
	}
	panic("Option was None()")
}
