package fsutil

import (
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	strings "strings"

	"github.com/pkg/errors"
)

func FollowLinks(root string, paths []string) ([]string, error) {
	r := &symlinkResolver{root: root, resolved: map[string]struct{}{}}
	for _, p := range paths {
		if err := r.append(p); err != nil {
			return nil, err
		}
	}
	res := make([]string, 0, len(r.resolved))
	for r := range r.resolved {
		res = append(res, r)
	}
	sort.Strings(res)
	return dedupePaths(res), nil
}

type symlinkResolver struct {
	root     string
	resolved map[string]struct{}
}

func (r *symlinkResolver) append(p string) error {
	p = filepath.Join(".", p)
	current := "."
	for {
		parts := strings.SplitN(p, string(filepath.Separator), 2)
		current = filepath.Join(current, parts[0])

		targets, err := r.readSymlink(current, true)
		if err != nil {
			return err
		}

		p = ""
		if len(parts) == 2 {
			p = parts[1]
		}

		if p == "" || targets != nil {
			if _, ok := r.resolved[current]; ok {
				return nil
			}
		}

		if targets != nil {
			r.resolved[current] = struct{}{}
			for _, target := range targets {
				if err := r.append(filepath.Join(target, p)); err != nil {
					return err
				}
			}
			return nil
		}

		if p == "" {
			r.resolved[current] = struct{}{}
			return nil
		}
	}
}

func (r *symlinkResolver) readSymlink(p string, allowWildcard bool) ([]string, error) {
	realPath := filepath.Join(r.root, p)
	base := filepath.Base(p)
	if allowWildcard && containsWildcards(base) {
		fis, err := ioutil.ReadDir(filepath.Dir(realPath))
		if err != nil {
			if os.IsNotExist(err) {
				return nil, nil
			}
			return nil, errors.Wrapf(err, "failed to read dir %s", filepath.Dir(realPath))
		}
		var out []string
		for _, f := range fis {
			if ok, _ := filepath.Match(base, f.Name()); ok {
				res, err := r.readSymlink(filepath.Join(filepath.Dir(p), f.Name()), false)
				if err != nil {
					return nil, err
				}
				out = append(out, res...)
			}
		}
		return out, nil
	}

	fi, err := os.Lstat(realPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, errors.Wrapf(err, "failed to lstat %s", realPath)
	}
	if fi.Mode()&os.ModeSymlink == 0 {
		return nil, nil
	}
	link, err := os.Readlink(realPath)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to readlink %s", realPath)
	}
	link = filepath.Clean(link)
	if filepath.IsAbs(link) {
		return []string{link}, nil
	}
	return []string{
		filepath.Join(string(filepath.Separator), filepath.Join(filepath.Dir(p), link)),
	}, nil
}

func containsWildcards(name string) bool {
	isWindows := runtime.GOOS == "windows"
	for i := 0; i < len(name); i++ {
		ch := name[i]
		if ch == '\\' && !isWindows {
			i++
		} else if ch == '*' || ch == '?' || ch == '[' {
			return true
		}
	}
	return false
}

// dedupePaths expects input as a sorted list
func dedupePaths(in []string) []string {
	out := make([]string, 0, len(in))
	var last string
	for _, s := range in {
		// if one of the paths is root there is no filter
		if s == "." {
			return nil
		}
		if strings.HasPrefix(s, last+string(filepath.Separator)) {
			continue
		}
		out = append(out, s)
		last = s
	}
	return out
}