// 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 di

import (
	"fmt"
	"strconv"
	"sync/atomic"

	DIE "github.com/IBM/fp-go/di/erasure"
	E "github.com/IBM/fp-go/either"
	IO "github.com/IBM/fp-go/io"
	IOE "github.com/IBM/fp-go/ioeither"
	IOO "github.com/IBM/fp-go/iooption"
	O "github.com/IBM/fp-go/option"
)

// Dependency describes the relationship to a service, that has a type and
// a behaviour such as required, option or lazy
type Dependency[T any] interface {
	DIE.Dependency
	// Unerase converts a value with erased type signature into a strongly typed value
	Unerase(val any) E.Either[error, T]
}

// InjectionToken uniquely identifies a dependency by giving it an Id, Type and name
type InjectionToken[T any] interface {
	Dependency[T]
	// Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`.
	// If the dependency cannot be resolved, the resolution process fails
	Identity() Dependency[T]
	// Option identifies this dependency as optional, it will be resolved eagerly and injected as [O.Option[T]].
	// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as [O.None[T]]
	Option() Dependency[O.Option[T]]
	// IOEither identifies this dependency as mandatory but it will be resolved lazily as a [IOE.IOEither[error, T]]. This
	// value is memoized to make sure the dependency is a singleton.
	// If the dependency cannot be resolved, the resolution process fails
	IOEither() Dependency[IOE.IOEither[error, T]]
	// IOOption identifies this dependency as optional but it will be resolved lazily as a [IOO.IOOption[T]]. This
	// value is memoized to make sure the dependency is a singleton.
	// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value.
	IOOption() Dependency[IOO.IOOption[T]]
}

// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name that can have multiple implementations.
// Implementations are provided via the [MultiInjectionToken.Item] injection token.
type MultiInjectionToken[T any] interface {
	// Container returns the injection token used to request an array of all provided items
	Container() InjectionToken[[]T]
	// Item returns the injection token used to provide an item
	Item() InjectionToken[T]
}

// makeID creates a generator of unique string IDs
func makeID() IO.IO[string] {
	var count atomic.Int64
	return IO.MakeIO(func() string {
		return strconv.FormatInt(count.Add(1), 16)
	})
}

// genID is the common generator of unique string IDs
var genID = makeID()

type tokenBase struct {
	name            string
	id              string
	flag            int
	providerFactory O.Option[DIE.ProviderFactory]
}

type token[T any] struct {
	base   *tokenBase
	toType func(val any) E.Either[error, T]
}

func (t *token[T]) Id() string {
	return t.base.id
}

func (t *token[T]) Flag() int {
	return t.base.flag
}

func (t *token[T]) String() string {
	return t.base.name
}

func (t *token[T]) Unerase(val any) E.Either[error, T] {
	return t.toType(val)
}

func (t *token[T]) ProviderFactory() O.Option[DIE.ProviderFactory] {
	return t.base.providerFactory
}
func makeTokenBase(name string, id string, typ int, providerFactory O.Option[DIE.ProviderFactory]) *tokenBase {
	return &tokenBase{name, id, typ, providerFactory}
}

func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T], providerFactory O.Option[DIE.ProviderFactory]) Dependency[T] {
	return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
}

type injectionToken[T any] struct {
	token[T]
	option   Dependency[O.Option[T]]
	ioeither Dependency[IOE.IOEither[error, T]]
	iooption Dependency[IOO.IOOption[T]]
}

type multiInjectionToken[T any] struct {
	container *injectionToken[[]T]
	item      *injectionToken[T]
}

func (i *injectionToken[T]) Identity() Dependency[T] {
	return i
}

func (i *injectionToken[T]) Option() Dependency[O.Option[T]] {
	return i.option
}

func (i *injectionToken[T]) IOEither() Dependency[IOE.IOEither[error, T]] {
	return i.ioeither
}

func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] {
	return i.iooption
}

func (i *injectionToken[T]) ProviderFactory() O.Option[DIE.ProviderFactory] {
	return i.base.providerFactory
}

func (m *multiInjectionToken[T]) Container() InjectionToken[[]T] {
	return m.container
}

func (m *multiInjectionToken[T]) Item() InjectionToken[T] {
	return m.item
}

// makeToken create a unique [InjectionToken] for a specific type
func makeInjectionToken[T any](name string, providerFactory O.Option[DIE.ProviderFactory]) InjectionToken[T] {
	id := genID()
	toIdentity := toType[T]()
	return &injectionToken[T]{
		token[T]{makeTokenBase(name, id, DIE.Identity, providerFactory), toIdentity},
		makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType(toIdentity), providerFactory),
		makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType(toIdentity), providerFactory),
		makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType(toIdentity), providerFactory),
	}
}

// MakeToken create a unique [InjectionToken] for a specific type
func MakeToken[T any](name string) InjectionToken[T] {
	return makeInjectionToken[T](name, O.None[DIE.ProviderFactory]())
}

// MakeToken create a unique [InjectionToken] for a specific type
func MakeTokenWithDefault[T any](name string, providerFactory DIE.ProviderFactory) InjectionToken[T] {
	return makeInjectionToken[T](name, O.Of(providerFactory))
}

// MakeMultiToken creates a [MultiInjectionToken]
func MakeMultiToken[T any](name string) MultiInjectionToken[T] {
	id := genID()
	toItem := toType[T]()
	toContainer := toArrayType(toItem)
	containerName := fmt.Sprintf("Container[%s]", name)
	itemName := fmt.Sprintf("Item[%s]", name)
	// empty factory
	providerFactory := O.None[DIE.ProviderFactory]()
	// container
	container := &injectionToken[[]T]{
		token[[]T]{makeTokenBase(containerName, id, DIE.Multi|DIE.Identity, providerFactory), toContainer},
		makeToken[O.Option[[]T]](fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer), providerFactory),
		makeToken[IOE.IOEither[error, []T]](fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer), providerFactory),
		makeToken[IOO.IOOption[[]T]](fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, toIOOptionType(toContainer), providerFactory),
	}
	// item
	item := &injectionToken[T]{
		token[T]{makeTokenBase(itemName, id, DIE.Item|DIE.Identity, providerFactory), toItem},
		makeToken[O.Option[T]](fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem), providerFactory),
		makeToken[IOE.IOEither[error, T]](fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem), providerFactory),
		makeToken[IOO.IOOption[T]](fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem), providerFactory),
	}
	// returns the token
	return &multiInjectionToken[T]{container, item}
}
