// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package schema

import (
	"errors"
	"fmt"
	"log"
	"strconv"

	"github.com/opentofu/opentofu/internal/legacy/tofu"
	"github.com/zclconf/go-cty/cty"
)

var ReservedDataSourceFields = []string{
	"connection",
	"count",
	"depends_on",
	"lifecycle",
	"provider",
	"provisioner",
}

var ReservedResourceFields = []string{
	"connection",
	"count",
	"depends_on",
	"id",
	"lifecycle",
	"provider",
	"provisioner",
}

// Resource represents a thing in OpenTofu that has a set of configurable
// attributes and a lifecycle (create, read, update, delete).
//
// The Resource schema is an abstraction that allows provider writers to
// worry only about CRUD operations while off-loading validation, diff
// generation, etc. to this higher level library.
//
// In spite of the name, this struct is not used only for tofu resources,
// but also for data sources. In the case of data sources, the Create,
// Update and Delete functions must not be provided.
type Resource struct {
	// Schema is the schema for the configuration of this resource.
	//
	// The keys of this map are the configuration keys, and the values
	// describe the schema of the configuration value.
	//
	// The schema is used to represent both configurable data as well
	// as data that might be computed in the process of creating this
	// resource.
	Schema map[string]*Schema

	// SchemaVersion is the version number for this resource's Schema
	// definition. The current SchemaVersion stored in the state for each
	// resource. Provider authors can increment this version number
	// when Schema semantics change. If the State's SchemaVersion is less than
	// the current SchemaVersion, the InstanceState is yielded to the
	// MigrateState callback, where the provider can make whatever changes it
	// needs to update the state to be compatible to the latest version of the
	// Schema.
	//
	// When unset, SchemaVersion defaults to 0, so provider authors can start
	// their Versioning at any integer >= 1
	SchemaVersion int

	// MigrateState is deprecated and any new changes to a resource's schema
	// should be handled by StateUpgraders. Existing MigrateState implementations
	// should remain for compatibility with existing state. MigrateState will
	// still be called if the stored SchemaVersion is less than the
	// first version of the StateUpgraders.
	//
	// MigrateState is responsible for updating an InstanceState with an old
	// version to the format expected by the current version of the Schema.
	//
	// It is called during Refresh if the State's stored SchemaVersion is less
	// than the current SchemaVersion of the Resource.
	//
	// The function is yielded the state's stored SchemaVersion and a pointer to
	// the InstanceState that needs updating, as well as the configured
	// provider's configured meta interface{}, in case the migration process
	// needs to make any remote API calls.
	MigrateState StateMigrateFunc

	// StateUpgraders contains the functions responsible for upgrading an
	// existing state with an old schema version to a newer schema. It is
	// called specifically by OpenTofu when the stored schema version is less
	// than the current SchemaVersion of the Resource.
	//
	// StateUpgraders map specific schema versions to a StateUpgrader
	// function. The registered versions are expected to be ordered,
	// consecutive values. The initial value may be greater than 0 to account
	// for legacy schemas that weren't recorded and can be handled by
	// MigrateState.
	StateUpgraders []StateUpgrader

	// The functions below are the CRUD operations for this resource.
	//
	// The only optional operation is Update. If Update is not implemented,
	// then updates will not be supported for this resource.
	//
	// The ResourceData parameter in the functions below are used to
	// query configuration and changes for the resource as well as to set
	// the ID, computed data, etc.
	//
	// The interface{} parameter is the result of the ConfigureFunc in
	// the provider for this resource. If the provider does not define
	// a ConfigureFunc, this will be nil. This parameter should be used
	// to store API clients, configuration structures, etc.
	//
	// If any errors occur during each of the operation, an error should be
	// returned. If a resource was partially updated, be careful to enable
	// partial state mode for ResourceData and use it accordingly.
	//
	// Exists is a function that is called to check if a resource still
	// exists. If this returns false, then this will affect the diff
	// accordingly. If this function isn't set, it will not be called. You
	// can also signal existence in the Read method by calling d.SetId("")
	// if the Resource is no longer present and should be removed from state.
	// The *ResourceData passed to Exists should _not_ be modified.
	Create CreateFunc
	Read   ReadFunc
	Update UpdateFunc
	Delete DeleteFunc
	Exists ExistsFunc

	// CustomizeDiff is a custom function for working with the diff that
	// OpenTofu has created for this resource - it can be used to customize the
	// diff that has been created, diff values not controlled by configuration,
	// or even veto the diff altogether and abort the plan. It is passed a
	// *ResourceDiff, a structure similar to ResourceData but lacking most write
	// functions like Set, while introducing new functions that work with the
	// diff such as SetNew, SetNewComputed, and ForceNew.
	//
	// The phases OpenTofu runs this in, and the state available via functions
	// like Get and GetChange, are as follows:
	//
	//  * New resource: One run with no state
	//  * Existing resource: One run with state
	//   * Existing resource, forced new: One run with state (before ForceNew),
	//     then one run without state (as if new resource)
	//  * Tainted resource: No runs (custom diff logic is skipped)
	//  * Destroy: No runs (standard diff logic is skipped on destroy diffs)
	//
	// This function needs to be resilient to support all scenarios.
	//
	// If this function needs to access external API resources, remember to flag
	// the RequiresRefresh attribute mentioned below to ensure that
	// -refresh=false is blocked when running plan or apply, as this means that
	// this resource requires refresh-like behaviour to work effectively.
	//
	// For the most part, only computed fields can be customized by this
	// function.
	//
	// This function is only allowed on regular resources (not data sources).
	CustomizeDiff CustomizeDiffFunc

	// Importer is the ResourceImporter implementation for this resource.
	// If this is nil, then this resource does not support importing. If
	// this is non-nil, then it supports importing and ResourceImporter
	// must be validated. The validity of ResourceImporter is verified
	// by InternalValidate on Resource.
	Importer *ResourceImporter

	// If non-empty, this string is emitted as a warning during Validate.
	DeprecationMessage string

	// Timeouts allow users to specify specific time durations in which an
	// operation should time out, to allow them to extend an action to suit their
	// usage. For example, a user may specify a large Creation timeout for their
	// AWS RDS Instance due to it's size, or restoring from a snapshot.
	// Resource implementors must enable Timeout support by adding the allowed
	// actions (Create, Read, Update, Delete, Default) to the Resource struct, and
	// accessing them in the matching methods.
	Timeouts *ResourceTimeout
}

