// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package main

// gen_interface creates the assemble/validate cpp files given the
// interface.json5 file.
// See README for more details.

import (
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/flynn/json5"
)

var (
	outDir  = flag.String("out_dir", "../../src/gpu/gl", "Where to output the GrGlAssembleInterface_* and GrGlInterface.cpp files")
	inTable = flag.String("in_table", "./interface.json5", "The JSON5 table to read in")
	dryRun  = flag.Bool("dryrun", false, "Print the outputs, don't write to file")
)

const (
	CORE_FEATURE        = "<core>"
	SPACER              = "    "
	GLES_FILE_NAME      = "GrGLAssembleGLESInterfaceAutogen.cpp"
	GL_FILE_NAME        = "GrGLAssembleGLInterfaceAutogen.cpp"
	WEBGL_FILE_NAME     = "GrGLAssembleWebGLInterfaceAutogen.cpp"
	INTERFACE_FILE_NAME = "GrGLInterfaceAutogen.cpp"
)

// FeatureSet represents one set of requirements for each of the GL "standards" that
// Skia supports.  This is currently OpenGL, OpenGL ES and WebGL.
// OpenGL is typically abbreviated as just "GL".
// https://www.khronos.org/registry/OpenGL/index_gl.php
// https://www.khronos.org/opengles/
// https://www.khronos.org/registry/webgl/specs/1.0/
type FeatureSet struct {
	GLReqs    []Requirement `json:"GL"`
	GLESReqs  []Requirement `json:"GLES"`
	WebGLReqs []Requirement `json:"WebGL"`

	Functions         []string           `json:"functions"`
	HardCodeFunctions []HardCodeFunction `json:"hardcode_functions"`
	OptionalFunctions []string           `json:"optional"` // not checked in validate

	// only assembled/validated when testing
	TestOnlyFunctions []string `json:"test_functions"`

	Required bool `json:"required"`
}

// Requirement lists how we know if a function exists. Extension is the
// GL extension (or the string CORE_FEATURE if it's part of the core functionality).
// MinVersion optionally indicates the minimum version of a standard
// that has the function.
// SuffixOverride allows the extension suffix to be manually specified instead
// of automatically derived from the extension name.
// (for example, if an NV extension specifies some EXT extensions)
type Requirement struct {
	Extension      string     `json:"ext"` // required
	MinVersion     *GLVersion `json:"min_version"`
	SuffixOverride *string    `json:"suffix"`
}

// HardCodeFunction indicates to not use the C++ macro and just directly
// adds a given function ptr to the struct.
type HardCodeFunction struct {
	PtrName  string `json:"ptr_name"`
	CastName string `json:"cast_name"`
	GetName  string `json:"get_name"`
}

var CORE_REQUIREMENT = Requirement{Extension: CORE_FEATURE, MinVersion: nil}

type GLVersion [2]int

// RequirementGetter functions allows us to "iterate" over the requirements
// of the different standards which are stored as keys in FeatureSet and
// normally not easily iterable.
type RequirementGetter func(FeatureSet) []Requirement

func glRequirements(fs FeatureSet) []Requirement {
	return fs.GLReqs
}

func glesRequirements(fs FeatureSet) []Requirement {
	return fs.GLESReqs
}

func webglRequirements(fs FeatureSet) []Requirement {
	return fs.WebGLReqs
}

// generateAssembleInterface creates one GrGLAssembleInterface_[type]_gen.cpp
// for each of the standards
func generateAssembleInterface(features []FeatureSet) {
	gl := fillAssembleTemplate(ASSEMBLE_INTERFACE_GL, features, glRequirements)
	writeToFile(*outDir, GL_FILE_NAME, gl)
	gles := fillAssembleTemplate(ASSEMBLE_INTERFACE_GL_ES, features, glesRequirements)
	writeToFile(*outDir, GLES_FILE_NAME, gles)
	webgl := fillAssembleTemplate(ASSEMBLE_INTERFACE_WEBGL, features, webglRequirements)
	writeToFile(*outDir, WEBGL_FILE_NAME, webgl)
}

