package lfs

import (
	"fmt"
	"strings"

	"github.com/git-lfs/git-lfs/git"
)

// Attribute wraps the structure and some operations of Git's conception of an
// "attribute", as defined here: http://git-scm.com/docs/gitattributes.
type Attribute struct {
	// The Section of an Attribute refers to the location at which all
	// properties are relative to. For example, for a Section with the value
	// "core", Git will produce something like:
	//
	// [core]
	//	autocrlf = true
	//	...
	Section string

	// The Properties of an Attribute refer to all of the keys and values
	// that define that Attribute.
	Properties map[string]string
	// Previous values of these attributes that can be automatically upgraded
	Upgradeables map[string][]string
}

// FilterOptions serves as an argument to Install().
type FilterOptions struct {
	GitConfig  *git.Configuration
	Force      bool
	Local      bool
	Worktree   bool
	System     bool
	SkipSmudge bool
}

func (o *FilterOptions) Install() error {
	if o.SkipSmudge {
		return skipSmudgeFilterAttribute().Install(o)
	}
	return filterAttribute().Install(o)
}

func (o *FilterOptions) Uninstall() error {
	return filterAttribute().Uninstall(o)
}

func filterAttribute() *Attribute {
	return &Attribute{
		Section: "filter.lfs",
		Properties: map[string]string{
			"clean":    "git-lfs clean -- %f",
			"smudge":   "git-lfs smudge -- %f",
			"process":  "git-lfs filter-process",
			"required": "true",
		},
		Upgradeables: map[string][]string{
			"clean": []string{
				"git-lfs clean %f",
			},
			"smudge": []string{
				"git-lfs smudge %f",
				"git-lfs smudge --skip %f",
				"git-lfs smudge --skip -- %f",
			},
			"process": []string{
				"git-lfs filter",
				"git-lfs filter --skip",
				"git-lfs filter-process --skip",
			},
		},
	}
}

func skipSmudgeFilterAttribute() *Attribute {
	return &Attribute{
		Section: "filter.lfs",
		Properties: map[string]string{
			"clean":    "git-lfs clean -- %f",
			"smudge":   "git-lfs smudge --skip -- %f",
			"process":  "git-lfs filter-process --skip",
			"required": "true",
		},
		Upgradeables: map[string][]string{
			"clean": []string{
				"git-lfs clean -- %f",
			},
			"smudge": []string{
				"git-lfs smudge %f",
				"git-lfs smudge --skip %f",
				"git-lfs smudge -- %f",
			},
			"process": []string{
				"git-lfs filter",
				"git-lfs filter --skip",
				"git-lfs filter-process",
			},
		},
	}
}

// Install instructs Git to set all keys and values relative to the root
// location of this Attribute. For any particular key/value pair, if a matching
// key is already set, it will be overridden if it is either a) empty, or b) the
// `force` argument is passed as true. If an attribute is already set to a
// different value than what is given, and force is false, an error will be
// returned immediately, and the rest of the attributes will not be set.
func (a *Attribute) Install(opt *FilterOptions) error {
	for k, v := range a.Properties {
		var upgradeables []string
		if a.Upgradeables != nil {
			// use pre-normalised key since caller will have set up the same
			upgradeables = a.Upgradeables[k]
		}
		key := a.normalizeKey(k)
		if err := a.set(opt.GitConfig, key, v, upgradeables, opt); err != nil {
			return err
		}
	}

	return nil
}

// normalizeKey makes an absolute path out of a partial relative one. For a
// relative path of "foo", and a root Section of "bar", "bar.foo" will be returned.
func (a *Attribute) normalizeKey(relative string) string {
	return strings.Join([]string{a.Section, relative}, ".")
}

// set attempts to set a single key/value pair portion of this Attribute. If a
// matching key already exists and the value is not equal to the desired value,
// an error will be thrown if force is set to false. If force is true, the value
// will be overridden.
func (a *Attribute) set(gitConfig *git.Configuration, key, value string, upgradeables []string, opt *FilterOptions) error {
	var currentValue string
	if opt.Local {
		currentValue = gitConfig.FindLocal(key)
	} else if opt.Worktree {
		currentValue = gitConfig.FindWorktree(key)
	} else if opt.System {
		currentValue = gitConfig.FindSystem(key)
	} else {
		currentValue = gitConfig.FindGlobal(key)
	}

	if opt.Force || shouldReset(currentValue, upgradeables) {
		var err error
		if opt.Local {
			_, err = gitConfig.SetLocal(key, value)
		} else if opt.Worktree {
			_, err = gitConfig.SetWorktree(key, value)
		} else if opt.System {
			_, err = gitConfig.SetSystem(key, value)
		} else {
			_, err = gitConfig.SetGlobal(key, value)
		}
		return err
	} else if currentValue != value {
		return fmt.Errorf("the %q attribute should be %q but is %q",
			key, value, currentValue)
	}

	return nil
}

// Uninstall removes all properties in the path of this property.
func (a *Attribute) Uninstall(opt *FilterOptions) error {
	var err error
	if opt.Local {
		_, err = opt.GitConfig.UnsetLocalSection(a.Section)
	} else if opt.Worktree {
		_, err = opt.GitConfig.UnsetWorktreeSection(a.Section)
	} else if opt.System {
		_, err = opt.GitConfig.UnsetSystemSection(a.Section)
	} else {
		_, err = opt.GitConfig.UnsetGlobalSection(a.Section)
	}
	return err
}

// shouldReset determines whether or not a value is resettable given its current
// value on the system. If the value is empty (length = 0), then it will pass.
// It will also pass if it matches any upgradeable value
func shouldReset(value string, upgradeables []string) bool {
	if len(value) == 0 {
		return true
	}

	for _, u := range upgradeables {
		if value == u {
			return true
		}
	}

	return false
}
