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

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	C "github.com/urfave/cli/v2"
)

func generateNestedCallbacks(i, total int) string {
	var buf strings.Builder
	for j := i; j < total; j++ {
		if j > i {
			buf.WriteString(" ")
		}
		buf.WriteString(fmt.Sprintf("func(T%d)", j+1))
	}
	if i > 0 {
		buf.WriteString(" ")
	}
	buf.WriteString(tupleType("T")(total))
	return buf.String()
}

func generateContextReaderIOEitherTraverseTuple(suffix string) func(f, fg *os.File, i int) {
	return func(f, fg *os.File, i int) {
		// tupleT type
		tupleT := tupleType("T")(i)
		tupleA := tupleType("A")(i)

		// non-generic version
		// generic version
		fmt.Fprintf(f, "\n// Traverse%sTuple%d converts a [T.Tuple%d] of [A] via transformer functions transforming [A] to a [ReaderIOEither] into a [ReaderIOEither] of a [T.Tuple%d].\n", suffix, i, i, i)
		fmt.Fprintf(f, "func Traverse%sTuple%d[", suffix, i)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "F%d ~func(A%d) ReaderIOEither[T%d]", j+1, j+1, j+1)
		}
		for j := 0; j < i; j++ {
			fmt.Fprintf(f, ", A%d, T%d", j+1, j+1)
		}
		fmt.Fprintf(f, " any](")
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "f%d F%d", j+1, j+1)
		}
		fmt.Fprintf(f, ") func(%s) ReaderIOEither[%s] {\n", tupleA, tupleT)
		fmt.Fprintf(f, "  return G.Traverse%sTuple%d[ReaderIOEither[%s]](", suffix, i, tupleT)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "f%d", j+1)
		}
		fmt.Fprintf(f, ")\n")
		fmt.Fprintf(f, "}\n")

		// generic version
		fmt.Fprintf(fg, "\n// Traverse%sTuple%d converts a [T.Tuple%d] of readers into a reader of a [T.Tuple%d].\n", suffix, i, i, i)
		fmt.Fprintf(fg, "func Traverse%sTuple%d[\n", suffix, i)
		fmt.Fprintf(fg, "  GR_TUPLE%d ~func(context.Context) GIO_TUPLE%d,\n", i, i)
		// the transformation functions
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  F%d ~func(A%d) GR_T%d,\n", j+1, j+1, j+1)
		}
		// the readers
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  GR_T%d ~func(context.Context) GIO_T%d,\n", j+1, j+1)
		}
		// the tuples
		fmt.Fprintf(fg, "  GIO_TUPLE%d ~func() E.Either[error, %s],\n", i, tupleT)
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  GIO_T%d ~func() E.Either[error, T%d],\n", j+1, j+1)
		}
		// input and result parameters
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  A%d,\n", j+1)
			fmt.Fprintf(fg, "  T%d", j+1)
			if j < i-1 {
				fmt.Fprintf(fg, ",\n")
			}
		}
		fmt.Fprintf(fg, " any](")
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(fg, ", ")
			}
			fmt.Fprintf(fg, "f%d F%d", j+1, j+1)
		}
		fmt.Fprintf(fg, ") func(%s) GR_TUPLE%d {\n", tupleA, i)
		fmt.Fprintf(fg, "  return func(t %s) GR_TUPLE%d {\n", tupleA, i)
		fmt.Fprintf(fg, "    return A.TraverseTuple%d(\n", i)
		// map call
		var cr string
		if i > 1 {
			cb := generateNestedCallbacks(1, i)
			cio := fmt.Sprintf("func() E.Either[error, %s]", cb)
			cr = fmt.Sprintf("func(context.Context) %s", cio)
		} else {
			cr = fmt.Sprintf("GR_TUPLE%d", i)
		}
		fmt.Fprintf(fg, "      Map[GR_T%d, %s, GIO_T%d],\n", 1, cr, 1)
		// the apply calls
		for j := 1; j < i; j++ {
			if j < i-1 {
				cb := generateNestedCallbacks(j+1, i)
				cio := fmt.Sprintf("func() E.Either[error, %s]", cb)
				cr = fmt.Sprintf("func(context.Context) %s", cio)
			} else {
				cr = fmt.Sprintf("GR_TUPLE%d", i)
			}
			fmt.Fprintf(fg, "      Ap%s[%s, func(context.Context) func() E.Either[error, %s], GR_T%d],\n", suffix, cr, generateNestedCallbacks(j, i), j+1)
		}
		// function parameters
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "      f%d,\n", j+1)
		}

		// raw parameters
		fmt.Fprintf(fg, "      t,\n")

		fmt.Fprintf(fg, "    )\n")
		fmt.Fprintf(fg, "  }\n")
		fmt.Fprintf(fg, "}\n")
	}
}

