/*
Copyright 2019 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 genall

import (
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"

	"sigs.k8s.io/controller-tools/pkg/loader"
)

// nopCloser is a WriteCloser whose Close
// is a no-op.
type nopCloser struct {
	io.Writer
}

func (n nopCloser) Close() error {
	return nil
}

// DirectoryPerGenerator produces output rules mapping output to a different subdirectory
// of the given base directory for each generator (with each subdirectory specified as
// the key in the input map).
func DirectoryPerGenerator(base string, generators map[string]Generator) OutputRules {
	rules := OutputRules{
		Default:     OutputArtifacts{Config: OutputToDirectory(base)},
		ByGenerator: make(map[Generator]OutputRule, len(generators)),
	}

	for name, gen := range generators {
		rules.ByGenerator[gen] = OutputArtifacts{
			Config: OutputToDirectory(filepath.Join(base, name)),
		}
	}

	return rules
}

// OutputRules defines how to output artificats on a per-generator basis.
type OutputRules struct {
	// Default is the output rule used when no specific per-generator overrides match.
	Default OutputRule
	// ByGenerator contains specific per-generator overrides.
	ByGenerator map[Generator]OutputRule
}

// ForGenerator returns the output rule that should be used
// by the given Generator.
func (o OutputRules) ForGenerator(gen Generator) OutputRule {
	if forGen, specific := o.ByGenerator[gen]; specific {
		return forGen
	}
	return o.Default
}

// OutputRule defines how to output artifacts from a generator.
type OutputRule interface {
	// Open opens the given artifact path for writing.  If a package is passed,
	// the artifact is considered to be used as part of the package (e.g.
	// generated code), while a nil package indicates that the artifact is
	// config (or something else not involved in Go compilation).
	Open(pkg *loader.Package, path string) (io.WriteCloser, error)
}

// OutputToNothing skips outputting anything.
var OutputToNothing = outputToNothing{}

type outputToNothing struct{}

func (o outputToNothing) Open(_ *loader.Package, _ string) (io.WriteCloser, error) {
	return nopCloser{ioutil.Discard}, nil
}

// OutputToDirectory outputs each artifact to the given directory, regardless
// of if it's package-associated or not.
type OutputToDirectory string

func (o OutputToDirectory) Open(_ *loader.Package, itemPath string) (io.WriteCloser, error) {
	// ensure the directory exists
	if err := os.MkdirAll(string(o), os.ModePerm); err != nil {
		return nil, err
	}
	path := filepath.Join(string(o), itemPath)
	return os.Create(path)
}

// OutputToStdout outputs everything to standard-out, with no separation.
// Generally useful for single-artifact outputs.
var OutputToStdout = outputToStdout{}

type outputToStdout struct{}

func (o outputToStdout) Open(_ *loader.Package, itemPath string) (io.WriteCloser, error) {
	return nopCloser{os.Stdout}, nil
}

// OutputArtifacts outputs artifacts to different locations, depending on
// whether they're package-associated or not.  Non-package associated artifacts
// are output to the Config directory, while package-associated ones are output
// to their package's source files' directory, unless an alternate path is
// specified in Code.
type OutputArtifacts struct {
	Config OutputToDirectory
	Code   OutputToDirectory `marker:",optional"`
}

func (o OutputArtifacts) Open(pkg *loader.Package, itemPath string) (io.WriteCloser, error) {
	if pkg == nil {
		return o.Config.Open(pkg, itemPath)
	}

	if o.Code != "" {
		return o.Code.Open(pkg, itemPath)
	}

	if len(pkg.CompiledGoFiles) == 0 {
		return nil, fmt.Errorf("cannot ouput to a package with no path on disk")
	}
	outDir := filepath.Dir(pkg.CompiledGoFiles[0])
	outPath := filepath.Join(outDir, itemPath)
	return os.Create(outPath)
}
