// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package generic

import (
	A "github.com/IBM/fp-go/array/generic"
	F "github.com/IBM/fp-go/function"
	C "github.com/IBM/fp-go/internal/chain"
	"github.com/IBM/fp-go/internal/utils"
	IO "github.com/IBM/fp-go/iooption/generic"
	M "github.com/IBM/fp-go/monoid"
	N "github.com/IBM/fp-go/number"
	O "github.com/IBM/fp-go/option"
	P "github.com/IBM/fp-go/pair"
)

// Next returns the iterator for the next element in an iterator `P.Pair`
func Next[GU ~func() O.Option[P.Pair[GU, U]], U any](m P.Pair[GU, U]) GU {
	return P.Head(m)
}

// Current returns the current element in an iterator `P.Pair`
func Current[GU ~func() O.Option[P.Pair[GU, U]], U any](m P.Pair[GU, U]) U {
	return P.Tail(m)
}

// From constructs an array from a set of variadic arguments
func From[GU ~func() O.Option[P.Pair[GU, U]], U any](data ...U) GU {
	return FromArray[GU](data)
}

// Empty returns the empty iterator
func Empty[GU ~func() O.Option[P.Pair[GU, U]], U any]() GU {
	return IO.None[GU]()
}

// Of returns an iterator with one single element
func Of[GU ~func() O.Option[P.Pair[GU, U]], U any](a U) GU {
	return IO.Of[GU](P.MakePair(Empty[GU](), a))
}

// FromArray returns an iterator from multiple elements
func FromArray[GU ~func() O.Option[P.Pair[GU, U]], US ~[]U, U any](as US) GU {
	return A.MatchLeft(Empty[GU], func(head U, tail US) GU {
		return func() O.Option[P.Pair[GU, U]] {
			return O.Of(P.MakePair(FromArray[GU](tail), head))
		}
	})(as)
}

// reduce applies a function for each value of the iterator with a floating result
func reduce[GU ~func() O.Option[P.Pair[GU, U]], U, V any](as GU, f func(V, U) V, initial V) V {
	next, ok := O.Unwrap(as())
	current := initial
	for ok {
		// next (with bad side effect)
		current = f(current, Current(next))
		next, ok = O.Unwrap(Next(next)())
	}
	return current
}

// Reduce applies a function for each value of the iterator with a floating result
func Reduce[GU ~func() O.Option[P.Pair[GU, U]], U, V any](f func(V, U) V, initial V) func(GU) V {
	return F.Bind23of3(reduce[GU, U, V])(f, initial)
}

// ToArray converts the iterator to an array
func ToArray[GU ~func() O.Option[P.Pair[GU, U]], US ~[]U, U any](u GU) US {
	return Reduce[GU](A.Append[US], A.Empty[US]())(u)
}

func Map[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) V, U, V any](f FCT) func(ma GU) GV {
	// pre-declare to avoid cyclic reference
	var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GV, V]]

	recurse := func(ma GU) GV {
		return F.Nullary2(
			ma,
			m,
		)
	}

	m = O.Map(P.BiMap(recurse, f))

	return recurse
}

func MonadMap[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU, f func(U) V) GV {
	return Map[GV, GU](f)(ma)
}

func concat[GU ~func() O.Option[P.Pair[GU, U]], U any](right, left GU) GU {
	var m func(ma O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GU, U]]

	recurse := func(left GU) GU {
		return F.Nullary2(left, m)
	}

	m = O.Fold(
		right,
		F.Flow2(
			P.BiMap(recurse, F.Identity[U]),
			O.Some[P.Pair[GU, U]],
		))

	return recurse(left)
}

func Chain[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](f func(U) GV) func(GU) GV {
	// pre-declare to avoid cyclic reference
	var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GV, V]]

	recurse := func(ma GU) GV {
		return F.Nullary2(
			ma,
			m,
		)
	}
	m = O.Chain(
		F.Flow3(
			P.BiMap(recurse, f),
			P.Paired(concat[GV]),
			func(v GV) O.Option[P.Pair[GV, V]] {
				return v()
			},
		),
	)

	return recurse
}