func generateContextReaderIOEitherSequenceTuple(suffix string) func(f, fg *os.File, i int) {
	return func(f, fg *os.File, i int) {
		// tuple type
		tuple := tupleType("T")(i)

		// non-generic version
		// generic version
		fmt.Fprintf(f, "\n// Sequence%sTuple%d converts a [T.Tuple%d] of [ReaderIOEither] into a [ReaderIOEither] of a [T.Tuple%d].\n", suffix, i, i, i)
		fmt.Fprintf(f, "func Sequence%sTuple%d[", suffix, i)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "T%d", j+1)
		}
		fmt.Fprintf(f, " any](t T.Tuple%d[", i)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "ReaderIOEither[T%d]", j+1)
		}
		fmt.Fprintf(f, "]) ReaderIOEither[%s] {\n", tuple)
		fmt.Fprintf(f, "  return G.Sequence%sTuple%d[ReaderIOEither[%s]](t)\n", suffix, i, tuple)
		fmt.Fprintf(f, "}\n")

		// generic version
		fmt.Fprintf(fg, "\n// Sequence%sTuple%d converts a [T.Tuple%d] of readers into a reader of a [T.Tuple%d].\n", suffix, i, i, i)
		fmt.Fprintf(fg, "func Sequence%sTuple%d[\n", suffix, i)

		fmt.Fprintf(fg, "  GR_TUPLE%d ~func(context.Context) GIO_TUPLE%d,\n", i, i)
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  GR_T%d ~func(context.Context) GIO_T%d,\n", j+1, j+1)
		}

		fmt.Fprintf(fg, "  GIO_TUPLE%d ~func() E.Either[error, %s],\n", i, tuple)
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  GIO_T%d ~func() E.Either[error, T%d],\n", j+1, j+1)
		}

		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  T%d", j+1)
			if j < i-1 {
				fmt.Fprintf(fg, ",\n")
			}
		}
		fmt.Fprintf(fg, " any](t T.Tuple%d[", i)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(fg, ", ")
			}
			fmt.Fprintf(fg, "GR_T%d", j+1)
		}
		fmt.Fprintf(fg, "]) GR_TUPLE%d {\n", i)
		fmt.Fprintf(fg, "  return A.SequenceTuple%d(\n", i)
		// map call
		var cr string
		if i > 1 {
			cb := generateNestedCallbacks(1, i)
			cio := fmt.Sprintf("func() E.Either[error, %s]", cb)
			cr = fmt.Sprintf("func(context.Context) %s", cio)
		} else {
			cr = fmt.Sprintf("GR_TUPLE%d", i)
		}
		fmt.Fprintf(fg, "    Map[GR_T%d, %s, GIO_T%d],\n", 1, cr, 1)
		// the apply calls
		for j := 1; j < i; j++ {
			if j < i-1 {
				cb := generateNestedCallbacks(j+1, i)
				cio := fmt.Sprintf("func() E.Either[error, %s]", cb)
				cr = fmt.Sprintf("func(context.Context) %s", cio)
			} else {
				cr = fmt.Sprintf("GR_TUPLE%d", i)
			}
			fmt.Fprintf(fg, "    Ap%s[%s, func(context.Context) func() E.Either[error, %s], GR_T%d],\n", suffix, cr, generateNestedCallbacks(j, i), j+1)
		}
		// raw parameters
		fmt.Fprintf(fg, "    t,\n")

		fmt.Fprintf(fg, " )\n")
		fmt.Fprintf(fg, "}\n")
	}
}

