// Package go-msi helps to generate msi package for a Go project.
package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/Masterminds/semver"
	"github.com/mh-cbon/go-msi/manifest"
	"github.com/mh-cbon/go-msi/rtf"
	"github.com/mh-cbon/go-msi/tpls"
	"github.com/mh-cbon/go-msi/util"
	"github.com/mh-cbon/go-msi/wix"
	"github.com/mh-cbon/stringexec"
	"github.com/urfave/cli"
)

// VERSION holds the program version.
var VERSION = "0.0.0"

// TPLPATH points to the template directory on the target system.
// Should be used only for non windows systems to indicate template locations.
var TPLPATH = "" // non-windows build, use ldflags to tell about that.

func main() {

	if TPLPATH == "" { // built for windows
		b, err := util.GetBinPath()
		if err != nil {
			panic(err)
		}
		TPLPATH = b
	}
	tmpBuildDir, err := ioutil.TempDir("", "go-msi")
	if err != nil {
		panic(err)
	}

	app := cli.NewApp()
	app.Name = "go-msi"
	app.Version = VERSION
	app.Usage = "Easy msi pakage for Go"
	app.UsageText = "go-msi <cmd> <options>"
	app.Commands = []cli.Command{
		{
			Name:   "check-json",
			Usage:  "Check the JSON wix manifest",
			Action: checkJSON,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "path, p",
					Value: "wix.json",
					Usage: "Path to the wix manifest file",
				},
			},
		},
		{
			Name:   "check-env",
			Usage:  "Provide a report about your environment setup",
			Action: checkEnv,
		},
		{
			Name:   "set-guid",
			Usage:  "Sets appropriate guids in your wix manifest",
			Action: setGUID,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "path, p",
					Value: "wix.json",
					Usage: "Path to the wix manifest file",
				},
				cli.BoolFlag{
					Name:  "force, f",
					Usage: "Force update the guids",
				},
			},
		},
		{
			Name:   "generate-templates",
			Usage:  "Generate wix templates",
			Action: generateTemplates,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "path, p",
					Value: "wix.json",
					Usage: "Path to the wix manifest file",
				},
				cli.StringFlag{
					Name:  "src, s",
					Value: filepath.Join(TPLPATH, "templates"),
					Usage: "Directory path to the wix templates files",
				},
				cli.StringFlag{
					Name:  "out, o",
					Value: tmpBuildDir,
					Usage: "Directory path to the generated wix templates files",
				},
				cli.StringFlag{
					Name:  "version",
					Value: "",
					Usage: "The version of your program",
				},
				cli.StringFlag{
					Name:  "license, l",
					Value: "",
					Usage: "Path to the license file",
				},
			},
		},
		{
			Name:   "to-windows",
			Usage:  "Write Windows1252 encoded file",
			Action: toWindows1252,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "src, s",
					Value: "",
					Usage: "Path to an UTF-8 encoded file",
				},
				cli.StringFlag{
					Name:  "out, o",
					Value: "",
					Usage: "Path to the ANSI generated file",
				},
			},
		},
		{
			Name:   "to-rtf",
			Usage:  "Write RTF formatted file",
			Action: toRtf,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "src, s",
					Value: "",
					Usage: "Path to a text file",
				},
				cli.StringFlag{
					Name:  "out, o",
					Value: "",
					Usage: "Path to the RTF generated file",
				},
				cli.BoolFlag{
					Name:  "reencode, e",
					Usage: "Also re encode UTF-8 to Windows1252 charset",
				},
			},
		},
		{
			Name:   "gen-wix-cmd",
			Usage:  "Generate a batch file of Wix commands to run",
			Action: generateWixCommands,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "path, p",
					Value: "wix.json",
					Usage: "Path to the wix manifest file",
				},
				cli.StringFlag{
					Name:  "src, s",
					Value: filepath.Join(TPLPATH, "templates"),
					Usage: "Directory path to the wix templates files",
				},
				cli.StringFlag{
					Name:  "out, o",
					Value: tmpBuildDir,
					Usage: "Directory path to the generated wix cmd file",
				},
				cli.StringFlag{
					Name:  "arch, a",
					Value: "",
					Usage: "A target architecture, amd64 or 386 (ia64 is not handled)",
				},
				cli.StringFlag{
					Name:  "msi, m",
					Value: "",
					Usage: "Path to write resulting msi file to",
				},
			},
		},
		{
			Name:   "run-wix-cmd",
			Usage:  "Run the batch file of Wix commands",
			Action: runWixCommands,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "out, o",
					Value: tmpBuildDir,
					Usage: "Directory path to the generated wix cmd file",
				},
			},
		},
		{
			Name:   "make",
			Usage:  "All-in-one command to make MSI files",
			Action: quickMake,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "path, p",
					Value: "wix.json",
					Usage: "Path to the wix manifest file",
				},
				cli.StringFlag{
					Name:  "src, s",
					Value: filepath.Join(TPLPATH, "templates"),
					Usage: "Directory path to the wix templates files",
				},
				cli.StringFlag{
					Name:  "out, o",
					Value: tmpBuildDir,
					Usage: "Directory path to the generated wix cmd file",
				},
				cli.StringFlag{
					Name:  "arch, a",
					Value: "",
					Usage: "A target architecture, amd64 or 386 (ia64 is not handled)",
				},
				cli.StringFlag{
					Name:  "msi, m",
					Value: "",
					Usage: "Path to write resulting msi file to",
				},
				cli.StringFlag{
					Name:  "version",
					Value: "",
					Usage: "The version of your program",
				},
				cli.StringFlag{
					Name:  "license, l",
					Value: "",
					Usage: "Path to the license file",
				},
				cli.BoolFlag{
					Name:  "keep, k",
					Usage: "Keep output directory containing build files (useful for debug)",
				},
			},
		},
		{
			Name:   "choco",
			Usage:  "Generate a chocolatey package of your msi files",
			Action: chocoMake,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "path, p",
					Value: "wix.json",
					Usage: "Path to the wix manifest file",
				},
				cli.StringFlag{
					Name:  "src, s",
					Value: filepath.Join(TPLPATH, "templates", "choco"),
					Usage: "Directory path to the wix templates files",
				},
				cli.StringFlag{
					Name:  "version",
					Value: "",
					Usage: "The version of your program",
				},
				cli.StringFlag{
					Name:  "out, o",
					Value: tmpBuildDir,
					Usage: "Directory path to the generated chocolatey build file",
				},
				cli.StringFlag{
					Name:  "input, i",
					Value: "",
					Usage: "Path to the msi file to package into the chocolatey package",
				},
				cli.StringFlag{
					Name:  "changelog-cmd, c",
					Value: "",
					Usage: "A command to generate the content of the changlog in the package",
				},
				cli.BoolFlag{
					Name:  "keep, k",
					Usage: "Keep output directory containing build files (useful for debug)",
				},
			},
		},
	}

	app.Run(os.Args)
}

