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

import (
	"errors"
	"fmt"
	"net/url"
	"path"
	"strings"

	"github.com/arduino/arduino-cli/arduino/cores"
	"github.com/arduino/arduino-cli/arduino/cores/packageindex"
	paths "github.com/arduino/go-paths-helper"
	properties "github.com/arduino/go-properties-orderedmap"
	"github.com/sirupsen/logrus"
	semver "go.bug.st/relaxed-semver"
)

// PackageManager defines the superior oracle which understands all about
// Arduino Packages, how to parse them, download, and so on.
//
// The manager also keeps track of the status of the Packages (their Platform Releases, actually)
// installed in the system.
type PackageManager struct {
	Log                    logrus.FieldLogger
	Packages               cores.Packages
	IndexDir               *paths.Path
	PackagesDir            *paths.Path
	DownloadDir            *paths.Path
	TempDir                *paths.Path
	CustomGlobalProperties *properties.Map
}

// NewPackageManager returns a new instance of the PackageManager
func NewPackageManager(indexDir, packagesDir, downloadDir, tempDir *paths.Path) *PackageManager {
	return &PackageManager{
		Log:                    logrus.StandardLogger(),
		Packages:               cores.NewPackages(),
		IndexDir:               indexDir,
		PackagesDir:            packagesDir,
		DownloadDir:            downloadDir,
		TempDir:                tempDir,
		CustomGlobalProperties: properties.NewMap(),
	}
}

// FindPlatformReleaseProvidingBoardsWithVidPid FIXMEDOC
func (pm *PackageManager) FindPlatformReleaseProvidingBoardsWithVidPid(vid, pid string) []*cores.PlatformRelease {
	res := []*cores.PlatformRelease{}
	for _, targetPackage := range pm.Packages {
		for _, targetPlatform := range targetPackage.Platforms {
			platformRelease := targetPlatform.GetLatestRelease()
			if platformRelease == nil {
				continue
			}
			for _, boardManifest := range platformRelease.BoardsManifest {
				if boardManifest.HasUsbID(vid, pid) {
					res = append(res, platformRelease)
					break
				}
			}
		}
	}
	return res
}

// FindBoardsWithVidPid FIXMEDOC
func (pm *PackageManager) FindBoardsWithVidPid(vid, pid string) []*cores.Board {
	res := []*cores.Board{}
	for _, targetPackage := range pm.Packages {
		for _, targetPlatform := range targetPackage.Platforms {
			if platform := pm.GetInstalledPlatformRelease(targetPlatform); platform != nil {
				for _, board := range platform.Boards {
					if board.HasUsbID(vid, pid) {
						res = append(res, board)
					}
				}
			}
		}
	}
	return res
}

// FindBoardsWithID FIXMEDOC
func (pm *PackageManager) FindBoardsWithID(id string) []*cores.Board {
	res := []*cores.Board{}
	for _, targetPackage := range pm.Packages {
		for _, targetPlatform := range targetPackage.Platforms {
			if platform := pm.GetInstalledPlatformRelease(targetPlatform); platform != nil {
				for _, board := range platform.Boards {
					if board.BoardID == id {
						res = append(res, board)
					}
				}
			}
		}
	}
	return res
}

// FindBoardWithFQBN returns the board identified by the fqbn, or an error
func (pm *PackageManager) FindBoardWithFQBN(fqbnIn string) (*cores.Board, error) {
	fqbn, err := cores.ParseFQBN(fqbnIn)
	if err != nil {
		return nil, fmt.Errorf("parsing fqbn: %s", err)
	}

	_, _, board, _, _, err := pm.ResolveFQBN(fqbn)
	return board, err
}

