/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 cli

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/spf13/cobra"
	"mynewt.apache.org/newt/newt/builder"
	"mynewt.apache.org/newt/newt/imgprod"
	"mynewt.apache.org/newt/newt/manifest"
	"mynewt.apache.org/newt/newt/pkg"
	"mynewt.apache.org/newt/newt/project"
	"mynewt.apache.org/newt/newt/target"
	"mynewt.apache.org/newt/util"
)

const TARGET_TEST_NAME = "unittest"

var testablePkgMap map[*pkg.LocalPackage]struct{}

func testablePkgs() map[*pkg.LocalPackage]struct{} {
	if testablePkgMap != nil {
		return testablePkgMap
	}

	testablePkgMap := map[*pkg.LocalPackage]struct{}{}

	// Create a map of path => lclPkg.
	proj, err := project.TryGetProject()
	if err != nil {
		return nil
	}

	allPkgs := proj.PackagesOfType(-1)
	pathLpkgMap := make(map[string]*pkg.LocalPackage, len(allPkgs))
	for _, p := range allPkgs {
		lpkg := p.(*pkg.LocalPackage)
		pathLpkgMap[lpkg.BasePath()] = lpkg
	}

	// Add all unit test packages to the testable package map.
	testPkgs := proj.PackagesOfType(pkg.PACKAGE_TYPE_UNITTEST)
	for _, p := range testPkgs {
		lclPack := p.(*pkg.LocalPackage)
		testablePkgMap[lclPack] = struct{}{}
	}

	// Next add first ancestor of each test package.
	for _, testPkgItf := range testPkgs {
		testPkg := testPkgItf.(*pkg.LocalPackage)
		for cur := filepath.ToSlash(filepath.Dir(testPkg.BasePath())); cur != proj.BasePath; cur = filepath.ToSlash(filepath.Dir(cur)) {
			lpkg := pathLpkgMap[cur]
			if lpkg != nil && lpkg.Type() != pkg.PACKAGE_TYPE_UNITTEST {
				testablePkgMap[lpkg] = struct{}{}
				break
			}
		}
	}

	return testablePkgMap
}

func pkgToUnitTests(pack *pkg.LocalPackage) []*pkg.LocalPackage {
	// If the user specified a unittest package, just test that one.
	if pack.Type() == pkg.PACKAGE_TYPE_UNITTEST {
		return []*pkg.LocalPackage{pack}
	}

	// Otherwise, return all the package's direct descendants that are unit
	// test packages.
	result := []*pkg.LocalPackage{}
	for p, _ := range testablePkgs() {
		if p.Type() == pkg.PACKAGE_TYPE_UNITTEST &&
			strings.HasPrefix(p.FullName(), pack.FullName()) {

			result = append(result, p)
		}
	}

	return result
}

var extraJtagCmd string
var noGDB_flag bool
var diffFriendly_flag bool
var imgFileOverride string
var elfFileOverride string

func buildRunCmd(cmd *cobra.Command, args []string, printShellCmds bool, executeShell bool) {
	if len(args) < 1 {
		NewtUsage(cmd, nil)
	}

	util.PrintShellCmds = printShellCmds
	util.ExecuteShell = executeShell

	TryGetProject()

	// Verify and resolve each specified package.
	targets, all, err := ResolveTargetsOrAll(args...)
	if err != nil {
		NewtUsage(cmd, err)
	}

	if all {
		// Collect all targets that specify an app package.
		targets = []*target.Target{}
		for _, name := range targetList() {
			t := ResolveTarget(name)
			if t != nil && t.AppName != "" {
				targets = append(targets, t)
			}
		}
	}

	for i, _ := range targets {
		// Reset the global state for the next build.
		// XXX: It is not good that this is necessary.  This is certainly going
		// to bite us...
		if i > 0 {
			if err := ResetGlobalState(); err != nil {
				NewtUsage(nil, err)
			}
		}

		// Look up the target by name.  This has to be done a second time here
		// now that the project has been reset.
		t := ResolveTarget(targets[i].FullName())
		if t == nil {
			NewtUsage(nil, util.NewNewtError("Failed to resolve target: "+
				targets[i].Name()))
		}

		util.StatusMessage(util.VERBOSITY_DEFAULT, "Building target %s\n",
			t.FullName())

		b, err := builder.NewTargetBuilder(t)
		if err != nil {
			NewtUsage(nil, err)
		}

		if err := b.Build(); err != nil {
			if b.AppBuilder != nil {
				if b.AppBuilder.GetModifiedRepos() != nil {
					util.ErrorMessage(util.VERBOSITY_DEFAULT,
						"Warning: Following external repos are modified or missing, which might be causing build errors:\n%v\n",
						b.AppBuilder.GetModifiedRepos())
				}
			}
			NewtUsage(nil, err)
		}

		// Produce bare "imageless" manifest.
		mopts, err := manifest.OptsForNonImage(b)
		if err != nil {
			NewtUsage(nil, err)
		}
		if err := imgprod.ProduceManifest(mopts); err != nil {
			NewtUsage(nil, err)
		}

		util.StatusMessage(util.VERBOSITY_DEFAULT,
			"Target successfully built: %s\n", t.Name())
	}
}