// ShimInstanceStateFromValue converts a cty.Value to a
// tofu.InstanceState.
func (r *Resource) ShimInstanceStateFromValue(state cty.Value) (*tofu.InstanceState, error) {
	// Get the raw shimmed value. While this is correct, the set hashes don't
	// match those from the Schema.
	s := tofu.NewInstanceStateShimmedFromValue(state, r.SchemaVersion)

	// We now rebuild the state through the ResourceData, so that the set indexes
	// match what helper/schema expects.
	data, err := schemaMap(r.Schema).Data(s, nil)
	if err != nil {
		return nil, err
	}

	s = data.State()
	if s == nil {
		s = &tofu.InstanceState{}
	}
	return s, nil
}

// See Resource documentation.
type CreateFunc func(*ResourceData, interface{}) error

// See Resource documentation.
type ReadFunc func(*ResourceData, interface{}) error

// See Resource documentation.
type UpdateFunc func(*ResourceData, interface{}) error

// See Resource documentation.
type DeleteFunc func(*ResourceData, interface{}) error

// See Resource documentation.
type ExistsFunc func(*ResourceData, interface{}) (bool, error)

// See Resource documentation.
type StateMigrateFunc func(
	int, *tofu.InstanceState, interface{}) (*tofu.InstanceState, error)

type StateUpgrader struct {
	// Version is the version schema that this Upgrader will handle, converting
	// it to Version+1.
	Version int

	// Type describes the schema that this function can upgrade. Type is
	// required to decode the schema if the state was stored in a legacy
	// flatmap format.
	Type cty.Type

	// Upgrade takes the JSON encoded state and the provider meta value, and
	// upgrades the state one single schema version. The provided state is
	// decoded into the default json types using a map[string]interface{}. It
	// is up to the StateUpgradeFunc to ensure that the returned value can be
	// encoded using the new schema.
	Upgrade StateUpgradeFunc
}

// See StateUpgrader
type StateUpgradeFunc func(rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error)

// See Resource documentation.
type CustomizeDiffFunc func(*ResourceDiff, interface{}) error

