// Copyright © 2016 Asteris, LLC
//
// 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 render

import (
	"crypto/rand"
	"fmt"

	"github.com/asteris-llc/converge/executor"
	"github.com/asteris-llc/converge/graph"
	"github.com/asteris-llc/converge/graph/node"
	"github.com/asteris-llc/converge/graph/node/conditional"
	"github.com/asteris-llc/converge/resource"
	"github.com/asteris-llc/converge/resource/module"
	multierror "github.com/hashicorp/go-multierror"
	"github.com/pkg/errors"
	"golang.org/x/net/context"
)

// Values for rendering
type Values map[string]resource.Value

// Render a graph with the provided values
func Render(ctx context.Context, g *graph.Graph, top Values) (*graph.Graph, error) {
	renderingPlant, err := NewFactory(ctx, g)
	if err != nil {
		return nil, err
	}
	return g.RootFirstTransform(ctx, func(meta *node.Node, out *graph.Graph) error {
		pipeline := Pipeline(out, meta.ID, renderingPlant, top)
		value, err := pipeline.Exec(ctx, meta.Value())
		if err != nil {
			return err
		}
		out.Add(meta.WithValue(value))
		renderingPlant.Graph = out
		return nil
	})
}

type pipelineGen struct {
	Graph          *graph.Graph
	RenderingPlant *Factory
	ID             string
	Top            Values
}

// Pipeline generates a pipelined form of rendering
func Pipeline(g *graph.Graph, id string, factory *Factory, top Values) executor.Pipeline {
	p := pipelineGen{Graph: g, RenderingPlant: factory, Top: top, ID: id}
	return executor.NewPipeline().
		AndThen(p.maybeTransformRoot).
		AndThen(p.prepareNode).
		AndThen(p.wrapTask)
}

// Check to see if the current id is "root", if so generate a new module
// preparer for it and add in all of the command-line parameters; otherwise if
// the node is a valide resource.Resource return it.  If it's not root and not a
// resource.Resource return an error.
func (p pipelineGen) maybeTransformRoot(ctx context.Context, idi interface{}) (interface{}, error) {
	if graph.IsRoot(p.ID) {
		return module.NewPreparer(p.Top), nil
	}
	if res, ok := idi.(resource.Resource); ok {
		return res, nil
	}
	return nil, typeError("render.maybeTransformRoot", p.ID, "resource.Resource", idi)
}

// Run prepare on the node and return the resource.Resource to be wrapped
func (p pipelineGen) prepareNode(ctx context.Context, idi interface{}) (interface{}, error) {
	var metadataErr error

	res, ok := idi.(resource.Resource)
	if !ok {
		return nil, typeError("render.prepareNode", p.ID, "resource.Resource", idi)
	}

	renderer, err := p.RenderingPlant.GetRenderer(p.ID)
	if err != nil {
		return nil, err
	}

	if p.shouldRenderMetadata() {
		_, metadataErr = p.renderMetadata(renderer)
	}

	prepared, err := res.Prepare(ctx, renderer)

	merged := mergeMaybeUnresolvables(err, metadataErr)

	if merged != nil {
		if errIsUnresolvable(merged) {
			return createThunk(func(factory *Factory) (resource.Task, error) {
				dynamicRenderer, rendErr := factory.GetRenderer(p.ID)
				if rendErr != nil {
					return nil, rendErr
				}
				if p.shouldRenderMetadata() {
					_, rendErr := p.renderMetadata(dynamicRenderer)
					if rendErr != nil {
						return nil, rendErr
					}
				}
				return res.Prepare(ctx, dynamicRenderer)
			}), nil
		}
		return nil, merged
	}
	return prepared, nil
}

func mergeMaybeUnresolvables(err1, err2 error) error {
	if err1 == nil {
		return err2
	}
	if err2 == nil {
		return err1
	}
	if errIsUnresolvable(err1) && errIsUnresolvable(err2) {
		return err1
	}
	if !errIsUnresolvable(err1) {
		return err1
	}
	if !errIsUnresolvable(err2) {
		return err2
	}
	return multierror.Append(err1, err2)
}

func errIsUnresolvable(err error) bool {
	_, ok := errors.Cause(err).(ErrUnresolvable)
	return ok
}

func errIsBadTemplate(err error) bool {
	_, ok := errors.Cause(err).(ErrBadTemplate)
	return ok
}

func (p pipelineGen) shouldRenderMetadata() bool {
	meta, ok := p.Graph.Get(p.ID)
	if !ok {
		return false
	}
	return conditional.IsConditional(meta)
}

func (p pipelineGen) renderMetadata(r *Renderer) (string, error) {
	meta, ok := p.Graph.Get(p.ID)
	if !ok {
		return "", errors.New(p.ID + " does not exist in graph, cannot render metadata")
	}
	if !conditional.IsConditional(meta) {
		return "", nil
	}
	return conditional.RenderPredicate(meta, r.Render)
}

// Takes a resource.Task and wraps it in resource.TaskWrapper
func (p pipelineGen) wrapTask(ctx context.Context, taski interface{}) (interface{}, error) {
	if task, ok := taski.(*PrepareThunk); ok {
		return task, nil
	}
	if task, ok := taski.(resource.Task); ok {
		return resource.WrapTask(task), nil
	}
	return nil, typeError("render.wrapTask", p.ID, "resource.Task", taski)
}

func typeError(where, what, expected string, actual interface{}) error {
	return fmt.Errorf("type error in %s: expected %s to be type %s but received type %T", where, what, expected, actual)
}

// PrepareThunk returns a possibly lazily evaluated preparer
type PrepareThunk struct {
	// prevent hashing thunks into a single value
	Data  []byte
	Thunk func(*Factory) (resource.Task, error) `hash:"ignore"`
}

// Check allows thunk to implement resource.Task
func (p *PrepareThunk) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	return nil, errors.New("Unresolved thunk: cannot be evaluated")
}

// Apply allows thunk to implement resource.Task
func (p *PrepareThunk) Apply(context.Context) (resource.TaskStatus, error) {
	return nil, errors.New("Unresolved thunk: cannot be evaluated")
}

func createThunk(f func(*Factory) (resource.Task, error)) *PrepareThunk {
	junk := make([]byte, 32)
	rand.Read(junk)
	return &PrepareThunk{
		Thunk: f,
		Data:  junk,
	}
}
