/**
 * 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 builder

import (
	"io/ioutil"
	"os"
	"os/exec"

	"github.com/kballard/go-shellquote"
	log "github.com/sirupsen/logrus"
	"mynewt.apache.org/newt/newt/stage"
	"mynewt.apache.org/newt/util"
)

// replaceArtifactsIfChanged compares the artifacts just produced (temp
// directory) to those from the previous build (user bin directory).  If they
// are different, it replaces the old with the new so that they get relinked
// during this build.
func replaceArtifactsIfChanged(oldDir string, newDir string) error {
	eq, err := util.DirsAreEqual(oldDir, newDir)
	if err != nil {
		return err
	}

	if eq {
		// No changes detected.
		return nil
	}

	log.Debugf("changes detected; replacing %s with %s", oldDir, newDir)
	os.RemoveAll(oldDir)
	if err := util.MoveDir(newDir, oldDir); err != nil {
		return err
	}

	return nil
}

// createTempUserDirs creates a set of temporary directories for holding build
// inputs.  It returns:
//     * base-dir
//     * src-dir
//     * include-dir
func createTempUserDirs(label string) (string, string, string, error) {
	tmpDir, err := ioutil.TempDir("", "mynewt-user-"+label)
	if err != nil {
		return "", "", "", util.ChildNewtError(err)
	}
	log.Debugf("created user %s dir: %s", label, tmpDir)

	tmpSrcDir := UserTempSrcDir(tmpDir)
	log.Debugf("creating user %s src dir: %s", label, tmpSrcDir)
	if err := os.MkdirAll(tmpSrcDir, 0755); err != nil {
		os.RemoveAll(tmpDir)
		return "", "", "", util.ChildNewtError(err)
	}

	tmpIncDir := UserTempIncludeDir(tmpDir)
	log.Debugf("creating user %s include dir: %s", label, tmpIncDir)
	if err := os.MkdirAll(tmpIncDir, 0755); err != nil {
		os.RemoveAll(tmpDir)
		return "", "", "", util.ChildNewtError(err)
	}

	return tmpDir, tmpSrcDir, tmpIncDir, nil
}

// envVarsForCmd calculates the set of environment variables to export for the
// specified external command.
func (t *TargetBuilder) envVarsForCmd(sf stage.StageFunc, userSrcDir string,
	userIncDir string, workDir string) (map[string]string, error) {

	// Determine whether the owning package is part of the loader or the app.
	slot := 0
	buildName := "app"

	if t.LoaderBuilder != nil {
		rpkg := t.res.LpkgRpkgMap[sf.Pkg]
		if rpkg == nil {
			return nil, util.FmtNewtError(
				"resolution missing expected package: %s", sf.Pkg.FullName())
		}

		if t.LoaderBuilder.PkgMap[rpkg] != nil {
			buildName = "loader"
		} else {
			slot = 1
		}
	}

	env, err := t.AppBuilder.EnvVars(slot)
	if err != nil {
		return nil, err
	}

	p := UserEnvParams{
		Lpkg:         sf.Pkg,
		TargetName:   t.target.FullName(),
		BuildProfile: t.target.BuildProfile,
		AppName:      t.appPkg.FullName(),
		BuildName:    buildName,
		UserSrcDir:   userSrcDir,
		UserIncDir:   userIncDir,
		WorkDir:      workDir,
		GeneratedDir: GeneratedBaseDir(t.target.FullName()),
		TergetBinDir: TargetBinDir(t.target.FullName()),
	}
	uenv := UserEnvVars(p)
	for k, v := range uenv {
		env[k] = v
	}

	c, err := t.NewCompiler("", "")
	if err != nil {
		return nil, err
	}
	tenv := ToolchainEnvVars(c)
	for k, v := range tenv {
		env[k] = v
	}

	return env, nil
}

// execExtCmds executes a set of user scripts.
func (t *TargetBuilder) execExtCmds(sf stage.StageFunc, userSrcDir string,
	userIncDir string, workDir string) error {

	env, err := t.envVarsForCmd(sf, userSrcDir, userIncDir, workDir)
	if err != nil {
		return err
	}

	toks, err := shellquote.Split(sf.Name)
	if err != nil {
		return util.FmtNewtError(
			"invalid command string: \"%s\": %s", sf.Name, err.Error())
	}

	// Replace environment variables in command string.
	for i, tok := range toks {
		toks[i] = os.ExpandEnv(tok)
	}

	// If the command is in the user's PATH, expand it to its real location.
	cmd, err := exec.LookPath(toks[0])
	if err == nil {
		toks[0] = cmd
	}

	// Execute the commands from the package's directory.
	pwd, err := os.Getwd()
	if err != nil {
		return util.ChildNewtError(err)
	}
	if err := os.Chdir(sf.Pkg.BasePath()); err != nil {
		return util.ChildNewtError(err)
	}
	defer os.Chdir(pwd)

	util.StatusMessage(util.VERBOSITY_DEFAULT, "Executing %s/%s\n", sf.Pkg.FullName(), sf.Name)
	if err := util.ShellInteractiveCommand(toks, env, true); err != nil {
		return err
	}

	return nil
}

func getLinkTableEntry(name string) string {
	indent := "        "

	entry := indent + "__" + name + "_start__ = .;\n" +
		indent + name + " = .;\n" +
		indent + "KEEP(*(." + name + "))\n" +
		indent + "KEEP(*(SORT(." + name + ".*)))\n" +
		indent + "__" + name + "_end__ = .;\n\n"

	return entry
}

func (t *TargetBuilder) generateLinkTables() {
	var s []string

	dir := GeneratedBaseDir(t.target.FullName()) + "/link/include"
	err := os.MkdirAll(dir, os.ModePerm)
	if err != nil {
		log.Error("Generate link tables error:\n", err)
		return
	}

	linkHeader, err := os.Create(dir + "/link_tables.ld.h")
	if err != nil {
		log.Error("Generate link tables error:\n", err)
		return
	}

	for _, pkg := range t.res.LpkgRpkgMap {
		s = append(s, pkg.Lpkg.LinkTables()...)
	}

	if len(s) == 0 {
		return
	}

	for _, linkTable := range s {
		linkHeader.WriteString(getLinkTableEntry(linkTable))
	}

}

//link tables
// execPreBuildCmds runs the target's set of pre-build user commands.  It is an
// error if any command fails (exits with a nonzero status).
func (t *TargetBuilder) execPreBuildCmds(workDir string) error {
	// Create temporary directories where scripts can put build inputs.
	tmpDir, tmpSrcDir, tmpIncDir, err := createTempUserDirs("pre-build")
	if err != nil {
		return err
	}
	defer func() {
		log.Debugf("removing user pre-build dir: %s", tmpDir)
		os.RemoveAll(tmpDir)
	}()

	for _, sf := range t.res.PreBuildCmdCfg.StageFuncs {
		if err := t.execExtCmds(sf, tmpSrcDir, tmpIncDir, workDir); err != nil {
			return err
		}
	}

	srcDir := UserPreBuildSrcDir(t.target.FullName())
	if err := replaceArtifactsIfChanged(srcDir, tmpSrcDir); err != nil {
		return err
	}

	incDir := UserPreBuildIncludeDir(t.target.FullName())
	if err := replaceArtifactsIfChanged(incDir, tmpIncDir); err != nil {
		return err
	}

	return nil
}

// execPreLinkCmds runs the target's set of post-build user commands.  It is
// an error if any command fails (exits with a nonzero status).
func (t *TargetBuilder) execPreLinkCmds(workDir string) error {
	// Create temporary directories where scripts can put build inputs.
	tmpDir, tmpSrcDir, _, err := createTempUserDirs("pre-link")
	if err != nil {
		return err
	}
	defer func() {
		log.Debugf("removing user pre-link dir: %s", tmpDir)
		os.RemoveAll(tmpDir)
	}()

	for _, sf := range t.res.PreLinkCmdCfg.StageFuncs {
		if err := t.execExtCmds(sf, tmpSrcDir, "", workDir); err != nil {
			return err
		}
	}

	srcDir := UserPreLinkSrcDir(t.target.FullName())
	err = replaceArtifactsIfChanged(srcDir, tmpSrcDir)
	if err != nil {
		return err
	}

	return nil
}

// execPostLinkCmds runs the target's set of post-build user commands.  It is
// an error if any command fails (exits with a nonzero status).
func (t *TargetBuilder) execPostLinkCmds(workDir string) error {
	for _, sf := range t.res.PostLinkCmdCfg.StageFuncs {
		if err := t.execExtCmds(sf, "", "", workDir); err != nil {
			return err
		}
	}

	return nil
}

// makeUserDir creates a temporary directory where scripts can put build
// inputs.
func makeUserDir() (string, error) {
	tmpDir, err := ioutil.TempDir("", "mynewt-user")
	if err != nil {
		return "", util.ChildNewtError(err)
	}
	log.Debugf("created user dir: %s", tmpDir)

	return tmpDir, nil
}

func makeUserWorkDir() (string, error) {
	tmpDir, err := ioutil.TempDir("", "mynewt-user-work")
	if err != nil {
		return "", util.ChildNewtError(err)
	}
	log.Debugf("created user work dir: %s", tmpDir)

	return tmpDir, nil
}
