// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package glide

import (
	"bytes"
	"io/ioutil"
	"log"
	"os"
	"path"
	"path/filepath"

	"github.com/golang/dep"
	"github.com/golang/dep/gps"
	"github.com/golang/dep/internal/fs"
	"github.com/golang/dep/internal/importers/base"
	"github.com/pkg/errors"
	"gopkg.in/yaml.v2"
)

const glideYamlName = "glide.yaml"
const glideLockName = "glide.lock"

// Importer imports glide configuration into the dep configuration format.
type Importer struct {
	*base.Importer
	glideConfig glideYaml
	glideLock   glideLock
	lockFound   bool
}

// NewImporter for glide.
func NewImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *Importer {
	return &Importer{Importer: base.NewImporter(logger, verbose, sm)}
}

type glideYaml struct {
	Name        string         `yaml:"package"`
	Ignores     []string       `yaml:"ignore"`
	ExcludeDirs []string       `yaml:"excludeDirs"`
	Imports     []glidePackage `yaml:"import"`
	TestImports []glidePackage `yaml:"testImport"`
}

type glideLock struct {
	Imports     []glideLockedPackage `yaml:"imports"`
	TestImports []glideLockedPackage `yaml:"testImports"`
}

type glidePackage struct {
	Name       string `yaml:"package"`
	Reference  string `yaml:"version"` // could contain a semver, tag or branch
	Repository string `yaml:"repo"`

	// Unsupported fields that we will warn if used
	Subpackages []string `yaml:"subpackages"`
	OS          string   `yaml:"os"`
	Arch        string   `yaml:"arch"`
}

type glideLockedPackage struct {
	Name       string `yaml:"name"`
	Revision   string `yaml:"version"`
	Repository string `yaml:"repo"`
}

// Name of the importer.
func (g *Importer) Name() string {
	return "glide"
}

// HasDepMetadata checks if a directory contains config that the importer can handle.
func (g *Importer) HasDepMetadata(dir string) bool {
	// Only require glide.yaml, the lock is optional
	y := filepath.Join(dir, glideYamlName)
	if _, err := os.Stat(y); err != nil {
		return false
	}

	return true
}

// Import the config found in the directory.
func (g *Importer) Import(dir string, pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock, error) {
	err := g.load(dir)
	if err != nil {
		return nil, nil, err
	}

	m, l := g.convert(pr)
	return m, l, nil
}

// load the glide configuration files. Failure to load `glide.yaml` is considered
// unrecoverable and an error is returned for it. But if there is any error while trying
// to load the lock file, only a warning is logged.
func (g *Importer) load(projectDir string) error {
	g.Logger.Println("Detected glide configuration files...")
	y := filepath.Join(projectDir, glideYamlName)
	if g.Verbose {
		g.Logger.Printf("  Loading %s", y)
	}
	yb, err := ioutil.ReadFile(y)
	if err != nil {
		return errors.Wrapf(err, "unable to read %s", y)
	}
	err = yaml.Unmarshal(yb, &g.glideConfig)
	if err != nil {
		return errors.Wrapf(err, "unable to parse %s", y)
	}

	l := filepath.Join(projectDir, glideLockName)
	if exists, _ := fs.IsRegular(l); exists {
		if g.Verbose {
			g.Logger.Printf("  Loading %s", l)
		}
		lb, err := ioutil.ReadFile(l)
		if err != nil {
			g.Logger.Printf("  Warning: Ignoring lock file. Unable to read %s: %s\n", l, err)
			return nil
		}
		lock := glideLock{}
		err = yaml.Unmarshal(lb, &lock)
		if err != nil {
			g.Logger.Printf("  Warning: Ignoring lock file. Unable to parse %s: %s\n", l, err)
			return nil
		}
		g.lockFound = true
		g.glideLock = lock
	}

	return nil
}

// convert the glide configuration files into dep configuration files.
func (g *Importer) convert(pr gps.ProjectRoot) (*dep.Manifest, *dep.Lock) {
	projectName := string(pr)

	task := bytes.NewBufferString("Converting from glide.yaml")
	if g.lockFound {
		task.WriteString(" and glide.lock")
	}
	task.WriteString("...")
	g.Logger.Println(task)

	numPkgs := len(g.glideConfig.Imports) + len(g.glideConfig.TestImports) + len(g.glideLock.Imports) + len(g.glideLock.TestImports)
	packages := make([]base.ImportedPackage, 0, numPkgs)

	// Constraints
	for _, pkg := range append(g.glideConfig.Imports, g.glideConfig.TestImports...) {
		// Validate
		if pkg.Name == "" {
			g.Logger.Println(
				"  Warning: Skipping project. Invalid glide configuration, Name is required",
			)
			continue
		}

		// Warn
		if g.Verbose {
			if pkg.OS != "" {
				g.Logger.Printf("  The %s package specified an os, but that isn't supported by dep yet, and will be ignored. See https://github.com/golang/dep/issues/291.\n", pkg.Name)
			}
			if pkg.Arch != "" {
				g.Logger.Printf("  The %s package specified an arch, but that isn't supported by dep yet, and will be ignored. See https://github.com/golang/dep/issues/291.\n", pkg.Name)
			}
		}

		ip := base.ImportedPackage{
			Name:           pkg.Name,
			Source:         pkg.Repository,
			ConstraintHint: pkg.Reference,
		}
		packages = append(packages, ip)
	}

	// Locks
	for _, pkg := range append(g.glideLock.Imports, g.glideLock.TestImports...) {
		// Validate
		if pkg.Name == "" {
			g.Logger.Println("  Warning: Skipping project. Invalid glide lock, Name is required")
			continue
		}

		ip := base.ImportedPackage{
			Name:     pkg.Name,
			Source:   pkg.Repository,
			LockHint: pkg.Revision,
		}
		packages = append(packages, ip)
	}

	g.ImportPackages(packages, false)

	// Ignores
	g.Manifest.Ignored = append(g.Manifest.Ignored, g.glideConfig.Ignores...)
	if len(g.glideConfig.ExcludeDirs) > 0 {
		if g.glideConfig.Name != "" && g.glideConfig.Name != projectName {
			g.Logger.Printf("  Glide thinks the package is '%s' but dep thinks it is '%s', using dep's value.\n", g.glideConfig.Name, projectName)
		}

		for _, dir := range g.glideConfig.ExcludeDirs {
			pkg := path.Join(projectName, dir)
			g.Manifest.Ignored = append(g.Manifest.Ignored, pkg)
		}
	}

	return g.Manifest, g.Lock
}