var verReg = regexp.MustCompile(`\s[0-9]+[.][0-9]+[.][0-9]+`)

func checkEnv(c *cli.Context) error {

	for _, b := range []string{"heat", "light", "candle"} {
		if out, err := util.Exec(b, "-h"); out == "" {
			fmt.Printf("!!	%v not found: %q\n", b, err)
		} else {
			match := verReg.FindAllString(out, -1)
			if len(match) < 1 {
				fmt.Printf("??	%v probably not found\n", b)
			} else {
				version := strings.TrimSpace(match[0])
				ver, err := semver.NewVersion(version)
				if err != nil {
					fmt.Printf("??	%v found but its version is not parsable %v\n", b, version)
				} else {
					min := "3.10.0"
					if !ver.GreaterThan(semver.MustParse(min)) {
						fmt.Printf("!!	%v found %v but %v is required\n", b, version, min)
					} else {
						fmt.Printf("ok	%v found %v\n", b, version)
					}
				}
			}
		}
	}
	if out, err := util.Exec("choco", "-v"); out == "" {
		fmt.Printf("!!	%v not found: %q\n", "chocolatey", err)
	} else {
		match := verReg.FindAllString(" "+out, -1)
		if len(match) < 1 {
			fmt.Printf("??	%v probably not found\n", "chocolatey")
		} else {
			version := strings.TrimSpace(match[0])
			ver, err := semver.NewVersion(version)
			if err != nil {
				fmt.Printf("??	%v found but its version is not parsable %v\n", "chocolatey", version)
			} else {
				min := "0.10.0"
				if !ver.GreaterThan(semver.MustParse(min)) {
					fmt.Printf("!!	%v found %v but >%v is required\n", "chocolatey", version, min)
				} else {
					fmt.Printf("ok	%v found %v\n", "chocolatey", version)
				}
			}
		}
	}
	return nil
}

