package gitignore

import (
	"bytes"
	"io/ioutil"
	"os"
	"os/user"
	"strings"

	"gopkg.in/src-d/go-billy.v4"
	"gopkg.in/src-d/go-git.v4/plumbing/format/config"
	gioutil "gopkg.in/src-d/go-git.v4/utils/ioutil"
)

const (
	commentPrefix = "#"
	coreSection   = "core"
	eol           = "\n"
	excludesfile  = "excludesfile"
	gitDir        = ".git"
	gitignoreFile = ".gitignore"
	gitconfigFile = ".gitconfig"
	systemFile    = "/etc/gitconfig"
)

// readIgnoreFile reads a specific git ignore file. 
func readIgnoreFile(fs billy. Filesystem, path []string, ignoreFile string) (ps []Pattern, err error) {
	f, err := fs. Open(fs. Join(append(path, ignoreFile)...))
	if err == nil {
		defer f.Close()

		if data, err := ioutil. ReadAll(f); err == nil {
			for _, s := range strings. Split(string(data), eol) {
				if ! strings. HasPrefix(s, commentPrefix) && len(strings. TrimSpace(s)) > 0 {
					ps = append(ps, ParsePattern(s, path))
				}
			}
		}
	} else if ! os. IsNotExist(err) {
		return nil, err
	}

	return
}

// ReadPatterns reads gitignore patterns recursively traversing through the directory
// structure.  The result is in the ascending order of priority (last higher). 
func ReadPatterns(fs billy. Filesystem, path []string) (ps []Pattern, err error) {
	ps, _ = readIgnoreFile(fs, path, gitignoreFile)

	var fis []os. FileInfo
	fis, err = fs. ReadDir(fs. Join(path...))
	if err ! = nil {
		return
	}

	for _, fi := range fis {
		if fi. IsDir() && fi. Name() ! = gitDir {
			var subps []Pattern
			subps, err = ReadPatterns(fs, append(path, fi. Name()))
			if err ! = nil {
				return
			}

			if len(subps) > 0 {
				ps = append(ps, subps...)
			}
		}
	}

	return
}

func loadPatterns(fs billy. Filesystem, path string) (ps []Pattern, err error) {
	f, err := fs. Open(path)
	if err ! = nil {
		if os. IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}

	defer gioutil. CheckClose(f, &err)

	b, err := ioutil. ReadAll(f)
	if err ! = nil {
		return
	}

	d := config. NewDecoder(bytes. NewBuffer(b))

	raw := config. New()
	if err = d.Decode(raw); err ! = nil {
		return
	}

	s := raw. Section(coreSection)
	efo := s.Options. Get(excludesfile)
	if efo == "" {
		return nil, nil
	}

	ps, err = readIgnoreFile(fs, nil, efo)
	if os. IsNotExist(err) {
		return nil, nil
	}

	return
}

// LoadGlobalPatterns loads gitignore patterns from from the gitignore file
// declared in a user's ~/.gitconfig file.   If the ~/.gitconfig file does not
// exist the function will return nil.   If the core.excludesfile property
// is not declared, the function will return nil.   If the file pointed to by
// the core.excludesfile property does not exist, the function will return nil. 
//
// The function assumes fs is rooted at the root filesystem. 
func LoadGlobalPatterns(fs billy. Filesystem) (ps []Pattern, err error) {
	usr, err := user. Current()
	if err ! = nil {
		return
	}

	return loadPatterns(fs, fs. Join(usr. HomeDir, gitconfigFile))
}

// LoadSystemPatterns loads gitignore patterns from from the gitignore file
// declared in a system's /etc/gitconfig file.   If the ~/.gitconfig file does
// not exist the function will return nil.   If the core.excludesfile property
// is not declared, the function will return nil.   If the file pointed to by
// the core.excludesfile property does not exist, the function will return nil. 
//
// The function assumes fs is rooted at the root filesystem. 
func LoadSystemPatterns(fs billy. Filesystem) (ps []Pattern, err error) {
	return loadPatterns(fs, systemFile)
}