// fillAssembleTemplate returns a generated file given a template (for a single standard)
// to fill out and a slice of features with which to fill it.  getReqs is used to select
// the requirements for the standard we are working on.
func fillAssembleTemplate(template string, features []FeatureSet, getReqs RequirementGetter) string {
	content := ""
	for _, feature := range features {
		// For each feature set, we are going to create a series of
		// if statements, which check for the requirements (e.g. extensions, version)
		// and inside those if branches, write the code to load the
		// correct function pointer to the interface. GET_PROC and
		// GET_PROC_SUFFIX are macros defined in C++ part of the template
		// to accomplish this (for a core feature and extensions, respectively).
		reqs := getReqs(feature)
		if len(reqs) == 0 {
			continue
		}
		// blocks holds all the if blocks generated - it will be joined with else
		// after and appended to content
		blocks := []string{}
		for i, req := range reqs {
			block := ""
			ifExpr := requirementIfExpression(req, true)

			if ifExpr != "" {
				if strings.HasPrefix(ifExpr, "(") {
					ifExpr = "if " + ifExpr + " {"
				} else {
					ifExpr = "if (" + ifExpr + ") {"
				}
				// Indent the first if statement
				if i == 0 {
					block = addLine(block, ifExpr)
				} else {
					block += ifExpr + "\n"
				}
			}
			// sort for determinism
			sort.Strings(feature.Functions)
			for _, function := range feature.Functions {
				block = assembleFunction(block, ifExpr, function, req)
			}
			sort.Strings(feature.TestOnlyFunctions)
			if len(feature.TestOnlyFunctions) > 0 {
				block += "#if GR_TEST_UTILS\n"
				for _, function := range feature.TestOnlyFunctions {
					block = assembleFunction(block, ifExpr, function, req)
				}
				block += "#endif\n"
			}

			// a hard code function does not use the C++ macro
			for _, hcf := range feature.HardCodeFunctions {
				if ifExpr != "" {
					// extra tab for being in an if statement
					block += SPACER
				}
				line := fmt.Sprintf(`functions->%s =(%s*)get(ctx, "%s");`, hcf.PtrName, hcf.CastName, hcf.GetName)
				block = addLine(block, line)
			}
			if ifExpr != "" {
				block += SPACER + "}"
			}
			blocks = append(blocks, block)
		}
		content += strings.Join(blocks, " else ")

		if feature.Required && reqs[0] != CORE_REQUIREMENT {
			content += ` else {
        SkASSERT(false); // Required feature
        return nullptr;
    }`
		}

		if !strings.HasSuffix(content, "\n") {
			content += "\n"
		}
		// Add an extra space between blocks for easier readability
		content += "\n"

	}

	return strings.Replace(template, "[[content]]", content, 1)
}

// assembleFunction is a little helper that will add a function to the interface
// using an appropriate macro (e.g. if the function is added)
// with an extension.
func assembleFunction(block, ifExpr, function string, req Requirement) string {
	if ifExpr != "" {
		// extra tab for being in an if statement
		block += SPACER
	}
	suffix := deriveSuffix(req.Extension)
	// Some ARB extensions don't have ARB suffixes because they were written
	// for backwards compatibility simultaneous to adding them as required
	// in a new GL version.
	if suffix == "ARB" {
		suffix = ""
	}
	if req.SuffixOverride != nil {
		suffix = *req.SuffixOverride
	}
	if req.Extension == CORE_FEATURE || suffix == "" {
		block = addLine(block, fmt.Sprintf("GET_PROC(%s);", function))
	} else if req.Extension != "" {
		block = addLine(block, fmt.Sprintf("GET_PROC_SUFFIX(%s, %s);", function, suffix))
	}
	return block
}