func generateContextReaderIOEitherSequenceT(suffix string) func(f, fg *os.File, i int) {
	return func(f, fg *os.File, i int) {
		// tuple type
		tuple := tupleType("T")(i)

		// non-generic version
		// generic version
		fmt.Fprintf(f, "\n// Sequence%sT%d converts %d [ReaderIOEither] into a [ReaderIOEither] of a [T.Tuple%d].\n", suffix, i, i, i)
		fmt.Fprintf(f, "func Sequence%sT%d[", suffix, i)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "T%d", j+1)
		}
		fmt.Fprintf(f, " any](")
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "t%d ReaderIOEither[T%d]", j+1, j+1)
		}
		fmt.Fprintf(f, ") ReaderIOEither[%s] {\n", tuple)
		fmt.Fprintf(f, "  return G.Sequence%sT%d[ReaderIOEither[%s]](", suffix, i, tuple)
		for j := 0; j < i; j++ {
			if j > 0 {
				fmt.Fprintf(f, ", ")
			}
			fmt.Fprintf(f, "t%d", j+1)
		}
		fmt.Fprintf(f, ")\n")
		fmt.Fprintf(f, "}\n")

		// generic version
		fmt.Fprintf(fg, "\n// Sequence%sT%d converts %d readers into a reader of a [T.Tuple%d].\n", suffix, i, i, i)
		fmt.Fprintf(fg, "func Sequence%sT%d[\n", suffix, i)

		fmt.Fprintf(fg, "  GR_TUPLE%d ~func(context.Context) GIO_TUPLE%d,\n", i, i)
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  GR_T%d ~func(context.Context) GIO_T%d,\n", j+1, j+1)
		}

		fmt.Fprintf(fg, "  GIO_TUPLE%d ~func() E.Either[error, %s],\n", i, tuple)
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  GIO_T%d ~func() E.Either[error, T%d],\n", j+1, j+1)
		}

		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  T%d", j+1)
			if j < i-1 {
				fmt.Fprintf(fg, ",\n")
			}
		}
		fmt.Fprintf(fg, " any](\n")
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "  t%d GR_T%d,\n", j+1, j+1)
		}
		fmt.Fprintf(fg, ") GR_TUPLE%d {\n", i)
		fmt.Fprintf(fg, "  return A.SequenceT%d(\n", i)
		// map call
		var cr string
		if i > 1 {
			cb := generateNestedCallbacks(1, i)
			cio := fmt.Sprintf("func() E.Either[error, %s]", cb)
			cr = fmt.Sprintf("func(context.Context) %s", cio)
		} else {
			cr = fmt.Sprintf("GR_TUPLE%d", i)
		}
		fmt.Fprintf(fg, "    Map[GR_T%d, %s, GIO_T%d],\n", 1, cr, 1)
		// the apply calls
		for j := 1; j < i; j++ {
			if j < i-1 {
				cb := generateNestedCallbacks(j+1, i)
				cio := fmt.Sprintf("func() E.Either[error, %s]", cb)
				cr = fmt.Sprintf("func(context.Context) %s", cio)
			} else {
				cr = fmt.Sprintf("GR_TUPLE%d", i)
			}
			fmt.Fprintf(fg, "    Ap%s[%s, func(context.Context) func() E.Either[error, %s], GR_T%d],\n", suffix, cr, generateNestedCallbacks(j, i), j+1)
		}
		// raw parameters
		for j := 0; j < i; j++ {
			fmt.Fprintf(fg, "    t%d,\n", j+1)
		}

		fmt.Fprintf(fg, " )\n")
		fmt.Fprintf(fg, "}\n")
	}
}