func MonadChain[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU, f func(U) GV) GV {
	return Chain[GV, GU](f)(ma)
}

func MonadChainFirst[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU, f func(U) GV) GU {
	return C.MonadChainFirst(
		MonadChain[GU, GU, U, U],
		MonadMap[GU, GV, V, U],
		ma,
		f,
	)
}

func ChainFirst[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](f func(U) GV) func(GU) GU {
	return C.ChainFirst(
		Chain[GU, GU, U, U],
		Map[GU, GV, func(V) U, V, U],
		f,
	)
}

func Flatten[GV ~func() O.Option[P.Pair[GV, GU]], GU ~func() O.Option[P.Pair[GU, U]], U any](ma GV) GU {
	return MonadChain(ma, F.Identity[GU])
}

// MakeBy returns an [Iterator] with an infinite number of elements initialized with `f(i)`
func MakeBy[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(int) U, U any](f FCT) GU {

	var m func(int) O.Option[P.Pair[GU, U]]

	recurse := func(i int) GU {
		return F.Nullary2(
			F.Constant(i),
			m,
		)
	}

	m = F.Flow3(
		P.Of[int],
		P.BiMap(F.Flow2(
			utils.Inc,
			recurse),
			f),
		O.Of[P.Pair[GU, U]],
	)

	// bootstrap
	return recurse(0)
}

// Replicate creates an infinite [Iterator] containing a value.
func Replicate[GU ~func() O.Option[P.Pair[GU, U]], U any](a U) GU {
	return MakeBy[GU](F.Constant1[int](a))
}

// Repeat creates an [Iterator] containing a value repeated the specified number of times.
// Alias of [Replicate] combined with [Take]
func Repeat[GU ~func() O.Option[P.Pair[GU, U]], U any](n int, a U) GU {
	return F.Pipe2(
		a,
		Replicate[GU],
		Take[GU](n),
	)
}

// Count creates an [Iterator] containing a consecutive sequence of integers starting with the provided start value
func Count[GU ~func() O.Option[P.Pair[GU, int]]](start int) GU {
	return MakeBy[GU](N.Add(start))
}

func FilterMap[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) O.Option[V], U, V any](f FCT) func(ma GU) GV {
	// pre-declare to avoid cyclic reference
	var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GV, V]]

	recurse := func(ma GU) GV {
		return F.Nullary2(
			ma,
			m,
		)
	}

	m = O.Fold(
		Empty[GV](),
		func(t P.Pair[GU, U]) O.Option[P.Pair[GV, V]] {
			r := recurse(Next(t))
			return O.MonadFold(f(Current(t)), r, F.Flow2(
				F.Bind1st(P.MakePair[GV, V], r),
				O.Some[P.Pair[GV, V]],
			))
		},
	)

	return recurse
}

func Filter[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) bool, U any](f FCT) func(ma GU) GU {
	return FilterMap[GU, GU](O.FromPredicate(f))
}

func Ap[GUV ~func() O.Option[P.Pair[GUV, func(U) V]], GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU) func(fab GUV) GV {
	return Chain[GV, GUV](F.Bind1st(MonadMap[GV, GU], ma))
}

func MonadAp[GUV ~func() O.Option[P.Pair[GUV, func(U) V]], GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](fab GUV, ma GU) GV {
	return Ap[GUV, GV, GU](ma)(fab)
}

func FilterChain[GVV ~func() O.Option[P.Pair[GVV, GV]], GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) O.Option[GV], U, V any](f FCT) func(ma GU) GV {
	return F.Flow2(
		FilterMap[GVV, GU](f),
		Flatten[GVV],
	)
}

func FoldMap[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) V, U, V any](m M.Monoid[V]) func(FCT) func(ma GU) V {
	return func(f FCT) func(ma GU) V {
		return Reduce[GU](func(cur V, a U) V {
			return m.Concat(cur, f(a))
		}, m.Empty())
	}
}

func Fold[GU ~func() O.Option[P.Pair[GU, U]], U any](m M.Monoid[U]) func(ma GU) U {
	return Reduce[GU](m.Concat, m.Empty())
}