func cleanDir(path string) {
	util.StatusMessage(util.VERBOSITY_VERBOSE,
		"Cleaning directory %s\n", path)

	err := os.RemoveAll(path)
	if err != nil {
		NewtUsage(nil, util.NewNewtError(err.Error()))
	}
}

func cleanRunCmd(cmd *cobra.Command, args []string) {
	if len(args) < 1 {
		NewtUsage(cmd, util.NewNewtError("Must specify target"))
	}

	TryGetProject()

	cleanAll := false
	targets := []*target.Target{}
	for _, arg := range args {
		if arg == TARGET_KEYWORD_ALL {
			cleanAll = true
		} else {
			t, _, err := ResolveTargetOrUnittest(arg)
			if err != nil {
				NewtUsage(cmd, err)
			}
			targets = append(targets, t)
		}
	}

	if cleanAll {
		cleanDir(builder.BinRoot())
	} else {
		for _, t := range targets {
			cleanDir(builder.TargetBinDir(t.FullName()))
		}
	}
}

func pkgnames(pkgs []*pkg.LocalPackage) string {
	s := ""

	for _, p := range pkgs {
		s += p.Name() + " "
	}

	return s
}

func testRunCmd(cmd *cobra.Command, args []string, exclude string, executeShell bool, list bool) {
	if ((len(args) < 1) && !list) || ((len(args) > 0) && list) {
		NewtUsage(cmd, nil)
	}

	util.ExecuteShell = executeShell

	proj := TryGetProject()

	// Verify and resolve each specified package.
	testAll := false
	packs := []*pkg.LocalPackage{}

	if list {
		packItfs := proj.PackagesOfType(pkg.PACKAGE_TYPE_UNITTEST)
		testableNames := testablePkgList()
		packs = make([]*pkg.LocalPackage, len(packItfs))
		for i, p := range packItfs {
			packs[i] = p.(*pkg.LocalPackage)
		}

		names := testableNames
		for _, p := range packs {
			names = append(names, p.FullName())
		}

		sort.Strings(names)

		for _, name := range names {
			util.StatusMessage(util.VERBOSITY_DEFAULT, "%s\n", name)
		}

		return
	}

	for _, pkgName := range args {
		if pkgName == "all" {
			testAll = true
		} else {
			pack, err := proj.ResolvePackage(proj.LocalRepo(), pkgName)
			if err != nil {
				NewtUsage(cmd, err)
			}

			testPkgs := pkgToUnitTests(pack)
			if len(testPkgs) == 0 {
				NewtUsage(nil, util.FmtNewtError("Package %s contains no "+
					"unit tests", pack.FullName()))
			}

			packs = append(packs, testPkgs...)
		}
	}

	if testAll {
		packItfs := proj.PackagesOfType(pkg.PACKAGE_TYPE_UNITTEST)
		packs = make([]*pkg.LocalPackage, len(packItfs))
		for i, p := range packItfs {
			packs[i] = p.(*pkg.LocalPackage)
		}

		packs = pkg.SortLclPkgs(packs)
	}

	if len(exclude) > 0 {
		// filter out excluded tests
		orig := packs
		packs = packs[:0]
		excls := strings.Split(exclude, ",")
	packLoop:
		for _, pack := range orig {
			for _, excl := range excls {
				if pack.Name() == excl ||
					strings.HasPrefix(pack.Name(), excl+"/") ||
					pack.NameWithRepo() == excl ||
					strings.HasPrefix(pack.NameWithRepo(), excl+"/") {
					continue packLoop
				}
			}
			packs = append(packs, pack)
		}
	}

	if len(packs) == 0 {
		NewtUsage(nil, util.NewNewtError("No testable packages found"))
	}

	passedPkgs := []*pkg.LocalPackage{}
	failedPkgs := []*pkg.LocalPackage{}
	for _, pack := range packs {
		// Reset the global state for the next test.
		if err := ResetGlobalState(); err != nil {
			NewtUsage(nil, err)
		}

		t, err := ResolveUnittest(pack.Name())
		if err != nil {
			NewtUsage(nil, err)
		}

		b, err := builder.NewTargetTester(t, pack)
		if err != nil {
			NewtUsage(nil, err)
		}

		util.StatusMessage(util.VERBOSITY_DEFAULT, "Testing package %s\n",
			pack.FullName())

		err = b.SelfTestExecute()
		if err == nil {
			passedPkgs = append(passedPkgs, pack)
		} else {
			newtError := err.(*util.NewtError)
			util.StatusMessage(util.VERBOSITY_QUIET, newtError.Text)
			failedPkgs = append(failedPkgs, pack)
		}
	}

	passStr := fmt.Sprintf("Passed tests: [%s]", PackageNameList(passedPkgs))
	failStr := fmt.Sprintf("Failed tests: [%s]", PackageNameList(failedPkgs))

	if len(failedPkgs) > 0 {
		NewtUsage(nil, util.FmtNewtError("Test failure(s):\n%s\n%s", passStr,
			failStr))
	} else {
		util.StatusMessage(util.VERBOSITY_DEFAULT, "%s\n", passStr)
		util.StatusMessage(util.VERBOSITY_DEFAULT, "All tests passed\n")
	}
}

