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

import (
	"fmt"
	"time"

	A "github.com/IBM/fp-go/array"
	E "github.com/IBM/fp-go/either"
	"github.com/IBM/fp-go/errors"
	F "github.com/IBM/fp-go/function"
	I "github.com/IBM/fp-go/identity"
	IOE "github.com/IBM/fp-go/ioeither"
	N "github.com/IBM/fp-go/number"
	O "github.com/IBM/fp-go/option"
	"github.com/IBM/fp-go/ord"
	S "github.com/IBM/fp-go/string"
)

type Account struct {
	Balance float32
}

func MakeAccount(b float32) Account {
	return Account{Balance: b}
}

func getBalance(a Account) float32 {
	return a.Balance
}

type (
	Chapter08User struct {
		Id     int
		Name   string
		Active bool
		Saved  bool
	}
)

var (
	albert08 = Chapter08User{
		Id:     1,
		Active: true,
		Name:   "Albert",
	}

	gary08 = Chapter08User{
		Id:     2,
		Active: false,
		Name:   "Gary",
	}

	theresa08 = Chapter08User{
		Id:     3,
		Active: true,
		Name:   "Theresa",
	}

	yi08 = Chapter08User{Id: 4, Name: "Yi", Active: true}
)

func (u Chapter08User) getName() string {
	return u.Name
}

func (u Chapter08User) isActive() bool {
	return u.Active
}

var (
	ordFloat32       = ord.FromStrictCompare[float32]()
	UpdateLedger     = F.Identity[Account]
	RemainingBalance = F.Flow2(
		getBalance,
		S.Format[float32]("Your balance is $%0.2f"),
	)
	FinishTransaction = F.Flow2(
		UpdateLedger,
		RemainingBalance,
	)
	getTwenty = F.Flow2(
		Withdraw(20),
		O.Fold(F.Constant("You're broke!"), FinishTransaction),
	)

	// showWelcome :: User -> String
	showWelcome = F.Flow2(
		Chapter08User.getName,
		S.Format[string]("Welcome %s"),
	)

	// checkActive :: User -> Either error User
	checkActive = E.FromPredicate(Chapter08User.isActive, F.Constant1[Chapter08User](fmt.Errorf("your account is not active")))

	// validateUser :: (User -> Either String ()) -> User -> Either String User
	validateUser = F.Curry2(func(validate func(Chapter08User) E.Either[error, any], user Chapter08User) E.Either[error, Chapter08User] {
		return F.Pipe2(
			user,
			validate,
			E.MapTo[error, any](user),
		)
	})

	// save :: User -> IOEither error User
	save = func(user Chapter08User) IOE.IOEither[error, Chapter08User] {
		return IOE.FromIO[error](func() Chapter08User {
			var u = user
			u.Saved = true
			return u
		})
	}
)

func Withdraw(amount float32) func(account Account) O.Option[Account] {

	return F.Flow3(
		getBalance,
		O.FromPredicate(ord.Geq(ordFloat32)(amount)),
		O.Map(F.Flow2(
			N.Add(-amount),
			MakeAccount,
		)))
}

type User struct {
	BirthDate string
}

func getBirthDate(u User) string {
	return u.BirthDate
}

func MakeUser(d string) User {
	return User{BirthDate: d}
}

var parseDate = F.Bind1of2(E.Eitherize2(time.Parse))(time.DateOnly)

func GetAge(now time.Time) func(User) E.Either[error, float64] {
	return F.Flow3(
		getBirthDate,
		parseDate,
		E.Map[error](F.Flow3(
			now.Sub,
			time.Duration.Hours,
			N.Mul(1/24.0),
		)),
	)
}

func Example_widthdraw() {
	fmt.Println(getTwenty(MakeAccount(200)))
	fmt.Println(getTwenty(MakeAccount(10)))

	// Output:
	// Your balance is $180.00
	// You're broke!
}

func Example_getAge() {
	now, err := time.Parse(time.DateOnly, "2023-09-01")
	if err != nil {
		panic(err)
	}

	fmt.Println(GetAge(now)(MakeUser("2005-12-12")))
	fmt.Println(GetAge(now)(MakeUser("July 4, 2001")))

	fortune := F.Flow3(
		N.Add(365.0),
		S.Format[float64]("%0.0f"),
		Concat("If you survive, you will be "),
	)

	zoltar := F.Flow3(
		GetAge(now),
		E.Map[error](fortune),
		E.GetOrElse(errors.ToString),
	)

	fmt.Println(zoltar(MakeUser("2005-12-12")))

	// Output:
	// Right[float64](6472)
	// Left[*time.ParseError](parsing time "July 4, 2001" as "2006-01-02": cannot parse "July 4, 2001" as "2006")
	// If you survive, you will be 6837
}

func Example_solution08A() {
	incrF := I.Map(N.Add(1))

	fmt.Println(incrF(I.Of(2)))

	// Output: 3
}

func Example_solution08B() {
	// initial :: User -> Option rune
	initial := F.Flow3(
		Chapter08User.getName,
		S.ToRunes,
		A.Head[rune],
	)

	fmt.Println(initial(albert08))

	// Output:
	// Some[int32](65)
}

func Example_solution08C() {

	// eitherWelcome :: User -> Either String String
	eitherWelcome := F.Flow2(
		checkActive,
		E.Map[error](showWelcome),
	)

	fmt.Println(eitherWelcome(gary08))
	fmt.Println(eitherWelcome(theresa08))

	// Output:
	// Left[*errors.errorString](your account is not active)
	// Right[string](Welcome Theresa)
}

func Example_solution08D() {

	// // validateName :: User -> Either String ()
	validateName := F.Flow3(
		Chapter08User.getName,
		E.FromPredicate(F.Flow2(
			S.Size,
			ord.Gt(ord.FromStrictCompare[int]())(3),
		), errors.OnSome[string]("Your name %s is larger than 3 characters")),
		E.Map[error](F.ToAny[string]),
	)

	saveAndWelcome := F.Flow2(
		save,
		IOE.Map[error](showWelcome),
	)

	register := F.Flow3(
		validateUser(validateName),
		IOE.FromEither[error, Chapter08User],
		IOE.Chain(saveAndWelcome),
	)

	fmt.Println(validateName(gary08))
	fmt.Println(validateName(yi08))

	fmt.Println(register(albert08)())
	fmt.Println(register(yi08)())

	// Output:
	// Right[string](Gary)
	// Left[*errors.errorString](Your name Yi is larger than 3 characters)
	// Right[string](Welcome Albert)
	// Left[*errors.errorString](Your name Yi is larger than 3 characters)
}