func generateContextReaderIOEitherEitherize(f, fg *os.File, i int) {
	// non generic version
	fmt.Fprintf(f, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [ReaderIOEither[R]]\n// The inverse function is [Uneitherize%d]\n", i, i, i, i)
	fmt.Fprintf(f, "func Eitherize%d[F ~func(context.Context", i)
	for j := 0; j < i; j++ {
		fmt.Fprintf(f, ", T%d", j)
	}
	fmt.Fprintf(f, ") (R, error)")
	for j := 0; j < i; j++ {
		fmt.Fprintf(f, ", T%d", j)
	}
	fmt.Fprintf(f, ", R any](f F) func(")
	for j := 0; j < i; j++ {
		if j > 0 {
			fmt.Fprintf(f, ", ")
		}
		fmt.Fprintf(f, "T%d", j)
	}
	fmt.Fprintf(f, ") ReaderIOEither[R] {\n")
	fmt.Fprintf(f, "  return G.Eitherize%d[ReaderIOEither[R]](f)\n", i)
	fmt.Fprintln(f, "}")

	// generic version
	fmt.Fprintf(fg, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [GRA]\n// The inverse function is [Uneitherize%d]\n", i, i, i, i)
	fmt.Fprintf(fg, "func Eitherize%d[GRA ~func(context.Context) GIOA, F ~func(context.Context", i)
	for j := 0; j < i; j++ {
		fmt.Fprintf(fg, ", T%d", j)
	}
	fmt.Fprintf(fg, ") (R, error), GIOA ~func() E.Either[error, R]")
	for j := 0; j < i; j++ {
		fmt.Fprintf(fg, ", T%d", j)
	}
	fmt.Fprintf(fg, ", R any](f F) func(")
	for j := 0; j < i; j++ {
		if j > 0 {
			fmt.Fprintf(fg, ", ")
		}
		fmt.Fprintf(fg, "T%d", j)
	}
	fmt.Fprintf(fg, ") GRA {\n")
	fmt.Fprintf(fg, "  return RE.Eitherize%d[GRA](f)\n", i)
	fmt.Fprintln(fg, "}")
}

func generateContextReaderIOEitherUneitherize(f, fg *os.File, i int) {
	// non generic version
	fmt.Fprintf(f, "\n// Uneitherize%d converts a function with %d parameters returning a [ReaderIOEither[R]] into a function with %d parameters returning a tuple.\n// The first parameter is considered to be the [context.Context].\n", i, i+1, i)
	fmt.Fprintf(f, "func Uneitherize%d[F ~func(", i)
	for j := 0; j < i; j++ {
		if j > 0 {
			fmt.Fprintf(f, ", ")
		}
		fmt.Fprintf(f, "T%d", j)
	}
	fmt.Fprintf(f, ") ReaderIOEither[R]")
	for j := 0; j < i; j++ {
		fmt.Fprintf(f, ", T%d", j)
	}
	fmt.Fprintf(f, ", R any](f F) func(context.Context")
	for j := 0; j < i; j++ {
		fmt.Fprintf(f, ", T%d", j)
	}
	fmt.Fprintf(f, ") (R, error) {\n")
	fmt.Fprintf(f, "  return G.Uneitherize%d[ReaderIOEither[R]", i)

	fmt.Fprintf(f, ", func(context.Context")
	for j := 0; j < i; j++ {
		fmt.Fprintf(f, ", T%d", j)
	}
	fmt.Fprintf(f, ")(R, error)](f)\n")
	fmt.Fprintln(f, "}")

	// generic version
	fmt.Fprintf(fg, "\n// Uneitherize%d converts a function with %d parameters returning a [GRA] into a function with %d parameters returning a tuple.\n// The first parameter is considered to be the [context.Context].\n", i, i, i)
	fmt.Fprintf(fg, "func Uneitherize%d[GRA ~func(context.Context) GIOA, F ~func(context.Context", i)
	for j := 0; j < i; j++ {
		fmt.Fprintf(fg, ", T%d", j)
	}
	fmt.Fprintf(fg, ") (R, error), GIOA ~func() E.Either[error, R]")
	for j := 0; j < i; j++ {
		fmt.Fprintf(fg, ", T%d", j)
	}
	fmt.Fprintf(fg, ", R any](f func(")
	for j := 0; j < i; j++ {
		if j > 0 {
			fmt.Fprintf(fg, ", ")
		}
		fmt.Fprintf(fg, "T%d", j)
	}
	fmt.Fprintf(fg, ") GRA) F {\n")

	fmt.Fprintf(fg, "  return func(c context.Context")
	for j := 0; j < i; j++ {
		fmt.Fprintf(fg, ", t%d T%d", j, j)
	}
	fmt.Fprintf(fg, ") (R, error) {\n")
	fmt.Fprintf(fg, "    return E.UnwrapError(f(")
	for j := 0; j < i; j++ {
		if j > 0 {
			fmt.Fprintf(fg, ", ")
		}
		fmt.Fprintf(fg, "t%d", j)
	}
	fmt.Fprintf(fg, ")(c)())\n")
	fmt.Fprintf(fg, "  }\n")
	fmt.Fprintf(fg, "}\n")
}

func generateContextReaderIOEitherHelpers(filename string, count int) error {
	dir, err := os.Getwd()
	if err != nil {
		return err
	}
	absDir, err := filepath.Abs(dir)
	if err != nil {
		return err
	}
	pkg := filepath.Base(absDir)
	f, err := os.Create(filepath.Clean(filename))
	if err != nil {
		return err
	}
	defer f.Close()
	// construct subdirectory
	genFilename := filepath.Join("generic", filename)
	err = os.MkdirAll("generic", os.ModePerm)
	if err != nil {
		return err
	}
	fg, err := os.Create(filepath.Clean(genFilename))
	if err != nil {
		return err
	}
	defer fg.Close()

	// log
	log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)

	writePackage(f, pkg)

	fmt.Fprintf(f, `
import (
	"context"

	G "github.com/IBM/fp-go/context/%s/generic"
	T "github.com/IBM/fp-go/tuple"
)
`, pkg)

	writePackage(fg, "generic")

	fmt.Fprintf(fg, `
import (
	"context"

	E "github.com/IBM/fp-go/either"
	RE "github.com/IBM/fp-go/readerioeither/generic"
	A "github.com/IBM/fp-go/internal/apply"
	T "github.com/IBM/fp-go/tuple"
)
`)

	generateContextReaderIOEitherEitherize(f, fg, 0)
	generateContextReaderIOEitherUneitherize(f, fg, 0)

	for i := 1; i <= count; i++ {
		// eitherize
		generateContextReaderIOEitherEitherize(f, fg, i)
		generateContextReaderIOEitherUneitherize(f, fg, i)
		// sequenceT
		generateContextReaderIOEitherSequenceT("")(f, fg, i)
		generateContextReaderIOEitherSequenceT("Seq")(f, fg, i)
		generateContextReaderIOEitherSequenceT("Par")(f, fg, i)
		// sequenceTuple
		generateContextReaderIOEitherSequenceTuple("")(f, fg, i)
		generateContextReaderIOEitherSequenceTuple("Seq")(f, fg, i)
		generateContextReaderIOEitherSequenceTuple("Par")(f, fg, i)
		// traverseTuple
		generateContextReaderIOEitherTraverseTuple("")(f, fg, i)
		generateContextReaderIOEitherTraverseTuple("Seq")(f, fg, i)
		generateContextReaderIOEitherTraverseTuple("Par")(f, fg, i)
	}

	return nil
}

func ContextReaderIOEitherCommand() *C.Command {
	return &C.Command{
		Name:  "contextreaderioeither",
		Usage: "generate code for ContextReaderIOEither",
		Flags: []C.Flag{
			flagCount,
			flagFilename,
		},
		Action: func(ctx *C.Context) error {
			return generateContextReaderIOEitherHelpers(
				ctx.String(keyFilename),
				ctx.Int(keyCount),
			)
		},
	}
}