func loadRunCmd(cmd *cobra.Command, args []string) {
	if len(args) < 1 {
		NewtUsage(cmd, util.NewNewtError("Must specify target"))
	}

	TryGetProject()

	t := ResolveTarget(args[0])
	if t == nil {
		NewtUsage(cmd, util.NewNewtError("Invalid target name: "+args[0]))
	}

	b, err := builder.NewTargetBuilder(t)
	if err != nil {
		NewtUsage(nil, err)
	}

	if err := b.Load(extraJtagCmd, imgFileOverride); err != nil {
		NewtUsage(cmd, err)
	}
}

func debugRunCmd(cmd *cobra.Command, args []string) {
	if len(args) < 1 {
		NewtUsage(cmd, util.NewNewtError("Must specify target"))
	}

	TryGetProject()

	t := ResolveTarget(args[0])
	if t == nil {
		NewtUsage(cmd, util.NewNewtError("Invalid target name: "+args[0]))
	}

	b, err := builder.NewTargetBuilder(t)
	if err != nil {
		NewtUsage(nil, err)
	}

	if err := b.Debug(extraJtagCmd, false, noGDB_flag, elfFileOverride); err != nil {
		NewtUsage(cmd, err)
	}
}

func sizeRunCmd(cmd *cobra.Command, args []string, ram bool, flash bool, section string) {
	if len(args) < 1 {
		NewtUsage(cmd, util.NewNewtError("Must specify target"))
	}

	TryGetProject()

	t := ResolveTarget(args[0])
	if t == nil {
		NewtUsage(cmd, util.NewNewtError("Invalid target name: "+args[0]))
	}

	b, err := builder.NewTargetBuilder(t)
	if err != nil {
		NewtUsage(nil, err)
	}

	var sections []string

	if ram {
		sections = append(sections, "RAM")
	}

	if flash {
		sections = append(sections, "FLASH")
	}

	if section != "" {
		sections = append(sections, section)
	}

	if len(sections) > 0 {
		for _, sectionName := range sections {
			if err := b.SizeReport(sectionName, diffFriendly_flag); err != nil {
				NewtUsage(cmd, err)
			}
		}

		return
	}

	if err := b.Size(); err != nil {
		NewtUsage(cmd, err)
	}
}

