// Copyright 2019 CUE Authors
//
// 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 gocodec converts Go to and from CUE and validates Go values based on
// CUE constraints.
//
// CUE constraints can be used to validate Go types as well as fill out
// missing struct fields that are implied from the constraints and the values
// already defined by the struct value.
package gocodec

import (
	"sync"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
	"cuelang.org/go/internal/value"
)

// Config has no options yet, but is defined for future extensibility.
type Config struct {
}

// A Codec decodes and encodes CUE from and to Go values and validates and
// completes Go values based on CUE templates.
type Codec struct {
	runtime *cue.Context
	mutex   sync.RWMutex
}

// New creates a new Codec for the given instance.
//
// It is safe to use the methods of Codec concurrently as long as the given
// Runtime is not used elsewhere while using Codec. However, only the concurrent
// use of Decode, Validate, and Complete is efficient.
//
// Note: calling this with a *cue.Runtime value is deprecated.
func New[Ctx *cue.Runtime | *cue.Context](ctx Ctx, c *Config) *Codec {
	return &Codec{runtime: value.ConvertToContext(ctx)}
}

// ExtractType extracts a CUE value from a Go type.
//
// The type represented by x is converted as the underlying type. Specific
// values, such as map or slice elements or field values of structs are ignored.
// If x is of type [reflect.Type], the type represented by x is extracted.
//
// Fields of structs can be annoted using additional constrains using the 'cue'
// field tag. The value of the tag is a CUE expression, which may contain
// references to the JSON name of other fields in a struct.
//
//	type Sum struct {
//	    A int `cue:"c-b" json:"a,omitempty"`
//	    B int `cue:"c-a" json:"b,omitempty"`
//	    C int `cue:"a+b" json:"c,omitempty"`
//	}
func (c *Codec) ExtractType(x interface{}) (cue.Value, error) {
	// ExtractType cannot introduce new fields on repeated calls. We could
	// consider optimizing the lock usage based on this property.
	c.mutex.Lock()
	defer c.mutex.Unlock()

	return fromGoType(c.runtime, x)
}

// TODO: allow extracting constraints and type info separately?

// Decode converts x to a CUE value.
//
// If x is of type [reflect.Value] it will convert the value represented by x.
func (c *Codec) Decode(x interface{}) (cue.Value, error) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	// Depending on the type, can introduce new labels on repeated calls.
	return fromGoValue(c.runtime, x, false)
}

// Encode converts v to a Go value.
func (c *Codec) Encode(v cue.Value, x interface{}) error {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return v.Decode(x)
}

var defaultCodec = New(cuecontext.New(), nil)

// Validate calls Validate on a default Codec for the type of x.
func Validate(x interface{}) error {
	c := defaultCodec
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	r := defaultCodec.runtime
	v, err := fromGoType(r, x)
	if err != nil {
		return err
	}
	w, err := fromGoValue(r, x, false)
	if err != nil {
		return err
	}
	v = v.Unify(w)
	if err := v.Validate(); err != nil {
		return err
	}
	return nil
}

// Validate checks whether x satisfies the constraints defined by v.
//
// The given value must be created using the same Runtime with which c was
// initialized.
func (c *Codec) Validate(v cue.Value, x interface{}) error {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	r := checkAndForkContext(c.runtime, v)
	w, err := fromGoValue(r, x, false)
	if err != nil {
		return err
	}
	return w.Unify(v).Err()
}

// Complete sets previously undefined values in x that can be uniquely
// determined form the constraints defined by v if validation passes, or returns
// an error, without modifying anything, otherwise.
//
// Only undefined values are modified. A value is considered undefined if it is
// pointer type and is nil or if it is a field with a zero value that has a json
// tag with the omitempty flag.
//
// The given value must be created using the same Runtime with which c was
// initialized.
//
// Complete does a JSON round trip. This means that data not preserved in such a
// round trip, such as the location name of a [time.Time], is lost after a
// successful update.
func (c *Codec) Complete(v cue.Value, x interface{}) error {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	r := checkAndForkContext(c.runtime, v)
	w, err := fromGoValue(r, x, true)
	if err != nil {
		return err
	}

	w = w.Unify(v)
	if err := w.Validate(cue.Concrete(true)); err != nil {
		return err
	}
	return w.Decode(x)
}

func fromGoValue(r *cue.Context, x interface{}, allowDefault bool) (cue.Value, error) {
	v := value.FromGoValue(r, x, allowDefault)
	if err := v.Err(); err != nil {
		return v, err
	}
	return v, nil
}

func fromGoType(r *cue.Context, x interface{}) (cue.Value, error) {
	v := value.FromGoType(r, x)
	if err := v.Err(); err != nil {
		return v, err
	}
	return v, nil
}

func checkAndForkContext(r *cue.Context, v cue.Value) *cue.Context {
	rr := v.Context()
	if r != rr {
		panic("value not from same runtime")
	}
	return rr
}