// requirementIfExpression returns a string that is an if expression
// Notably, there is no if expression if the function is a "core" function
// on all supported versions.
// The expressions are wrapped in parentheses so they can be safely
// joined together with && or ||.
func requirementIfExpression(req Requirement, isLocal bool) string {
	mv := req.MinVersion
	if req == CORE_REQUIREMENT {
		return ""
	}
	if req.Extension == CORE_FEATURE && mv != nil {
		return fmt.Sprintf("(glVer >= GR_GL_VER(%d,%d))", mv[0], mv[1])
	}
	extVar := "fExtensions"
	if isLocal {
		extVar = "extensions"
	}
	// We know it has an extension
	if req.Extension != "" {
		if mv == nil {
			return fmt.Sprintf("%s.has(%q)", extVar, req.Extension)
		} else {
			return fmt.Sprintf("(glVer >= GR_GL_VER(%d,%d) && %s.has(%q))", mv[0], mv[1], extVar, req.Extension)
		}
	}
	abort("ERROR: requirement must have ext")
	return "ERROR"
}

// driveSuffix returns the suffix of the function associated with the given
// extension.
func deriveSuffix(ext string) string {
	// Some extensions begin with GL_ and then have the actual
	// extension like KHR, EXT etc.
	ext = strings.TrimPrefix(ext, "GL_")
	return strings.Split(ext, "_")[0]
}

// addLine is a little helper function which handles the newline and tab
func addLine(str, line string) string {
	return str + SPACER + line + "\n"
}

func writeToFile(parent, file, content string) {
	p := filepath.Join(parent, file)
	if *dryRun {
		fmt.Printf("Writing to %s\n", p)
		fmt.Println(content)
	} else {
		if err := ioutil.WriteFile(p, []byte(content), 0644); err != nil {
			abort("Error while writing to file %s: %s", p, err)
		}
	}
}

// validationEntry is a helper struct that contains anything
// necessary to make validation code for a given standard.
type validationEntry struct {
	StandardCheck string
	GetReqs       RequirementGetter
}

func generateValidateInterface(features []FeatureSet) {
	standards := []validationEntry{
		{
			StandardCheck: "GR_IS_GR_GL(fStandard)",
			GetReqs:       glRequirements,
		}, {
			StandardCheck: "GR_IS_GR_GL_ES(fStandard)",
			GetReqs:       glesRequirements,
		}, {
			StandardCheck: "GR_IS_GR_WEBGL(fStandard)",
			GetReqs:       webglRequirements,
		},
	}
	content := ""
	// For each feature, we are going to generate a series of
	// boolean expressions which check that the functions we thought
	// were gathered during the assemble phase actually were applied to
	// the interface (functionCheck). This check will be guarded
	// another set of if statements (one per standard) based
	// on the same requirements (standardChecks) that were used when
	// assembling the interface.
	for _, feature := range features {
		if allReqsAreCore(feature) {
			content += functionCheck(feature, 1)
		} else {
			content += SPACER
			standardChecks := []string{}
			for _, std := range standards {
				reqs := std.GetReqs(feature)
				if reqs == nil || len(reqs) == 0 {
					continue
				}
				expr := []string{}
				for _, r := range reqs {
					e := requirementIfExpression(r, false)
					if e != "" {
						expr = append(expr, e)
					}
				}
				check := ""
				if len(expr) == 0 {
					check = fmt.Sprintf("%s", std.StandardCheck)
				} else {
					lineBreak := "\n" + SPACER + "      "
					check = fmt.Sprintf("(%s && (%s%s))", std.StandardCheck, lineBreak, strings.Join(expr, " ||"+lineBreak))
				}
				standardChecks = append(standardChecks, check)
			}
			content += fmt.Sprintf("if (%s) {\n", strings.Join(standardChecks, " ||\n"+SPACER+"   "))
			content += functionCheck(feature, 2)

			content += SPACER + "}\n"
		}
		// add additional line between each block
		content += "\n"
	}
	content = strings.Replace(VALIDATE_INTERFACE, "[[content]]", content, 1)
	writeToFile(*outDir, INTERFACE_FILE_NAME, content)
}

