// 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 cores

import (
	"regexp"
	"runtime"

	"github.com/arduino/arduino-cli/arduino/resources"
	"github.com/arduino/go-paths-helper"
	properties "github.com/arduino/go-properties-orderedmap"
	semver "go.bug.st/relaxed-semver"
)

// Tool represents a single Tool, part of a Package.
type Tool struct {
	Name     string                  `json:"name,required"` // The Name of the Tool.
	Releases map[string]*ToolRelease `json:"releases"`      // Maps Version to Release.
	Package  *Package                `json:"-"`
}

// ToolRelease represents a single release of a tool
type ToolRelease struct {
	Version    *semver.RelaxedVersion `json:"version,required"` // The version number of this Release.
	Flavors    []*Flavor              `json:"systems"`          // Maps OS to Flavor
	Tool       *Tool                  `json:"-"`
	InstallDir *paths.Path            `json:"-"`
}

// Flavor represents a flavor of a Tool version.
type Flavor struct {
	OS       string `json:"os,required"` // The OS Supported by this flavor.
	Resource *resources.DownloadResource
}

// GetOrCreateRelease returns the ToolRelease object with the specified version
// or creates a new one if not found
func (tool *Tool) GetOrCreateRelease(version *semver.RelaxedVersion) *ToolRelease {
	if release, ok := tool.Releases[version.String()]; ok {
		return release
	}
	release := &ToolRelease{
		Version: version,
		Tool:    tool,
	}
	tool.Releases[version.String()] = release
	return release
}

// FindReleaseWithRelaxedVersion returns the specified release corresponding the provided version,
// or nil if not found.
func (tool *Tool) FindReleaseWithRelaxedVersion(version *semver.RelaxedVersion) *ToolRelease {
	return tool.Releases[version.String()]
}

// GetAllReleasesVersions returns all the version numbers in this Core Package.
func (tool *Tool) GetAllReleasesVersions() []*semver.RelaxedVersion {
	releases := tool.Releases
	versions := []*semver.RelaxedVersion{}
	for _, release := range releases {
		versions = append(versions, release.Version)
	}

	return versions
}

// LatestRelease obtains latest version of a core package.
func (tool *Tool) LatestRelease() *ToolRelease {
	latest := tool.latestReleaseVersion()
	if latest == nil {
		return nil
	}

	return tool.FindReleaseWithRelaxedVersion(latest)
}

// latestReleaseVersion obtains latest version number.
func (tool *Tool) latestReleaseVersion() *semver.RelaxedVersion {
	versions := tool.GetAllReleasesVersions()
	if len(versions) == 0 {
		return nil
	}
	max := versions[0]
	for i := 1; i < len(versions); i++ {
		if versions[i].GreaterThan(max) {
			max = versions[i]
		}
	}
	return max
}

// GetLatestInstalled returns the latest installed ToolRelease for the Tool, or nil if no releases are installed.
func (tool *Tool) GetLatestInstalled() *ToolRelease {
	var latest *ToolRelease
	for _, release := range tool.Releases {
		if release.IsInstalled() {
			if latest == nil {
				latest = release
			} else if latest.Version.LessThan(release.Version) {
				latest = release
			}
		}
	}
	return latest
}

func (tool *Tool) String() string {
	return tool.Package.Name + ":" + tool.Name
}

// IsInstalled returns true if the ToolRelease is installed
func (tr *ToolRelease) IsInstalled() bool {
	return tr.InstallDir != nil
}

func (tr *ToolRelease) String() string {
	return tr.Tool.String() + "@" + tr.Version.String()
}

// RuntimeProperties returns the runtime properties for this tool
func (tr *ToolRelease) RuntimeProperties() *properties.Map {
	res := properties.NewMap()
	if tr.IsInstalled() {
		res.Set("runtime.tools."+tr.Tool.Name+".path", tr.InstallDir.String())
		res.Set("runtime.tools."+tr.Tool.Name+"-"+tr.Version.String()+".path", tr.InstallDir.String())
	}
	return res
}

var (
	regexpArmLinux   = regexp.MustCompile("arm.*-linux-gnueabihf")
	regexpArm64Linux = regexp.MustCompile("(aarch64|arm64)-linux-gnu")
	regexpAmd64      = regexp.MustCompile("x86_64-.*linux-gnu")
	regexpi386       = regexp.MustCompile("i[3456]86-.*linux-gnu")
	regexpWindows    = regexp.MustCompile("i[3456]86-.*(mingw32|cygwin)")
	regexpMac64Bit   = regexp.MustCompile("(i[3456]86|x86_64)-apple-darwin.*")
	regexpmac32Bit   = regexp.MustCompile("i[3456]86-apple-darwin.*")
	regexpArmBSD     = regexp.MustCompile("arm.*-freebsd[0-9]*")
)

func (f *Flavor) isCompatibleWithCurrentMachine() bool {
	return f.isCompatibleWith(runtime.GOOS, runtime.GOARCH)
}

func (f *Flavor) isCompatibleWith(osName, osArch string) bool {
	if f.OS == "all" {
		return true
	}

	switch osName + "," + osArch {
	case "linux,arm", "linux,armbe":
		return regexpArmLinux.MatchString(f.OS)
	case "linux,arm64":
		return regexpArm64Linux.MatchString(f.OS)
	case "linux,amd64":
		return regexpAmd64.MatchString(f.OS)
	case "linux,386":
		return regexpi386.MatchString(f.OS)
	case "windows,386", "windows,amd64":
		return regexpWindows.MatchString(f.OS)
	case "darwin,amd64":
		return regexpmac32Bit.MatchString(f.OS) || regexpMac64Bit.MatchString(f.OS)
	case "darwin,386":
		return regexpmac32Bit.MatchString(f.OS)
	case "freebsd,arm":
		return regexpArmBSD.MatchString(f.OS)
	case "freebsd,386", "freebsd,amd64":
		genericFreeBSDexp := regexp.MustCompile(osArch + "%s-freebsd[0-9]*")
		return genericFreeBSDexp.MatchString(f.OS)
	}
	return false
}

// GetCompatibleFlavour returns the downloadable resource compatible with the running O.S.
func (tr *ToolRelease) GetCompatibleFlavour() *resources.DownloadResource {
	for _, flavour := range tr.Flavors {
		if flavour.isCompatibleWithCurrentMachine() {
			return flavour.Resource
		}
	}
	return nil
}
