// Copyright 2017 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 gps

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
)

// fsLink represents a symbolic link.
type fsLink struct {
	path string
	to   string

	// circular denotes if evaluating the symlink fails with "too many links" error.
	// This error means that it's very likely that the symlink has a circular reference.
	circular bool

	// broken denotes that attempting to resolve the link fails, most likely because
	// the destaination doesn't exist.
	broken bool
}

// filesystemState represents the state of a file system.
type filesystemState struct {
	root  string
	dirs  []string
	files []string
	links []fsLink
}

func (s filesystemState) setup() error {
	for _, dir := range s.dirs {
		p := filepath.Join(s.root, dir)

		if err := os.MkdirAll(p, 0777); err != nil {
			return errors.Errorf("os.MkdirAll(%q, 0777) err=%q", p, err)
		}
	}

	for _, file := range s.files {
		p := filepath.Join(s.root, file)

		f, err := os.Create(p)
		if err != nil {
			return errors.Errorf("os.Create(%q) err=%q", p, err)
		}

		if err := f.Close(); err != nil {
			return errors.Errorf("file %q Close() err=%q", p, err)
		}
	}

	for _, link := range s.links {
		p := filepath.Join(s.root, link.path)

		// On Windows, relative symlinks confuse filepath.Walk. So, we'll just sigh
		// and do absolute links, assuming they are relative to the directory of
		// link.path.
		//
		// Reference: https://github.com/golang/go/issues/17540
		//
		// TODO(ibrasho): This was fixed in Go 1.9. Remove this when support for
		// 1.8 is dropped.
		dir := filepath.Dir(p)
		to := ""
		if link.to != "" {
			to = filepath.Join(dir, link.to)
		}

		if err := os.Symlink(to, p); err != nil {
			return errors.Errorf("os.Symlink(%q, %q) err=%q", to, p, err)
		}
	}

	return nil
}

// deriveFilesystemState returns a filesystemState based on the state of
// the filesystem on root.
func deriveFilesystemState(root string) (filesystemState, error) {
	fs := filesystemState{root: root}

	err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if path == fs.root {
			return nil
		}

		relPath, err := filepath.Rel(fs.root, path)
		if err != nil {
			return err
		}

		if (info.Mode() & os.ModeSymlink) != 0 {
			l := fsLink{path: relPath}

			l.to, err = filepath.EvalSymlinks(path)
			if err != nil && strings.HasSuffix(err.Error(), "too many links") {
				l.circular = true
			} else if err != nil && os.IsNotExist(err) {
				l.broken = true
			} else if err != nil {
				return err
			}

			fs.links = append(fs.links, l)

			return nil
		}

		if info.IsDir() {
			fs.dirs = append(fs.dirs, relPath)

			return nil
		}

		fs.files = append(fs.files, relPath)

		return nil
	})

	if err != nil {
		return filesystemState{}, err
	}

	return fs, nil
}
