/*
Copyright 2020 The Kubernetes 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 config

import (
	"fmt"
	ioutil "io/ioutil"
	"sync"

	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"sigs.k8s.io/controller-runtime/pkg/config/v1alpha1"
)

// ControllerManagerConfiguration defines the functions necessary to parse a config file
// and to configure the Options struct for the ctrl.Manager.
type ControllerManagerConfiguration interface {
	runtime.Object

	// Complete returns the versioned configuration
	Complete() (v1alpha1.ControllerManagerConfigurationSpec, error)
}

// DeferredFileLoader is used to configure the decoder for loading controller
// runtime component config types.
type DeferredFileLoader struct {
	ControllerManagerConfiguration
	path   string
	scheme *runtime.Scheme
	once   sync.Once
	err    error
}

// File will set up the deferred file loader for the configuration
// this will also configure the defaults for the loader if nothing is
//
// Defaults:
//   Path: "./config.yaml"
//   Kind: GenericControllerManagerConfiguration
func File() *DeferredFileLoader {
	scheme := runtime.NewScheme()
	utilruntime.Must(v1alpha1.AddToScheme(scheme))
	return &DeferredFileLoader{
		path:                           "./config.yaml",
		ControllerManagerConfiguration: &v1alpha1.ControllerManagerConfiguration{},
		scheme:                         scheme,
	}
}

// Complete will use sync.Once to set the scheme.
func (d *DeferredFileLoader) Complete() (v1alpha1.ControllerManagerConfigurationSpec, error) {
	d.once.Do(d.loadFile)
	if d.err != nil {
		return v1alpha1.ControllerManagerConfigurationSpec{}, d.err
	}
	return d.ControllerManagerConfiguration.Complete()
}

// AtPath will set the path to load the file for the decoder.
func (d *DeferredFileLoader) AtPath(path string) *DeferredFileLoader {
	d.path = path
	return d
}

// OfKind will set the type to be used for decoding the file into.
func (d *DeferredFileLoader) OfKind(obj ControllerManagerConfiguration) *DeferredFileLoader {
	d.ControllerManagerConfiguration = obj
	return d
}

// InjectScheme will configure the scheme to be used for decoding the file.
func (d *DeferredFileLoader) InjectScheme(scheme *runtime.Scheme) error {
	d.scheme = scheme
	return nil
}

// loadFile is used from the mutex.Once to load the file.
func (d *DeferredFileLoader) loadFile() {
	if d.scheme == nil {
		d.err = fmt.Errorf("scheme not supplied to controller configuration loader")
		return
	}

	content, err := ioutil.ReadFile(d.path)
	if err != nil {
		d.err = fmt.Errorf("could not read file at %s", d.path)
		return
	}

	codecs := serializer.NewCodecFactory(d.scheme)

	// Regardless of if the bytes are of any external version,
	// it will be read successfully and converted into the internal version
	if err = runtime.DecodeInto(codecs.UniversalDecoder(), content, d.ControllerManagerConfiguration); err != nil {
		d.err = fmt.Errorf("could not decode file into runtime.Object")
	}
}