// Copyright (c) 2024 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 pair

import (
	"fmt"

	F "github.com/IBM/fp-go/function"
	Sg "github.com/IBM/fp-go/semigroup"
	T "github.com/IBM/fp-go/tuple"
)

type (
	pair struct {
		h, t any
	}

	// Pair defines a data structure that holds two strongly typed values
	Pair[A, B any] pair
)

// String prints some debug info for the object
//
// go:noinline
func pairString(s *pair) string {
	return fmt.Sprintf("Pair[%T, %T](%v, %v)", s.h, s.t, s.h, s.t)
}

// Format prints some debug info for the object
//
// go:noinline
func pairFormat(e *pair, f fmt.State, c rune) {
	switch c {
	case 's':
		fmt.Fprint(f, pairString(e))
	default:
		fmt.Fprint(f, pairString(e))
	}
}

// String prints some debug info for the object
func (s Pair[A, B]) String() string {
	return pairString((*pair)(&s))
}

// Format prints some debug info for the object
func (s Pair[A, B]) Format(f fmt.State, c rune) {
	pairFormat((*pair)(&s), f, c)
}

// Of creates a [Pair] with the same value to to both fields
func Of[A any](value A) Pair[A, A] {
	return Pair[A, A]{h: value, t: value}
}

// FromTuple creates a [Pair] from a [T.Tuple2]
func FromTuple[A, B any](t T.Tuple2[A, B]) Pair[A, B] {
	return Pair[A, B]{h: t.F1, t: t.F2}
}

// ToTuple creates a [T.Tuple2] from a [Pair]
func ToTuple[A, B any](t Pair[A, B]) T.Tuple2[A, B] {
	return T.MakeTuple2(Head(t), Tail(t))
}

// MakePair creates a [Pair] from two values
func MakePair[A, B any](a A, b B) Pair[A, B] {
	return Pair[A, B]{h: a, t: b}
}

// Head returns the head value of the pair
func Head[A, B any](fa Pair[A, B]) A {
	return fa.h.(A)
}

// Tail returns the head value of the pair
func Tail[A, B any](fa Pair[A, B]) B {
	return fa.t.(B)
}

// MonadMapHead maps the head value
func MonadMapHead[B, A, A1 any](fa Pair[A, B], f func(A) A1) Pair[A1, B] {
	return Pair[A1, B]{f(Head(fa)), fa.t}
}

// MonadMap maps the head value
func MonadMap[B, A, A1 any](fa Pair[A, B], f func(A) A1) Pair[A1, B] {
	return MonadMapHead(fa, f)
}

// MonadMapTail maps the Tail value
func MonadMapTail[A, B, B1 any](fa Pair[A, B], f func(B) B1) Pair[A, B1] {
	return Pair[A, B1]{fa.h, f(Tail(fa))}
}

// MonadBiMap maps both values
func MonadBiMap[A, B, A1, B1 any](fa Pair[A, B], f func(A) A1, g func(B) B1) Pair[A1, B1] {
	return Pair[A1, B1]{f(Head(fa)), g(Tail(fa))}
}

// Map maps the head value
func Map[B, A, A1 any](f func(A) A1) func(Pair[A, B]) Pair[A1, B] {
	return MapHead[B, A, A1](f)
}

// MapHead maps the head value
func MapHead[B, A, A1 any](f func(A) A1) func(Pair[A, B]) Pair[A1, B] {
	return F.Bind2nd(MonadMapHead[B, A, A1], f)
}

// MapTail maps the Tail value
func MapTail[A, B, B1 any](f func(B) B1) func(Pair[A, B]) Pair[A, B1] {
	return F.Bind2nd(MonadMapTail[A, B, B1], f)
}

// BiMap maps both values
func BiMap[A, B, A1, B1 any](f func(A) A1, g func(B) B1) func(Pair[A, B]) Pair[A1, B1] {
	return func(fa Pair[A, B]) Pair[A1, B1] {
		return MonadBiMap(fa, f, g)
	}
}

// MonadChainHead chains on the head value
func MonadChainHead[B, A, A1 any](sg Sg.Semigroup[B], fa Pair[A, B], f func(A) Pair[A1, B]) Pair[A1, B] {
	fb := f(Head(fa))
	return Pair[A1, B]{fb.h, sg.Concat(Tail(fa), Tail(fb))}
}

