// This file is part of arduino-cli.
//
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package builder

import (
	"bytes"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/arduino/arduino-cli/arduino/globals"
	"github.com/arduino/arduino-cli/arduino/sketch"
	"github.com/arduino/arduino-cli/cli/errorcodes"
	"github.com/arduino/arduino-cli/cli/feedback"

	"github.com/pkg/errors"
)

// As currently implemented on Linux,
// the maximum number of symbolic links that will be followed while resolving a pathname is 40
const maxFileSystemDepth = 40

var includesArduinoH = regexp.MustCompile(`(?m)^\s*#\s*include\s*[<\"]Arduino\.h[>\"]`)

// QuoteCppString returns the given string as a quoted string for use with the C
// preprocessor. This adds double quotes around it and escapes any
// double quotes and backslashes in the string.
func QuoteCppString(str string) string {
	str = strings.Replace(str, "\\", "\\\\", -1)
	str = strings.Replace(str, "\"", "\\\"", -1)
	return "\"" + str + "\""
}

// SketchSaveItemCpp saves a preprocessed .cpp sketch file on disk
func SketchSaveItemCpp(path string, contents []byte, destPath string) error {

	sketchName := filepath.Base(path)

	if err := os.MkdirAll(destPath, os.FileMode(0755)); err != nil {
		return errors.Wrap(err, "unable to create a folder to save the sketch")
	}

	destFile := filepath.Join(destPath, sketchName+".cpp")

	if err := ioutil.WriteFile(destFile, contents, os.FileMode(0644)); err != nil {
		return errors.Wrap(err, "unable to save the sketch on disk")
	}

	return nil
}

// simpleLocalWalk locally replaces filepath.Walk and/but goes through symlinks
func simpleLocalWalk(root string, maxDepth int, walkFn func(path string, info os.FileInfo, err error) error) error {

	info, err := os.Stat(root)

	if err != nil {
		return walkFn(root, nil, err)
	}

	err = walkFn(root, info, err)
	if err == filepath.SkipDir {
		return nil
	}

	if info.IsDir() {
		if maxDepth <= 0 {
			return walkFn(root, info, errors.New("Filesystem bottom is too deep (directory recursion or filesystem really deep): "+root))
		}
		maxDepth--
		files, err := ioutil.ReadDir(root)
		if err == nil {
			for _, file := range files {
				err = simpleLocalWalk(root+string(os.PathSeparator)+file.Name(), maxDepth, walkFn)
				if err == filepath.SkipDir {
					return nil
				}
			}
		}
	}

	return nil
}

// SketchLoad collects all the files composing a sketch.
// The parameter `sketchPath` holds a path pointing to a single sketch file or a sketch folder,
// the path must be absolute.
func SketchLoad(sketchPath, buildPath string) (*sketch.Sketch, error) {
	stat, err := os.Stat(sketchPath)
	if err != nil {
		return nil, errors.Wrap(err, "unable to stat Sketch location")
	}

	var sketchFolder, mainSketchFile string

	// if a sketch folder was passed, save the parent and point sketchPath to the main sketch file
	if stat.IsDir() {
		sketchFolder = sketchPath
		// allowed extensions are .ino and .pde (but not both)
		for extension := range globals.MainFileValidExtensions {
			candidateSketchFile := filepath.Join(sketchPath, stat.Name()+extension)
			if _, err := os.Stat(candidateSketchFile); !os.IsNotExist(err) {
				if mainSketchFile == "" {
					mainSketchFile = candidateSketchFile
				} else {
					return nil, errors.Errorf("multiple main sketch files found (%v,%v)",
						filepath.Base(mainSketchFile),
						filepath.Base(candidateSketchFile))
				}
			}
		}

		// check main file was found
		if mainSketchFile == "" {
			return nil, errors.Errorf("unable to find a sketch file in directory %v", sketchFolder)
		}

		// check main file is readable
		f, err := os.Open(mainSketchFile)
		if err != nil {
			return nil, errors.Wrap(err, "unable to open the main sketch file")
		}
		f.Close()

		// ensure it is not a directory
		info, err := os.Stat(mainSketchFile)
		if err != nil {
			return nil, errors.Wrap(err, "unable to check the main sketch file")
		}
		if info.IsDir() {
			return nil, errors.Wrap(errors.New(mainSketchFile), "sketch must not be a directory")
		}
	} else {
		sketchFolder = filepath.Dir(sketchPath)
		mainSketchFile = sketchPath
	}

	// collect all the sketch files
	var files []string
	rootVisited := false
	err = simpleLocalWalk(sketchFolder, maxFileSystemDepth, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			feedback.Errorf("Error during sketch processing: %v", err)
			os.Exit(errorcodes.ErrGeneric)
		}

		if info.IsDir() {
			// Filters in this if-block are NOT applied to the sketch folder itself.
			// Since the sketch folder is the first one processed by simpleLocalWalk,
			// we can set the `rootVisited` guard to exclude it.
			if rootVisited {
				// skip hidden folders
				if strings.HasPrefix(info.Name(), ".") {
					return filepath.SkipDir
				}

				// skip legacy SCM directories
				if info.Name() == "CVS" || info.Name() == "RCS" {
					return filepath.SkipDir
				}
			} else {
				rootVisited = true
			}

			// ignore (don't skip) directory
			return nil
		}

		// ignore hidden files
		if strings.HasPrefix(info.Name(), ".") {
			return nil
		}

		// ignore if file extension doesn't match
		ext := strings.ToLower(filepath.Ext(path))
		_, isMain := globals.MainFileValidExtensions[ext]
		_, isAdditional := globals.AdditionalFileValidExtensions[ext]
		if !(isMain || isAdditional) {
			return nil
		}

		// check if file is readable
		f, err := os.Open(path)
		if err != nil {
			return nil
		}
		f.Close()

		// collect the file
		files = append(files, path)

		// done
		return nil
	})

	if err != nil {
		return nil, errors.Wrap(err, "there was an error while collecting the sketch files")
	}

	return sketch.New(sketchFolder, mainSketchFile, buildPath, files)
}

