// Copyright (c) 2019 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package main

import (
	"bytes"
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

const (
	// how many lines to check for an existing copyright
	// this logic is not great and we should probably do something else
	// but this was copied from the python script
	copyrightLineLimit = 5
	headerPrefix       = "// Copyright"
)

var (
	flagDryRun  = flag.Bool("dry", false, "Do not edit files and just print out what files would be edited")
	flagOwner   = flag.String("owner", "Uber Technologies, Inc.", "Copyright owner")
	flagLicense = flag.String(
		"license",
		"MIT",
		fmt.Sprintf(
			"Type of license to use [%s]",
			strings.Join(validLicenses(), ", "),
		),
	)

	lineSkipPrefixes = []string{
		"// Code generated by",
		"// @generated",
	}
)

func main() {
	log.SetFlags(0)
	log.SetOutput(os.Stdout)
	log.SetPrefix("")
	if err := do(); err != nil {
		log.Fatal(err)
	}
}

func do() error {
	flag.Parse()

	if len(flag.Args()) < 1 {
		return fmt.Errorf("usage: %s GO_FILES", os.Args[0])
	}

	return updateFiles(
		flag.Args(),
		time.Now().UTC().Year(),
		*flagLicense,
		*flagOwner,
		*flagDryRun,
	)
}

func fullLicense(ts string, year int, owner string) string {
	var buf bytes.Buffer
	t, err := template.New("").Parse(ts)
	if err != nil {
		log.Panic("failed to parse license template", err)
	}

	data := struct {
		Year  int
		Owner string
	}{year, owner}
	if err := t.Execute(&buf, data); err != nil {
		log.Panic("failed to execture license template", err)
	}

	return strings.TrimSpace(buf.String())
}

// validLicenses grabs all the license templates from the folder
func validLicenses() []string {
	res := make([]string, 0, len(licenseTemplates))

	for k := range licenseTemplates {
		res = append(res, k)
	}

	sort.Strings(res)
	return res
}

func updateFiles(
	filePaths []string,
	year int,
	license string,
	owner string,
	dryRun bool,
) error {
	if err := checkFilePaths(filePaths); err != nil {
		return err
	}
	for _, filePath := range filePaths {
		if err := updateFile(filePath, year, license, owner, dryRun); err != nil {
			return err
		}
	}
	return nil
}

func checkFilePaths(filePaths []string) error {
	for _, filePath := range filePaths {
		if filepath.Ext(filePath) != ".go" {
			return fmt.Errorf("%s is not a go file", filePath)
		}
	}
	return nil
}

func updateFile(
	filePath string,
	year int,
	license string,
	owner string,
	dryRun bool,
) error {
	data, err := ioutil.ReadFile(filePath)
	if err != nil {
		return err
	}
	newData := updateData(data, year, license, owner)
	if !bytes.Equal(data, newData) {
		if dryRun {
			log.Print(filePath)
			return nil
		}
		// we could do something more complicated so that we do not
		// need to pass 0644 as the file mode, but in this case it should
		// never actually be used to create a file since we know the file
		// already exists, and it's easier to use the ReadFile/WriteFile
		// logic as it is right now, and since this is just a generation
		// program, this should be acceptable
		return ioutil.WriteFile(filePath, newData, 0644)
	}
	return nil
}

func updateData(
	data []byte,
	year int,
	license string,
	owner string,
) []byte {
	licenseText := fullLicense(string(licenseTemplates[license]), year, owner)

	return []byte(
		strings.Join(
			updateLines(strings.Split(string(data), "\n"), licenseText),
			"\n",
		),
	)
}

// a value in the returned slice may contain newlines itself
func updateLines(lines []string, license string) []string {
	for i, line := range lines {
		if i >= copyrightLineLimit {
			break
		}
		if strings.HasPrefix(line, headerPrefix) {
			// assume that the new license text always starts with the copyright
			// string. Pretty safe to assume, right? RIGHT?
			lines[i] = strings.Split(license, "\n")[0]
			return lines
		}
	}
	return addToLines(lines, license)
}

// a value in the returned slice may contain newlines itself
func addToLines(lines []string, license string) []string {
	i := 0
	for len(lines) > i && lineContainsSkipPrefix(lines[i]) {
		i++
		// skip comments under the generated line too
		for strings.HasPrefix(lines[i], "//") {
			i++
		}
	}
	if i == 0 {
		return append([]string{license, ""}, lines...)
	}
	return append(lines[0:i], append([]string{"", license}, lines[i:]...)...)
}

func lineContainsSkipPrefix(line string) bool {
	for _, skipPrefix := range lineSkipPrefixes {
		if strings.HasPrefix(line, skipPrefix) {
			return true
		}
	}
	return false
}