// functionCheck returns an if statement that checks that all functions
// in the passed in slice are on the interface (that is, they are truthy
// on the fFunctions struct)
func functionCheck(feature FeatureSet, indentLevel int) string {
	// sort for determinism
	sort.Strings(feature.Functions)
	indent := strings.Repeat(SPACER, indentLevel)

	checks := []string{}
	for _, function := range feature.Functions {
		if in(function, feature.OptionalFunctions) {
			continue
		}
		checks = append(checks, "!fFunctions.f"+function)
	}
	testOnly := []string{}
	for _, function := range feature.TestOnlyFunctions {
		if in(function, feature.OptionalFunctions) {
			continue
		}
		testOnly = append(testOnly, "!fFunctions.f"+function)
	}
	for _, hcf := range feature.HardCodeFunctions {
		checks = append(checks, "!fFunctions."+hcf.PtrName)
	}
	preCheck := ""
	if len(testOnly) != 0 {
		preCheck = fmt.Sprintf(`#if GR_TEST_UTILS
%sif (%s) {
%s%sRETURN_FALSE_INTERFACE;
%s}
#endif
`, indent, strings.Join(testOnly, " ||\n"+indent+"    "), indent, SPACER, indent)
	}

	if len(checks) == 0 {
		return preCheck + strings.Repeat(SPACER, indentLevel) + "// all functions were marked optional or test_only\n"
	}

	return preCheck + fmt.Sprintf(`%sif (%s) {
%s%sRETURN_FALSE_INTERFACE;
%s}
`, indent, strings.Join(checks, " ||\n"+indent+"    "), indent, SPACER, indent)
}

// allReqsAreCore returns true iff the FeatureSet is part of "core" for
// all standards
func allReqsAreCore(feature FeatureSet) bool {
	if feature.GLReqs == nil || feature.GLESReqs == nil {
		return false
	}
	return feature.GLReqs[0] == CORE_REQUIREMENT && feature.GLESReqs[0] == CORE_REQUIREMENT && feature.WebGLReqs[0] == CORE_REQUIREMENT
}

func validateFeatures(features []FeatureSet) {
	seen := map[string]bool{}
	for _, feature := range features {
		for _, fn := range feature.Functions {
			if seen[fn] {
				abort("ERROR: Duplicate function %s", fn)
			}
			seen[fn] = true
		}
		for _, fn := range feature.TestOnlyFunctions {
			if seen[fn] {
				abort("ERROR: Duplicate function %s\n", fn)
			}
			seen[fn] = true
		}
	}
}

// in returns true if |s| is *in* |a| slice.
func in(s string, a []string) bool {
	for _, x := range a {
		if x == s {
			return true
		}
	}
	return false
}

func abort(fmtStr string, inputs ...interface{}) {
	fmt.Printf(fmtStr+"\n", inputs...)
	os.Exit(1)
}

func main() {
	flag.Parse()
	b, err := ioutil.ReadFile(*inTable)
	if err != nil {
		abort("Could not read file %s", err)
	}

	dir, err := os.Open(*outDir)
	if err != nil {
		abort("Could not write to output dir %s", err)
	}
	defer dir.Close()
	if fi, err := dir.Stat(); err != nil {
		abort("Error getting info about %s: %s", *outDir, err)
	} else if !fi.IsDir() {
		abort("%s must be a directory", *outDir)
	}

	features := []FeatureSet{}

	err = json5.Unmarshal(b, &features)
	if err != nil {
		abort("Invalid JSON: %s", err)
	}

	validateFeatures(features)

	generateAssembleInterface(features)
	generateValidateInterface(features)
}