// SketchMergeSources merges all the source files included in a sketch
func SketchMergeSources(sketch *sketch.Sketch) (int, string, error) {
	lineOffset := 0
	mergedSource := ""

	// add Arduino.h inclusion directive if missing
	mainSrc, err := sketch.MainFile.GetSourceStr()
	if err != nil {
		return 0, "", err
	}
	if !includesArduinoH.MatchString(mainSrc) {
		mergedSource += "#include <Arduino.h>\n"
		lineOffset++
	}

	mergedSource += "#line 1 " + QuoteCppString(sketch.MainFile.Path) + "\n"
	mergedSource += mainSrc + "\n"
	lineOffset++

	for _, item := range sketch.OtherSketchFiles {
		src, err := item.GetSourceStr()
		if err != nil {
			return 0, "", err
		}
		mergedSource += "#line 1 " + QuoteCppString(item.Path) + "\n"
		mergedSource += src + "\n"
	}

	return lineOffset, mergedSource, nil
}

// SketchCopyAdditionalFiles copies the additional files for a sketch to the
// specified destination directory.
func SketchCopyAdditionalFiles(sketch *sketch.Sketch, destPath string) error {
	if err := os.MkdirAll(destPath, os.FileMode(0755)); err != nil {
		return errors.Wrap(err, "unable to create a folder to save the sketch files")
	}

	for _, item := range sketch.AdditionalFiles {
		relpath, err := filepath.Rel(sketch.LocationPath, item.Path)
		if err != nil {
			return errors.Wrap(err, "unable to compute relative path to the sketch for the item")
		}

		targetPath := filepath.Join(destPath, relpath)
		// create the directory containing the target
		if err = os.MkdirAll(filepath.Dir(targetPath), os.FileMode(0755)); err != nil {
			return errors.Wrap(err, "unable to create the folder containing the item")
		}

		err = writeIfDifferent(item.Path, targetPath)
		if err != nil {
			return errors.Wrap(err, "unable to write to destination file")
		}
	}

	return nil
}

func writeIfDifferent(sourcePath, destPath string) error {
	// read the source file
	newbytes, err := ioutil.ReadFile(sourcePath)
	if err != nil {
		return errors.Wrap(err, "unable to read contents of the source item")
	}

	// check whether the destination file exists
	_, err = os.Stat(destPath)
	if os.IsNotExist(err) {
		// write directly
		return ioutil.WriteFile(destPath, newbytes, os.FileMode(0644))
	}

	// read the destination file if it ex
	existingBytes, err := ioutil.ReadFile(destPath)
	if err != nil {
		return errors.Wrap(err, "unable to read contents of the destination item")
	}

	// overwrite if contents are different
	if bytes.Compare(existingBytes, newbytes) != 0 {
		return ioutil.WriteFile(destPath, newbytes, os.FileMode(0644))
	}

	// source and destination are the same, don't write anything
	return nil
}