func checkJSON(c *cli.Context) error {
	path := c.String("path")

	wixFile := manifest.WixManifest{}
	err := wixFile.Load(path)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	for _, hook := range wixFile.Hooks {
		if _, ok := manifest.HookPhases[hook.When]; !ok {
			return cli.NewExitError(`Invalid "when" value in hook: `+hook.When, 1)
		}
	}

	fmt.Println("The manifest is syntaxically correct !")

	if wixFile.NeedGUID() {
		fmt.Println("The manifest needs Guid")
		fmt.Println("To update your file automatically run:")
		fmt.Println("     go-msi set-guid")
		return cli.NewExitError("Incomplete manifest file detected", 1)
	}
	return nil
}

func setGUID(c *cli.Context) error {
	path := c.String("path")
	force := c.Bool("force")

	wixFile := manifest.WixManifest{}
	err := wixFile.Load(path)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	updated, err := wixFile.SetGuids(force)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if updated {
		fmt.Println("The manifest was updated")
	} else {
		fmt.Println("The manifest was not updated")
	}

	err = wixFile.Write(path)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	fmt.Println("The file is saved on disk")

	return nil
}

func generateTemplates(c *cli.Context) error {
	path := c.String("path")
	src := c.String("src")
	out := c.String("out")
	version := c.String("version")
	license := c.String("license")

	wixFile := manifest.WixManifest{}
	err := wixFile.Load(path)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if wixFile.NeedGUID() {
		fmt.Println("The manifest needs Guid")
		fmt.Println("To update your file automatically run:")
		fmt.Println("     go-msi set-guid")
		return cli.NewExitError("Cannot proceed, manifest file is incomplete", 1)
	}

	if c.IsSet("version") {
		wixFile.Version = version
	}

	if c.IsSet("license") {
		wixFile.License = license
	}

	err = wixFile.Normalize()
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	err = wixFile.RewriteFilePaths(out)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	templates, err := tpls.Find(src, "*.wxs")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if len(templates) == 0 {
		return cli.NewExitError("No templates *.wxs found in this directory", 1)
	}

	err = os.MkdirAll(out, 0744)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	for _, tpl := range templates {
		dst := filepath.Join(out, filepath.Base(tpl))
		err = tpls.GenerateTemplate(&wixFile, tpl, dst)
		if err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
	}

	fmt.Printf("Generated %d templates\n", len(templates))
	for _, tpl := range templates {
		dst := filepath.Join(out, filepath.Base(tpl))
		fmt.Printf("- %s\n", dst)
	}

	return nil
}

func toWindows1252(c *cli.Context) error {
	src := c.String("src")
	out := c.String("out")

	if src == "" {
		return cli.NewExitError("--src argument is required", 1)
	}
	if out == "" {
		return cli.NewExitError("--out argument is required", 1)
	}
	if _, err := os.Stat(src); os.IsNotExist(err) {
		return cli.NewExitError(err.Error(), 1)
	}
	os.MkdirAll(filepath.Dir(out), 0744)
	err := rtf.WriteAsWindows1252(src, out)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	return nil
}