// Apply creates, updates, and/or deletes a resource.
func (r *Resource) Apply(
	s *tofu.InstanceState,
	d *tofu.InstanceDiff,
	meta interface{}) (*tofu.InstanceState, error) {
	data, err := schemaMap(r.Schema).Data(s, d)
	if err != nil {
		return s, err
	}
	if s != nil && data != nil {
		data.providerMeta = s.ProviderMeta
	}

	// Instance Diff should have the timeout info, need to copy it over to the
	// ResourceData meta
	rt := ResourceTimeout{}
	if _, ok := d.Meta[TimeoutKey]; ok {
		if err := rt.DiffDecode(d); err != nil {
			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
		}
	} else if s != nil {
		if _, ok := s.Meta[TimeoutKey]; ok {
			if err := rt.StateDecode(s); err != nil {
				log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
			}
		}
	} else {
		log.Printf("[DEBUG] No meta timeoutkey found in Apply()")
	}
	data.timeouts = &rt

	if s == nil {
		// The OpenTofu API dictates that this should never happen, but
		// it doesn't hurt to be safe in this case.
		s = new(tofu.InstanceState)
	}

	if d.Destroy || d.RequiresNew() {
		if s.ID != "" {
			// Destroy the resource since it is created
			if err := r.Delete(data, meta); err != nil {
				return r.recordCurrentSchemaVersion(data.State()), err
			}

			// Make sure the ID is gone.
			data.SetId("")
		}

		// If we're only destroying, and not creating, then return
		// now since we're done!
		if !d.RequiresNew() {
			return nil, nil
		}

		// Reset the data to be stateless since we just destroyed
		data, err = schemaMap(r.Schema).Data(nil, d)
		// data was reset, need to re-apply the parsed timeouts
		data.timeouts = &rt
		if err != nil {
			return nil, err
		}
	}

	err = nil
	if data.Id() == "" {
		// We're creating, it is a new resource.
		data.MarkNewResource()
		err = r.Create(data, meta)
	} else {
		if r.Update == nil {
			return s, fmt.Errorf("doesn't support update")
		}

		err = r.Update(data, meta)
	}

	return r.recordCurrentSchemaVersion(data.State()), err
}

// Diff returns a diff of this resource.
func (r *Resource) Diff(
	s *tofu.InstanceState,
	c *tofu.ResourceConfig,
	meta interface{}) (*tofu.InstanceDiff, error) {

	t := &ResourceTimeout{}
	err := t.ConfigDecode(r, c)

	if err != nil {
		return nil, fmt.Errorf("[ERR] Error decoding timeout: %w", err)
	}

	instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, true)
	if err != nil {
		return instanceDiff, err
	}

	if instanceDiff != nil {
		if err := t.DiffEncode(instanceDiff); err != nil {
			log.Printf("[ERR] Error encoding timeout to instance diff: %s", err)
		}
	} else {
		log.Printf("[DEBUG] Instance Diff is nil in Diff()")
	}

	return instanceDiff, err
}

func (r *Resource) simpleDiff(
	s *tofu.InstanceState,
	c *tofu.ResourceConfig,
	meta interface{}) (*tofu.InstanceDiff, error) {

	instanceDiff, err := schemaMap(r.Schema).Diff(s, c, r.CustomizeDiff, meta, false)
	if err != nil {
		return instanceDiff, err
	}

	if instanceDiff == nil {
		instanceDiff = tofu.NewInstanceDiff()
	}

	// Make sure the old value is set in each of the instance diffs.
	// This was done by the RequiresNew logic in the full legacy Diff.
	for k, attr := range instanceDiff.Attributes {
		if attr == nil {
			continue
		}
		if s != nil {
			attr.Old = s.Attributes[k]
		}
	}

	return instanceDiff, nil
}

// Validate validates the resource configuration against the schema.
func (r *Resource) Validate(c *tofu.ResourceConfig) ([]string, []error) {
	warns, errs := schemaMap(r.Schema).Validate(c)

	if r.DeprecationMessage != "" {
		warns = append(warns, r.DeprecationMessage)
	}

	return warns, errs
}