// ResolveFQBN returns, in order:
// - the Package pointed by the fqbn
// - the PlatformRelease pointed by the fqbn
// - the Board pointed by the fqbn
// - the build properties for the board considering also the
//   configuration part of the fqbn
// - the PlatformRelease to be used for the build (if the board
//   requires a 3rd party core it may be different from the
//   PlatformRelease pointed by the fqbn)
// - an error if any of the above is not found
//
// In case of error the partial results found in the meantime are
// returned together with the error.
func (pm *PackageManager) ResolveFQBN(fqbn *cores.FQBN) (
	*cores.Package, *cores.PlatformRelease, *cores.Board,
	*properties.Map, *cores.PlatformRelease, error) {

	// Find package
	targetPackage := pm.Packages[fqbn.Package]
	if targetPackage == nil {
		return nil, nil, nil, nil, nil,
			errors.New("unknown package " + fqbn.Package)
	}

	// Find platform
	platform := targetPackage.Platforms[fqbn.PlatformArch]
	if platform == nil {
		return targetPackage, nil, nil, nil, nil,
			fmt.Errorf("unknown platform %s:%s", targetPackage, fqbn.PlatformArch)
	}
	platformRelease := pm.GetInstalledPlatformRelease(platform)
	if platformRelease == nil {
		return targetPackage, nil, nil, nil, nil,
			fmt.Errorf("platform %s is not installed", platform)
	}

	// Find board
	board := platformRelease.Boards[fqbn.BoardID]
	if board == nil {
		return targetPackage, platformRelease, nil, nil, nil,
			fmt.Errorf("board %s:%s not found", platformRelease, fqbn.BoardID)
	}

	buildProperties, err := board.GetBuildProperties(fqbn.Configs)
	if err != nil {
		return targetPackage, platformRelease, board, nil, nil,
			fmt.Errorf("getting build properties for board %s: %s", board, err)
	}

	// Determine the platform used for the build (in case the board refers
	// to a core contained in another platform)
	buildPlatformRelease := platformRelease
	coreParts := strings.Split(buildProperties.Get("build.core"), ":")
	if len(coreParts) > 1 {
		referredPackage := coreParts[0]
		buildPackage := pm.Packages[referredPackage]
		if buildPackage == nil {
			return targetPackage, platformRelease, board, buildProperties, nil,
				fmt.Errorf("missing package %s referenced by board %s", referredPackage, fqbn)
		}
		buildPlatform := buildPackage.Platforms[fqbn.PlatformArch]
		if buildPlatform == nil {
			return targetPackage, platformRelease, board, buildProperties, nil,
				fmt.Errorf("missing platform %s:%s referenced by board %s", referredPackage, fqbn.PlatformArch, fqbn)
		}
		buildPlatformRelease = pm.GetInstalledPlatformRelease(buildPlatform)
		if buildPlatformRelease == nil {
			return targetPackage, platformRelease, board, buildProperties, nil,
				fmt.Errorf("missing platform release %s:%s referenced by board %s", referredPackage, fqbn.PlatformArch, fqbn)
		}
	}

	// No errors... phew!
	return targetPackage, platformRelease, board, buildProperties, buildPlatformRelease, nil
}

// LoadPackageIndex loads a package index by looking up the local cached file from the specified URL
func (pm *PackageManager) LoadPackageIndex(URL *url.URL) error {
	indexPath := pm.IndexDir.Join(path.Base(URL.Path))
	index, err := packageindex.LoadIndex(indexPath)
	if err != nil {
		return fmt.Errorf("loading json index file %s: %s", indexPath, err)
	}

	for _, p := range index.Packages {
		p.URL = URL.String()
	}

	index.MergeIntoPackages(pm.Packages)
	return nil
}

// LoadPackageIndexFromFile load a package index from the specified file
func (pm *PackageManager) LoadPackageIndexFromFile(indexPath *paths.Path) (*packageindex.Index, error) {
	index, err := packageindex.LoadIndex(indexPath)
	if err != nil {
		return nil, fmt.Errorf("loading json index file %s: %s", indexPath, err)
	}

	index.MergeIntoPackages(pm.Packages)
	return index, nil
}