func toRtf(c *cli.Context) error {
	src := c.String("src")
	out := c.String("out")
	reencode := c.Bool("reencode")

	if src == "" {
		return cli.NewExitError("--src argument is required", 1)
	}
	if out == "" {
		return cli.NewExitError("--out argument is required", 1)
	}
	if _, err := os.Stat(src); os.IsNotExist(err) {
		return cli.NewExitError(err.Error(), 1)
	}

	os.MkdirAll(filepath.Dir(out), 0744)

	err := rtf.WriteAsRtf(src, out, reencode)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	return nil
}

func generateWixCommands(c *cli.Context) error {
	path := c.String("path")
	src := c.String("src")
	out := c.String("out")
	msi := c.String("msi")
	arch := c.String("arch")

	if msi == "" {
		return cli.NewExitError("--msi parameter must be set", 1)
	}

	templates, err := tpls.Find(src, "*.wxs")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if len(templates) == 0 {
		return cli.NewExitError("No templates *.wxs found in this directory", 1)
	}

	builtTemplates := make([]string, len(templates))
	for i, tpl := range templates {
		builtTemplates[i] = filepath.Join(out, filepath.Base(tpl))
	}

	wixFile := manifest.WixManifest{}
	err = wixFile.Load(path)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if wixFile.NeedGUID() {
		fmt.Println("The manifest needs Guid")
		fmt.Println("To update your file automatically run:")
		fmt.Println("     go-msi set-guid")
		return cli.NewExitError("Cannot proceed, manifest file is incomplete", 1)
	}

	err = wixFile.Normalize()
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	err = wixFile.RewriteFilePaths(out)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	msi, err = filepath.Abs(msi)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	msi, err = filepath.Rel(out, msi)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	cmdStr := wix.GenerateCmd(&wixFile, builtTemplates, msi, arch)

	targetFile := filepath.Join(out, "build.bat")
	err = ioutil.WriteFile(targetFile, []byte(cmdStr), 0644)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	return nil
}

func runWixCommands(c *cli.Context) error {
	out := c.String("out")

	bin, err := exec.LookPath("cmd.exe")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	args := []string{"/C", "build.bat"}
	oCmd := exec.Command(bin, args...)
	oCmd.Dir = out
	oCmd.Stdout = os.Stdout
	oCmd.Stderr = os.Stderr
	err = oCmd.Run()
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	return nil
}

func quickMake(c *cli.Context) error {
	path := c.String("path")
	src := c.String("src")
	out := c.String("out")
	version := c.String("version")
	license := c.String("license")
	msi := c.String("msi")
	arch := c.String("arch")
	keep := c.Bool("keep")

	if msi == "" {
		return cli.NewExitError("--msi parameter must be set", 1)
	}

	wixFile := manifest.WixManifest{}
	if err := wixFile.Load(path); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if wixFile.NeedGUID() {
		if _, err := wixFile.SetGuids(false); err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
	}

	// if err := os.RemoveAll(out); err != nil {
	// 	return cli.NewExitError(err.Error(), 1)
	// }
	if err := os.MkdirAll(out, 0744); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if c.IsSet("version") {
		wixFile.Version = version
	}

	if c.IsSet("license") {
		wixFile.License = license
	}

	if err := wixFile.Normalize(); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if err := wixFile.RewriteFilePaths(out); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if wixFile.License != "" {
		if !rtf.IsRtf(wixFile.License) {
			target := filepath.Join(out, filepath.Base(wixFile.License)+".rtf")
			err := rtf.WriteAsRtf(wixFile.License, target, true)
			if err != nil {
				return cli.NewExitError(err.Error(), 1)
			}
			wixFile.License, err = filepath.Rel(out, target)
			if err != nil {
				return cli.NewExitError(err.Error(), 1)
			}
		}
	}

	templates, err := tpls.Find(src, "*.wxs")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if len(templates) == 0 {
		return cli.NewExitError("No templates *.wxs found in this directory", 1)
	}

	builtTemplates := make([]string, len(templates))
	for i, tpl := range templates {
		dst := filepath.Join(out, filepath.Base(tpl))
		err = tpls.GenerateTemplate(&wixFile, tpl, dst)
		builtTemplates[i] = dst
		if err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
	}

	msi, err = filepath.Abs(msi)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	msi, err = filepath.Rel(out, msi)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	cmdStr := wix.GenerateCmd(&wixFile, builtTemplates, msi, arch)

	targetFile := filepath.Join(out, "build.bat")
	err = ioutil.WriteFile(targetFile, []byte(cmdStr), 0644)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	bin, err := exec.LookPath("cmd.exe")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	args := []string{"/C", "build.bat"}
	oCmd := exec.Command(bin, args...)
	oCmd.Dir = out
	oCmd.Stdout = os.Stdout
	oCmd.Stderr = os.Stderr
	err = oCmd.Run()
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if keep == false {
		// err = os.RemoveAll(out)
		// if err != nil {
			// return cli.NewExitError(err.Error(), 1)
		// }
	} else {
		fmt.Printf("Build files are available in %s\n", out)
	}

	fmt.Println("All Done!!")

	return nil
}