// ReadDataApply loads the data for a data source, given a diff that
// describes the configuration arguments and desired computed attributes.
func (r *Resource) ReadDataApply(
	d *tofu.InstanceDiff,
	meta interface{},
) (*tofu.InstanceState, error) {
	// Data sources are always built completely from scratch
	// on each read, so the source state is always nil.
	data, err := schemaMap(r.Schema).Data(nil, d)
	if err != nil {
		return nil, err
	}

	err = r.Read(data, meta)
	state := data.State()
	if state != nil && state.ID == "" {
		// Data sources can set an ID if they want, but they aren't
		// required to; we'll provide a placeholder if they don't,
		// to preserve the invariant that all resources have non-empty
		// ids.
		state.ID = "-"
	}

	return r.recordCurrentSchemaVersion(state), err
}

// RefreshWithoutUpgrade reads the instance state, but does not call
// MigrateState or the StateUpgraders, since those are now invoked in a
// separate API call.
// RefreshWithoutUpgrade is part of the new plugin shims.
func (r *Resource) RefreshWithoutUpgrade(
	s *tofu.InstanceState,
	meta interface{}) (*tofu.InstanceState, error) {
	// If the ID is already somehow blank, it doesn't exist
	if s.ID == "" {
		return nil, nil
	}

	rt := ResourceTimeout{}
	if _, ok := s.Meta[TimeoutKey]; ok {
		if err := rt.StateDecode(s); err != nil {
			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
		}
	}

	if r.Exists != nil {
		// Make a copy of data so that if it is modified it doesn't
		// affect our Read later.
		data, err := schemaMap(r.Schema).Data(s, nil)
		data.timeouts = &rt

		if err != nil {
			return s, err
		}

		if s != nil {
			data.providerMeta = s.ProviderMeta
		}

		exists, err := r.Exists(data, meta)
		if err != nil {
			return s, err
		}
		if !exists {
			return nil, nil
		}
	}

	data, err := schemaMap(r.Schema).Data(s, nil)
	data.timeouts = &rt
	if err != nil {
		return s, err
	}

	if s != nil {
		data.providerMeta = s.ProviderMeta
	}

	err = r.Read(data, meta)
	state := data.State()
	if state != nil && state.ID == "" {
		state = nil
	}

	return r.recordCurrentSchemaVersion(state), err
}

// Refresh refreshes the state of the resource.
func (r *Resource) Refresh(
	s *tofu.InstanceState,
	meta interface{}) (*tofu.InstanceState, error) {
	// If the ID is already somehow blank, it doesn't exist
	if s.ID == "" {
		return nil, nil
	}

	rt := ResourceTimeout{}
	if _, ok := s.Meta[TimeoutKey]; ok {
		if err := rt.StateDecode(s); err != nil {
			log.Printf("[ERR] Error decoding ResourceTimeout: %s", err)
		}
	}

	if r.Exists != nil {
		// Make a copy of data so that if it is modified it doesn't
		// affect our Read later.
		data, err := schemaMap(r.Schema).Data(s, nil)
		data.timeouts = &rt

		if err != nil {
			return s, err
		}

		exists, err := r.Exists(data, meta)
		if err != nil {
			return s, err
		}
		if !exists {
			return nil, nil
		}
	}

	// there may be new StateUpgraders that need to be run
	s, err := r.upgradeState(s, meta)
	if err != nil {
		return s, err
	}

	data, err := schemaMap(r.Schema).Data(s, nil)
	data.timeouts = &rt
	if err != nil {
		return s, err
	}

	err = r.Read(data, meta)
	state := data.State()
	if state != nil && state.ID == "" {
		state = nil
	}

	return r.recordCurrentSchemaVersion(state), err
}

func (r *Resource) upgradeState(s *tofu.InstanceState, meta interface{}) (*tofu.InstanceState, error) {
	var err error

	needsMigration, stateSchemaVersion := r.checkSchemaVersion(s)
	migrate := needsMigration && r.MigrateState != nil

	if migrate {
		s, err = r.MigrateState(stateSchemaVersion, s, meta)
		if err != nil {
			return s, err
		}
	}

	if len(r.StateUpgraders) == 0 {
		return s, nil
	}

	// If we ran MigrateState, then the stateSchemaVersion value is no longer
	// correct. We can expect the first upgrade function to be the correct
	// schema type version.
	if migrate {
		stateSchemaVersion = r.StateUpgraders[0].Version
	}

	schemaType := r.CoreConfigSchema().ImpliedType()
	// find the expected type to convert the state
	for _, upgrader := range r.StateUpgraders {
		if stateSchemaVersion == upgrader.Version {
			schemaType = upgrader.Type
		}
	}

	// StateUpgraders only operate on the new JSON format state, so the state
	// need to be converted.
	stateVal, err := StateValueFromInstanceState(s, schemaType)
	if err != nil {
		return nil, err
	}

	jsonState, err := StateValueToJSONMap(stateVal, schemaType)
	if err != nil {
		return nil, err
	}

	for _, upgrader := range r.StateUpgraders {
		if stateSchemaVersion != upgrader.Version {
			continue
		}

		jsonState, err = upgrader.Upgrade(jsonState, meta)
		if err != nil {
			return nil, err
		}
		stateSchemaVersion++
	}

	// now we need to re-flatmap the new state
	stateVal, err = JSONMapToStateValue(jsonState, r.CoreConfigSchema())
	if err != nil {
		return nil, err
	}

	return r.ShimInstanceStateFromValue(stateVal)
}