// Package looks for the Package with the given name, returning a structure
// able to perform further operations on that given resource
func (pm *PackageManager) Package(name string) *PackageActions {
	//TODO: perhaps these 2 structure should be merged? cores.Packages vs pkgmgr??
	var err error
	thePackage := pm.Packages[name]
	if thePackage == nil {
		err = fmt.Errorf("package '%s' not found", name)
	}
	return &PackageActions{
		aPackage:     thePackage,
		forwardError: err,
	}
}

// Actions that can be done on a Package

// PackageActions defines what actions can be performed on the specific Package
// It serves as a status container for the fluent APIs
type PackageActions struct {
	aPackage     *cores.Package
	forwardError error
}

// Tool looks for the Tool with the given name, returning a structure
// able to perform further operations on that given resource
func (pa *PackageActions) Tool(name string) *ToolActions {
	var tool *cores.Tool
	err := pa.forwardError
	if err == nil {
		tool = pa.aPackage.Tools[name]

		if tool == nil {
			err = fmt.Errorf("tool '%s' not found in package '%s'", name, pa.aPackage.Name)
		}
	}
	return &ToolActions{
		tool:         tool,
		forwardError: err,
	}
}

// END -- Actions that can be done on a Package

// Actions that can be done on a Tool

// ToolActions defines what actions can be performed on the specific Tool
// It serves as a status container for the fluent APIs
type ToolActions struct {
	tool         *cores.Tool
	forwardError error
}

// Get returns the final representation of the Tool
func (ta *ToolActions) Get() (*cores.Tool, error) {
	err := ta.forwardError
	if err == nil {
		return ta.tool, nil
	}
	return nil, err
}

// IsInstalled checks whether any release of the Tool is installed in the system
func (ta *ToolActions) IsInstalled() (bool, error) {
	if ta.forwardError != nil {
		return false, ta.forwardError
	}

	for _, release := range ta.tool.Releases {
		if release.IsInstalled() {
			return true, nil
		}
	}
	return false, nil
}

// Release FIXMEDOC
func (ta *ToolActions) Release(version *semver.RelaxedVersion) *ToolReleaseActions {
	if ta.forwardError != nil {
		return &ToolReleaseActions{forwardError: ta.forwardError}
	}
	release := ta.tool.FindReleaseWithRelaxedVersion(version)
	if release == nil {
		return &ToolReleaseActions{forwardError: fmt.Errorf("release %s not found for tool %s", version, ta.tool.String())}
	}
	return &ToolReleaseActions{release: release}
}

// END -- Actions that can be done on a Tool

// ToolReleaseActions defines what actions can be performed on the specific ToolRelease
// It serves as a status container for the fluent APIs
type ToolReleaseActions struct {
	release      *cores.ToolRelease
	forwardError error
}

// Get FIXMEDOC
func (tr *ToolReleaseActions) Get() (*cores.ToolRelease, error) {
	if tr.forwardError != nil {
		return nil, tr.forwardError
	}
	return tr.release, nil
}

// GetInstalledPlatformRelease returns the PlatformRelease installed (it is chosen)
func (pm *PackageManager) GetInstalledPlatformRelease(platform *cores.Platform) *cores.PlatformRelease {
	releases := platform.GetAllInstalled()
	if len(releases) == 0 {
		return nil
	}

	log := func(msg string, pl *cores.PlatformRelease) {
		pm.Log.WithField("bundle", pl.IsIDEBundled).
			WithField("version", pl.Version).
			WithField("managed", pm.IsManagedPlatformRelease(pl)).
			Infof("%s: %s", msg, pl)
	}

	best := releases[0]
	bestIsManaged := pm.IsManagedPlatformRelease(best)
	log("current best", best)

	for _, candidate := range releases[1:] {
		candidateIsManaged := pm.IsManagedPlatformRelease(candidate)
		log("candidate", candidate)
		// TODO: Disentangle this algorithm and make it more straightforward
		if bestIsManaged == candidateIsManaged {
			if best.IsIDEBundled == candidate.IsIDEBundled {
				if candidate.Version.GreaterThan(best.Version) {
					best = candidate
				}
			}
			if best.IsIDEBundled && !candidate.IsIDEBundled {
				best = candidate
			}
		}
		if !bestIsManaged && candidateIsManaged {
			best = candidate
			bestIsManaged = true
		}
		log("current best", best)
	}
	return best
}