// MonadChainTail chains on the Tail value
func MonadChainTail[A, B, B1 any](sg Sg.Semigroup[A], fb Pair[A, B], f func(B) Pair[A, B1]) Pair[A, B1] {
	fa := f(Tail(fb))
	return Pair[A, B1]{sg.Concat(Head(fb), Head(fa)), fa.t}
}

// MonadChain chains on the head value
func MonadChain[B, A, A1 any](sg Sg.Semigroup[B], fa Pair[A, B], f func(A) Pair[A1, B]) Pair[A1, B] {
	return MonadChainHead(sg, fa, f)
}

// ChainHead chains on the head value
func ChainHead[B, A, A1 any](sg Sg.Semigroup[B], f func(A) Pair[A1, B]) func(Pair[A, B]) Pair[A1, B] {
	return func(fa Pair[A, B]) Pair[A1, B] {
		return MonadChainHead(sg, fa, f)
	}
}

// ChainTail chains on the Tail value
func ChainTail[A, B, B1 any](sg Sg.Semigroup[A], f func(B) Pair[A, B1]) func(Pair[A, B]) Pair[A, B1] {
	return func(fa Pair[A, B]) Pair[A, B1] {
		return MonadChainTail(sg, fa, f)
	}
}

// Chain chains on the head value
func Chain[B, A, A1 any](sg Sg.Semigroup[B], f func(A) Pair[A1, B]) func(Pair[A, B]) Pair[A1, B] {
	return ChainHead[B, A, A1](sg, f)
}

// MonadApHead applies on the head value
func MonadApHead[B, A, A1 any](sg Sg.Semigroup[B], faa Pair[func(A) A1, B], fa Pair[A, B]) Pair[A1, B] {
	return Pair[A1, B]{Head(faa)(Head(fa)), sg.Concat(Tail(fa), Tail(faa))}
}

// MonadApTail applies on the Tail value
func MonadApTail[A, B, B1 any](sg Sg.Semigroup[A], fbb Pair[A, func(B) B1], fb Pair[A, B]) Pair[A, B1] {
	return Pair[A, B1]{sg.Concat(Head(fb), Head(fbb)), Tail(fbb)(Tail(fb))}
}

// MonadAp applies on the head value
func MonadAp[B, A, A1 any](sg Sg.Semigroup[B], faa Pair[func(A) A1, B], fa Pair[A, B]) Pair[A1, B] {
	return MonadApHead(sg, faa, fa)
}

// ApHead applies on the head value
func ApHead[B, A, A1 any](sg Sg.Semigroup[B], fa Pair[A, B]) func(Pair[func(A) A1, B]) Pair[A1, B] {
	return func(faa Pair[func(A) A1, B]) Pair[A1, B] {
		return MonadApHead(sg, faa, fa)
	}
}

// ApTail applies on the Tail value
func ApTail[A, B, B1 any](sg Sg.Semigroup[A], fb Pair[A, B]) func(Pair[A, func(B) B1]) Pair[A, B1] {
	return func(fbb Pair[A, func(B) B1]) Pair[A, B1] {
		return MonadApTail(sg, fbb, fb)
	}
}

// Ap applies on the head value
func Ap[B, A, A1 any](sg Sg.Semigroup[B], fa Pair[A, B]) func(Pair[func(A) A1, B]) Pair[A1, B] {
	return ApHead[B, A, A1](sg, fa)
}

// Swap swaps the two channels
func Swap[A, B any](fa Pair[A, B]) Pair[B, A] {
	return MakePair(Tail(fa), Head(fa))
}

// Paired converts a function with 2 parameters into a function taking a [Pair]
// The inverse function is [Unpaired]
func Paired[F ~func(T1, T2) R, T1, T2, R any](f F) func(Pair[T1, T2]) R {
	return func(t Pair[T1, T2]) R {
		return f(Head(t), Tail(t))
	}
}

// Unpaired converts a function with a [Pair] parameter into a function with 2 parameters
// The inverse function is [Paired]
func Unpaired[F ~func(Pair[T1, T2]) R, T1, T2, R any](f F) func(T1, T2) R {
	return func(t1 T1, t2 T2) R {
		return f(MakePair(t1, t2))
	}
}

func Merge[F ~func(B) func(A) R, A, B, R any](f F) func(Pair[A, B]) R {
	return func(p Pair[A, B]) R {
		return f(Tail(p))(Head(p))
	}
}
