package css

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"github.com/gohugoio/hugo/common/hmaps"
	"github.com/gohugoio/hugo/common/hugo"
	"github.com/gohugoio/hugo/common/paths"
	"github.com/gohugoio/hugo/common/types/css"
	"github.com/gohugoio/hugo/deps"
	"github.com/gohugoio/hugo/resources"
	"github.com/gohugoio/hugo/resources/resource"
	"github.com/gohugoio/hugo/resources/resource_transformers/babel"
	"github.com/gohugoio/hugo/resources/resource_transformers/cssjs"
	jstransform "github.com/gohugoio/hugo/resources/resource_transformers/js"
	"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
	"github.com/gohugoio/hugo/resources/resource_transformers/tocss/sass"
	"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
	"github.com/gohugoio/hugo/tpl/internal"
	"github.com/gohugoio/hugo/tpl/internal/resourcehelpers"
	"github.com/spf13/cast"
)

const name = "css"

// Namespace provides template functions for the "css" namespace.
type Namespace struct {
	d                 *deps.Deps
	scssClientLibSass *scss.Client
	postcssClient     *cssjs.PostCSSClient
	tailwindcssClient *cssjs.TailwindCSSClient
	babelClient       *babel.Client
	jsTransformClient *jstransform.Client

	// The Dart Client requires a os/exec process, so  only
	// create it if we really need it.
	// This is mostly to avoid creating one per site build test.
	scssClientDartSassInit sync.Once
	scssClientDartSass     *dartsass.Client
}

// Quoted returns a string that needs to be quoted in CSS.
func (ns *Namespace) Quoted(v any) css.QuotedString {
	s := cast.ToString(v)
	return css.QuotedString(s)
}

// Unquoted returns a string that does not need to be quoted in CSS.
func (ns *Namespace) Unquoted(v any) css.UnquotedString {
	s := cast.ToString(v)
	return css.UnquotedString(s)
}

// PostCSS processes the given Resource with PostCSS.
func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
	if len(args) > 2 {
		return nil, errors.New("must not provide more arguments than resource object and options")
	}

	r, m, err := resourcehelpers.ResolveArgs(args)
	if err != nil {
		return nil, err
	}

	return ns.postcssClient.Process(r, m)
}

// TailwindCSS processes the given Resource with tailwindcss.
func (ns *Namespace) TailwindCSS(args ...any) (resource.Resource, error) {
	if len(args) > 2 {
		return nil, errors.New("must not provide more arguments than resource object and options")
	}

	r, m, err := resourcehelpers.ResolveArgs(args)
	if err != nil {
		return nil, err
	}

	return ns.tailwindcssClient.Process(r, m)
}

// Sass processes the given Resource with SASS.
func (ns *Namespace) Sass(args ...any) (resource.Resource, error) {
	if len(args) > 2 {
		return nil, errors.New("must not provide more arguments than resource object and options")
	}

	var (
		r          resources.ResourceTransformer
		m          map[string]any
		targetPath string
		err        error
		ok         bool
		transpiler = sass.TranspilerLibSass // Deprecated default. Will be dartsass in future versions. See below.
	)

	r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)

	if !ok {
		r, m, err = resourcehelpers.ResolveArgs(args)
		if err != nil {
			return nil, err
		}
	}

	if m != nil {
		if t, _, found := hmaps.LookupEqualFold(m, "transpiler"); found {
			switch t {
			case sass.TranspilerDart:
				transpiler = cast.ToString(t)
			case sass.TranspilerLibSass:
				hugo.Deprecate("css.Sass: libsass", "Use dartsass instead. See https://gohugo.io/functions/css/sass/#dart-sass", "v0.153.0")
				transpiler = cast.ToString(t)
			default:
				return nil, fmt.Errorf("unsupported transpiler %q; valid values are %q or %q", t, sass.TranspilerLibSass, sass.TranspilerDart)
			}
		}
	}

	if transpiler == sass.TranspilerLibSass {
		var options scss.Options
		if targetPath != "" {
			options.TargetPath = paths.ToSlashTrimLeading(targetPath)
		} else if m != nil {
			options, err = scss.DecodeOptions(m)
			if err != nil {
				return nil, err
			}
		}

		return ns.scssClientLibSass.ToCSS(r, options)
	}

	if m == nil {
		m = make(map[string]any)
	}
	if targetPath != "" {
		m["targetPath"] = targetPath
	}

	client, err := ns.getscssClientDartSass()
	if err != nil {
		return nil, err
	}

	return client.ToCSS(r, m)
}

// Build processes the given CSS Resource with ESBuild.
// Note that this method is identical to the one in the js Namespace.
func (ns *Namespace) Build(args ...any) (resource.Resource, error) {
	var (
		r          resources.ResourceTransformer
		m          map[string]any
		targetPath string
		err        error
		ok         bool
	)

	r, targetPath, ok = resourcehelpers.ResolveIfFirstArgIsString(args)

	if !ok {
		r, m, err = resourcehelpers.ResolveArgs(args)
		if err != nil {
			return nil, err
		}
	}

	if targetPath != "" {
		m = map[string]any{"targetPath": targetPath}
	}

	return ns.jsTransformClient.Process(r, m)
}

func init() {
	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
		scssClient, err := scss.New(d.BaseFs.Assets, d.ResourceSpec)
		if err != nil {
			panic(err)
		}
		ctx := &Namespace{
			d:                 d,
			scssClientLibSass: scssClient,
			postcssClient:     cssjs.NewPostCSSClient(d.ResourceSpec),
			tailwindcssClient: cssjs.NewTailwindCSSClient(d.ResourceSpec),
			babelClient:       babel.New(d.ResourceSpec),
			jsTransformClient: jstransform.New(d.BaseFs.Assets, d.ResourceSpec, true),
		}

		ns := &internal.TemplateFuncsNamespace{
			Name:    name,
			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
		}

		ns.AddMethodMapping(ctx.PostCSS,
			[]string{"postCSS"},
			[][2]string{},
		)

		ns.AddMethodMapping(ctx.Quoted,
			nil,
			[][2]string{},
		)

		ns.AddMethodMapping(ctx.Sass,
			[]string{"toCSS"},
			[][2]string{},
		)

		ns.AddMethodMapping(ctx.Build,
			nil,
			[][2]string{},
		)

		ns.AddMethodMapping(ctx.TailwindCSS,
			nil,
			[][2]string{},
		)

		ns.AddMethodMapping(ctx.Unquoted,
			nil,
			[][2]string{},
		)

		return ns
	}

	internal.AddTemplateFuncsNamespace(f)
}

func (ns *Namespace) getscssClientDartSass() (*dartsass.Client, error) {
	var err error
	ns.scssClientDartSassInit.Do(func() {
		ns.scssClientDartSass, err = dartsass.New(ns.d.BaseFs.Assets, ns.d.ResourceSpec)
		if err != nil {
			return
		}
		ns.d.BuildClosers.Add(ns.scssClientDartSass)
	})

	return ns.scssClientDartSass, err
}