func chocoMake(c *cli.Context) error {
	path := c.String("path")
	src := c.String("src")
	out := c.String("out")
	input := c.String("input")
	version := c.String("version")
	changelogCmd := c.String("changelog-cmd")
	keep := c.Bool("keep")

	wixFile := manifest.WixManifest{}
	if err := wixFile.Load(path); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if err := os.RemoveAll(out); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if err := os.MkdirAll(out, 0744); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if c.IsSet("version") {
		wixFile.Version = version
	}

	if err := wixFile.Normalize(); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	templates, err := tpls.Find(src, "*")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if len(templates) == 0 {
		return cli.NewExitError("No templates found in this directory", 1)
	}

	out, err = filepath.Abs(out)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	wixFile.Choco.BuildDir = out
	wixFile.Choco.MsiFile = filepath.Base(input)
	wixFile.Choco.MsiSum, err = util.ComputeSha256(input)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if changelogCmd != "" {
		windows, err2 := stringexec.Command(changelogCmd)
		if err2 != nil {
			return cli.NewExitError(err.Error(), 1)
		}
		windows.Stderr = os.Stderr
		out, err3 := windows.Output()
		if err3 != nil {
			return cli.NewExitError(fmt.Sprintf("Failed to execute command to generate the changelog:%q\n%v", changelogCmd, err.Error()), 1)
		}
		sout := string(out)
		souts := strings.Split(sout, "\n")
		if len(souts) > 2 {
			souts = souts[2:] // why ? command line artifacts ? todo: put an explanation here.
		}
		sout = strings.Join(souts, "\n")

		wixFile.Choco.ChangeLog = sout
	}

	if err = util.CopyFile(filepath.Join(wixFile.Choco.BuildDir, wixFile.Choco.MsiFile), input); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	for _, tpl := range templates {
		dst := filepath.Join(out, filepath.Base(tpl))
		err = tpls.GenerateTemplate(&wixFile, tpl, dst)
		if err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
	}

	bin, err := exec.LookPath("choco")
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	oCmd := exec.Command(bin, "pack")
	oCmd.Dir = out
	oCmd.Stdout = os.Stdout
	oCmd.Stderr = os.Stderr
	err = oCmd.Run()
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	SrcNupkg := fmt.Sprintf("%s\\%s.%s.nupkg", out, wixFile.Choco.ID, wixFile.VersionOk)
	DstNupkg := fmt.Sprintf("%s.%s.nupkg", wixFile.Choco.ID, wixFile.Version)

	if err = util.CopyFile(DstNupkg, SrcNupkg); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	if keep == false {
		err = os.RemoveAll(out)
		if err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
	} else {
		fmt.Printf("Build files are available in %s\n", out)
	}

	fmt.Printf("Package copied to %s\n", DstNupkg)
	fmt.Println("All Done!!")

	return nil
}