func AddBuildCommands(cmd *cobra.Command) {
	var printShellCmds bool
	var executeShell bool

	buildCmd := &cobra.Command{
		Use:   "build <target-name> [target-names...]",
		Short: "Build one or more targets",
		Run: func(cmd *cobra.Command, args []string) {
			buildRunCmd(cmd, args, printShellCmds, executeShell)
		},
	}

	buildCmd.Flags().BoolVarP(&printShellCmds, "printCmds", "p", false,
		"Print executed build commands")

	buildCmd.Flags().StringVarP(&util.InjectSyscfg, "syscfg", "S", "",
		"Injected syscfg settings, key=value pairs separated by colon")

	buildCmd.Flags().BoolVar(&executeShell, "executeShell", false,
		"Execute build command using /bin/sh (Linux and MacOS only)")

	cmd.AddCommand(buildCmd)
	AddTabCompleteFn(buildCmd, func() []string {
		return append(targetList(), "all")
	})

	cleanCmd := &cobra.Command{
		Use:   "clean <target-name> [target-names...] | all",
		Short: "Delete build artifacts for one or more targets",
		Run:   cleanRunCmd,
	}

	cmd.AddCommand(cleanCmd)
	AddTabCompleteFn(cleanCmd, func() []string {
		return append(append(targetList(), unittestList()...), "all")
	})

	var exclude string
	var list bool
	testCmd := &cobra.Command{
		Use:   "test <package-name> [package-names...] | all",
		Short: "Executes unit tests for one or more packages",
		Run: func(cmd *cobra.Command, args []string) {
			testRunCmd(cmd, args, exclude, executeShell, list)
		},
	}
	testCmd.Flags().StringVarP(&exclude, "exclude", "e", "", "Comma separated list of packages to exclude")
	testCmd.Flags().BoolVar(&executeShell, "executeShell", false,
		"Execute build command using /bin/sh (Linux and MacOS only)")
	testCmd.Flags().BoolVar(&list,
		"list", false,
		"Show all available unit tests")

	cmd.AddCommand(testCmd)
	AddTabCompleteFn(testCmd, func() []string {
		return append(testablePkgList(), "all", "allexcept")
	})

	loadHelpText := "Load application image on to the board for <target-name>"

	loadCmd := &cobra.Command{
		Use:   "load <target-name>",
		Short: "Load built target to board",
		Long:  loadHelpText,
		Run:   loadRunCmd,
	}

	cmd.AddCommand(loadCmd)
	AddTabCompleteFn(loadCmd, targetList)

	loadCmd.PersistentFlags().StringVarP(&extraJtagCmd, "extrajtagcmd", "", "",
		"Extra commands to send to JTAG software")
	loadCmd.PersistentFlags().StringVarP(&imgFileOverride, "imgfile", "", "",
		"Path of .img file to load instead of target artifact")

	debugHelpText := "Open a debugger session for <target-name>"

	debugCmd := &cobra.Command{
		Use:   "debug <target-name>",
		Short: "Open debugger session to target",
		Long:  debugHelpText,
		Run:   debugRunCmd,
	}

	debugCmd.PersistentFlags().StringVarP(&extraJtagCmd, "extrajtagcmd", "",
		"", "Extra commands to send to JTAG software")
	debugCmd.PersistentFlags().BoolVarP(&noGDB_flag, "noGDB", "n", false,
		"Do not start GDB from command line")
	debugCmd.PersistentFlags().StringVarP(&elfFileOverride, "elffile", "",
		"", "Path of .elf file to debug instead of target artifact")

	cmd.AddCommand(debugCmd)
	AddTabCompleteFn(debugCmd, targetList)

	sizeHelpText := "Calculate the size of target components specified by " +
		"<target-name>."

	var ram, flash bool
	var section string
	sizeCmd := &cobra.Command{
		Use:   "size <target-name>",
		Short: "Size of target components",
		Long:  sizeHelpText,
		Run: func(cmd *cobra.Command, args []string) {
			sizeRunCmd(cmd, args, ram, flash, section)
		},
	}

	sizeCmd.PersistentFlags().BoolVarP(&diffFriendly_flag, "diffable", "d", false,
		"Produce diff-friendly output of statistics")
	sizeCmd.Flags().BoolVarP(&ram, "ram", "R", false, "Print RAM statistics")
	sizeCmd.Flags().BoolVarP(&flash, "flash", "F", false,
		"Print FLASH statistics")
	sizeCmd.Flags().StringVarP(&section, "section", "S", "", "Print section statistics")

	cmd.AddCommand(sizeCmd)
	AddTabCompleteFn(sizeCmd, targetList)
}
