// Copyright (C) 2019 Google Inc.
//
// 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.

// gen-grammar generates the spirv.json grammar file from the official SPIR-V
// grammar JSON file.
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"text/template"

	"github.com/pkg/errors"

	"../grammar"
)

type grammarDefinition struct {
	name string
	url  string
}

var (
	spirvGrammar = grammarDefinition{
		name: "SPIR-V",
		url:  "https://raw.githubusercontent.com/KhronosGroup/SPIRV-Headers/master/include/spirv/unified1/spirv.core.grammar.json",
	}

	extensionGrammars = []grammarDefinition{
		{
			name: "GLSL.std.450",
			url:  "https://raw.githubusercontent.com/KhronosGroup/SPIRV-Headers/master/include/spirv/unified1/extinst.glsl.std.450.grammar.json",
		}, {
			name: "OpenCL.std",
			url:  "https://raw.githubusercontent.com/KhronosGroup/SPIRV-Headers/master/include/spirv/unified1/extinst.opencl.std.100.grammar.json",
		}, {
			name: "OpenCL.DebugInfo.100",
			url:  "https://raw.githubusercontent.com/KhronosGroup/SPIRV-Tools/master/source/extinst.opencl.debuginfo.100.grammar.json",
		},
	}

	templatePath = flag.String("template", "", "Path to input template file (required)")
	outputPath   = flag.String("out", "", "Path to output generated file (required)")
	cachePath    = flag.String("cache", "", "Cache directory for downloaded files (optional)")

	thisDir = func() string {
		_, file, _, _ := runtime.Caller(1)
		return filepath.Dir(file)
	}()
)

func main() {
	flag.Parse()
	if *templatePath == "" || *outputPath == "" {
		flag.Usage()
		os.Exit(1)
	}
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	tf, err := ioutil.ReadFile(*templatePath)
	if err != nil {
		return errors.Wrap(err, "Could not open template file")
	}

	type extension struct {
		grammar.Root
		Name string
	}

	args := struct {
		SPIRV      grammar.Root
		Extensions []extension
		All        grammar.Root // Combination of SPIRV + Extensions
	}{}

	if args.SPIRV, err = parseGrammar(spirvGrammar); err != nil {
		return errors.Wrap(err, "Failed to parse SPIR-V grammar file")
	}
	args.All.Instructions = append(args.All.Instructions, args.SPIRV.Instructions...)
	args.All.OperandKinds = append(args.All.OperandKinds, args.SPIRV.OperandKinds...)

	for _, ext := range extensionGrammars {
		root, err := parseGrammar(ext)
		if err != nil {
			return errors.Wrap(err, "Failed to parse extension grammar file")
		}
		args.Extensions = append(args.Extensions, extension{Root: root, Name: ext.name})
		args.All.Instructions = append(args.All.Instructions, root.Instructions...)
		args.All.OperandKinds = append(args.All.OperandKinds, root.OperandKinds...)
	}

	t, err := template.New("tmpl").
		Funcs(template.FuncMap{
			"GenerateArguments": func() string {
				relPath := func(path string) string {
					rel, err := filepath.Rel(thisDir, path)
					if err != nil {
						return path
					}
					return rel
				}
				escape := func(str string) string {
					return strings.ReplaceAll(str, `\`, `/`)
				}
				args := []string{
					"--template=" + escape(relPath(*templatePath)),
					"--out=" + escape(relPath(*outputPath)),
				}
				return "gen-grammar.go " + strings.Join(args, " ")
			},
			"OperandKindsMatch": func(k grammar.OperandKind) string {
				sb := strings.Builder{}
				for i, e := range k.Enumerants {
					if i > 0 {
						sb.WriteString("|")
					}
					sb.WriteString(e.Enumerant)
				}
				return sb.String()
			},
			"AllExtOpcodes": func() string {
				sb := strings.Builder{}
				for _, ext := range args.Extensions {
					for _, inst := range ext.Root.Instructions {
						if sb.Len() > 0 {
							sb.WriteString("|")
						}
						sb.WriteString(inst.Opname)
					}
				}
				return sb.String()
			},
			"Title":   strings.Title,
			"Replace": strings.ReplaceAll,
			"Global": func(s string) string {
				return strings.ReplaceAll(strings.Title(s), ".", "")
			},
		}).Parse(string(tf))
	if err != nil {
		return errors.Wrap(err, "Failed to parse template")
	}

	buf := bytes.Buffer{}
	if err := t.Execute(&buf, args); err != nil {
		return errors.Wrap(err, "Failed to execute template")
	}

	out := buf.String()
	out = strings.ReplaceAll(out, "•", "")

	if err := ioutil.WriteFile(*outputPath, []byte(out), 0777); err != nil {
		return errors.Wrap(err, "Failed to write output file")
	}

	return nil
}

// parseGrammar downloads (or loads from the cache) the grammar file and returns
// the parsed grammar.Root.
func parseGrammar(def grammarDefinition) (grammar.Root, error) {
	file, err := getOrDownload(def.name, def.url)
	if err != nil {
		return grammar.Root{}, errors.Wrap(err, "Failed to load grammar file")
	}

	g := grammar.Root{}
	if err := json.NewDecoder(bytes.NewReader(file)).Decode(&g); err != nil {
		return grammar.Root{}, errors.Wrap(err, "Failed to parse grammar file")
	}

	return g, nil
}

// getOrDownload loads the specific file from the cache, or downloads the file
// from the given url.
func getOrDownload(name, url string) ([]byte, error) {
	if *cachePath != "" {
		if err := os.MkdirAll(*cachePath, 0777); err == nil {
			path := filepath.Join(*cachePath, name)
			if isFile(path) {
				return ioutil.ReadFile(path)
			}
		}
	}
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if *cachePath != "" {
		ioutil.WriteFile(filepath.Join(*cachePath, name), data, 0777)
	}
	return data, nil
}

// isFile returns true if path is a file.
func isFile(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return !s.IsDir()
}

// isDir returns true if path is a directory.
func isDir(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return s.IsDir()
}