// InternalValidate should be called to validate the structure
// of the resource.
//
// This should be called in a unit test for any resource to verify
// before release that a resource is properly configured for use with
// this library.
//
// Provider.InternalValidate() will automatically call this for all of
// the resources it manages, so you don't need to call this manually if it
// is part of a Provider.
func (r *Resource) InternalValidate(topSchemaMap schemaMap, writable bool) error {
	if r == nil {
		return errors.New("resource is nil")
	}

	if !writable {
		if r.Create != nil || r.Update != nil || r.Delete != nil {
			return fmt.Errorf("must not implement Create, Update or Delete")
		}

		// CustomizeDiff cannot be defined for read-only resources
		if r.CustomizeDiff != nil {
			return fmt.Errorf("cannot implement CustomizeDiff")
		}
	}

	tsm := topSchemaMap

	if r.isTopLevel() && writable {
		// All non-Computed attributes must be ForceNew if Update is not defined
		if r.Update == nil {
			nonForceNewAttrs := make([]string, 0)
			for k, v := range r.Schema {
				if !v.ForceNew && !v.Computed {
					nonForceNewAttrs = append(nonForceNewAttrs, k)
				}
			}
			if len(nonForceNewAttrs) > 0 {
				return fmt.Errorf(
					"No Update defined, must set ForceNew on: %#v", nonForceNewAttrs)
			}
		} else {
			nonUpdateableAttrs := make([]string, 0)
			for k, v := range r.Schema {
				if v.ForceNew || v.Computed && !v.Optional {
					nonUpdateableAttrs = append(nonUpdateableAttrs, k)
				}
			}
			updateableAttrs := len(r.Schema) - len(nonUpdateableAttrs)
			if updateableAttrs == 0 {
				return fmt.Errorf(
					"All fields are ForceNew or Computed w/out Optional, Update is superfluous")
			}
		}

		tsm = schemaMap(r.Schema)

		// Destroy, and Read are required
		if r.Read == nil {
			return fmt.Errorf("Read must be implemented")
		}
		if r.Delete == nil {
			return fmt.Errorf("Delete must be implemented")
		}

		// If we have an importer, we need to verify the importer.
		if r.Importer != nil {
			if err := r.Importer.InternalValidate(); err != nil {
				return err
			}
		}

		for k, f := range tsm {
			if isReservedResourceFieldName(k, f) {
				return fmt.Errorf("%s is a reserved field name", k)
			}
		}
	}

	lastVersion := -1
	for _, u := range r.StateUpgraders {
		if lastVersion >= 0 && u.Version-lastVersion > 1 {
			return fmt.Errorf("missing schema version between %d and %d", lastVersion, u.Version)
		}

		if u.Version >= r.SchemaVersion {
			return fmt.Errorf("StateUpgrader version %d is >= current version %d", u.Version, r.SchemaVersion)
		}

		if !u.Type.IsObjectType() {
			return fmt.Errorf("StateUpgrader %d type is not cty.Object", u.Version)
		}

		if u.Upgrade == nil {
			return fmt.Errorf("StateUpgrader %d missing StateUpgradeFunc", u.Version)
		}

		lastVersion = u.Version
	}

	if lastVersion >= 0 && lastVersion != r.SchemaVersion-1 {
		return fmt.Errorf("missing StateUpgrader between %d and %d", lastVersion, r.SchemaVersion)
	}

	// Data source
	if r.isTopLevel() && !writable {
		tsm = schemaMap(r.Schema)
		for k, _ := range tsm {
			if isReservedDataSourceFieldName(k) {
				return fmt.Errorf("%s is a reserved field name", k)
			}
		}
	}

	return schemaMap(r.Schema).InternalValidate(tsm)
}