// GetAllInstalledToolsReleases FIXMEDOC
func (pm *PackageManager) GetAllInstalledToolsReleases() []*cores.ToolRelease {
	tools := []*cores.ToolRelease{}
	for _, targetPackage := range pm.Packages {
		for _, tool := range targetPackage.Tools {
			for _, release := range tool.Releases {
				if release.IsInstalled() {
					tools = append(tools, release)
				}
			}
		}
	}
	return tools
}

// InstalledPlatformReleases returns all installed PlatformReleases. This function is
// useful to range all PlatformReleases in for loops.
func (pm *PackageManager) InstalledPlatformReleases() []*cores.PlatformRelease {
	platforms := []*cores.PlatformRelease{}
	for _, targetPackage := range pm.Packages {
		for _, platform := range targetPackage.Platforms {
			for _, release := range platform.GetAllInstalled() {
				platforms = append(platforms, release)
			}
		}
	}
	return platforms
}

// InstalledBoards returns all installed Boards. This function is useful to range
// all Boards in for loops.
func (pm *PackageManager) InstalledBoards() []*cores.Board {
	boards := []*cores.Board{}
	for _, targetPackage := range pm.Packages {
		for _, platform := range targetPackage.Platforms {
			for _, release := range platform.GetAllInstalled() {
				for _, board := range release.Boards {
					boards = append(boards, board)
				}
			}
		}
	}
	return boards
}

// FindToolsRequiredForBoard FIXMEDOC
func (pm *PackageManager) FindToolsRequiredForBoard(board *cores.Board) ([]*cores.ToolRelease, error) {
	pm.Log.Infof("Searching tools required for board %s", board)

	// core := board.Properties["build.core"]
	platform := board.PlatformRelease

	// maps "PACKAGER:TOOL" => ToolRelease
	foundTools := map[string]*cores.ToolRelease{}

	// a Platform may not specify required tools (because it's a platform that comes from a
	// user/hardware dir without a package_index.json) then add all available tools
	for _, targetPackage := range pm.Packages {
		for _, tool := range targetPackage.Tools {
			rel := tool.GetLatestInstalled()
			if rel != nil {
				foundTools[rel.Tool.Name] = rel
			}
		}
	}

	// replace the default tools above with the specific required by the current platform
	requiredTools := []*cores.ToolRelease{}
	platform.Dependencies.Sort()
	for _, toolDep := range platform.Dependencies {
		pm.Log.WithField("tool", toolDep).Infof("Required tool")
		tool := pm.FindToolDependency(toolDep)
		if tool == nil {
			return nil, fmt.Errorf("tool release not found: %s", toolDep)
		}
		requiredTools = append(requiredTools, tool)
		delete(foundTools, tool.Tool.Name)
	}

	for _, toolRel := range foundTools {
		requiredTools = append(requiredTools, toolRel)
	}
	return requiredTools, nil
}

// FindToolDependency returns the ToolRelease referenced by the ToolDependency or nil if
// the referenced tool doesn't exists.
func (pm *PackageManager) FindToolDependency(dep *cores.ToolDependency) *cores.ToolRelease {
	toolRelease, err := pm.Package(dep.ToolPackager).Tool(dep.ToolName).Release(dep.ToolVersion).Get()
	if err != nil {
		return nil
	}
	return toolRelease
}