func isReservedDataSourceFieldName(name string) bool {
	for _, reservedName := range ReservedDataSourceFields {
		if name == reservedName {
			return true
		}
	}
	return false
}

func isReservedResourceFieldName(name string, s *Schema) bool {
	// Allow phasing out "id"
	// See https://github.com/terraform-providers/terraform-provider-aws/pull/1626#issuecomment-328881415
	if name == "id" && (s.Deprecated != "" || s.Removed != "") {
		return false
	}

	for _, reservedName := range ReservedResourceFields {
		if name == reservedName {
			return true
		}
	}
	return false
}

// Data returns a ResourceData struct for this Resource. Each return value
// is a separate copy and can be safely modified differently.
//
// The data returned from this function has no actual affect on the Resource
// itself (including the state given to this function).
//
// This function is useful for unit tests and ResourceImporter functions.
func (r *Resource) Data(s *tofu.InstanceState) *ResourceData {
	result, err := schemaMap(r.Schema).Data(s, nil)
	if err != nil {
		// At the time of writing, this isn't possible (Data never returns
		// non-nil errors). We panic to find this in the future if we have to.
		// I don't see a reason for Data to ever return an error.
		panic(err)
	}

	// load the Resource timeouts
	result.timeouts = r.Timeouts
	if result.timeouts == nil {
		result.timeouts = &ResourceTimeout{}
	}

	// Set the schema version to latest by default
	result.meta = map[string]interface{}{
		"schema_version": strconv.Itoa(r.SchemaVersion),
	}

	return result
}

// TestResourceData Yields a ResourceData filled with this resource's schema for use in unit testing
//
// TODO: May be able to be removed with the above ResourceData function.
func (r *Resource) TestResourceData() *ResourceData {
	return &ResourceData{
		schema: r.Schema,
	}
}

// SchemasForFlatmapPath tries its best to find a sequence of schemas that
// the given dot-delimited attribute path traverses through in the schema
// of the receiving Resource.
func (r *Resource) SchemasForFlatmapPath(path string) []*Schema {
	return SchemasForFlatmapPath(path, r.Schema)
}

// Returns true if the resource is "top level" i.e. not a sub-resource.
func (r *Resource) isTopLevel() bool {
	// TODO: This is a heuristic; replace with a definitive attribute?
	return (r.Create != nil || r.Read != nil)
}

// Determines if a given InstanceState needs to be migrated by checking the
// stored version number with the current SchemaVersion
func (r *Resource) checkSchemaVersion(is *tofu.InstanceState) (bool, int) {
	// Get the raw interface{} value for the schema version. If it doesn't
	// exist or is nil then set it to zero.
	raw := is.Meta["schema_version"]
	if raw == nil {
		raw = "0"
	}

	// Try to convert it to a string. If it isn't a string then we pretend
	// that it isn't set at all. It should never not be a string unless it
	// was manually tampered with.
	rawString, ok := raw.(string)
	if !ok {
		rawString = "0"
	}

	stateSchemaVersion, _ := strconv.Atoi(rawString)

	// Don't run MigrateState if the version is handled by a StateUpgrader,
	// since StateMigrateFuncs are not required to handle unknown versions
	maxVersion := r.SchemaVersion
	if len(r.StateUpgraders) > 0 {
		maxVersion = r.StateUpgraders[0].Version
	}

	return stateSchemaVersion < maxVersion, stateSchemaVersion
}

func (r *Resource) recordCurrentSchemaVersion(
	state *tofu.InstanceState) *tofu.InstanceState {
	if state != nil && r.SchemaVersion > 0 {
		if state.Meta == nil {
			state.Meta = make(map[string]interface{})
		}
		state.Meta["schema_version"] = strconv.Itoa(r.SchemaVersion)
	}
	return state
}

// Noop is a convenience implementation of resource function which takes
// no action and returns no error.
func Noop(*ResourceData, interface{}) error {
	return nil
}

// RemoveFromState is a convenience implementation of a resource function
// which sets the resource ID to empty string (to remove it from state)
// and returns no error.
func RemoveFromState(d *ResourceData, _ interface{}) error {
	d.SetId("")
	return nil
}
