package deb

import (
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
)

// PackageLike is something like Package :) To be refined later
type PackageLike interface {
	GetField(string) string
	MatchesDependency(Dependency) bool
	MatchesArchitecture(string) bool
	GetName() string
	GetVersion() string
	GetArchitecture() string
}

// PackageCatalog is abstraction on top of PackageCollection and PackageList
type PackageCatalog interface {
	Scan(q PackageQuery) (result *PackageList)
	Search(dep Dependency, allMatches bool) (searchResults []*Package)
	SearchSupported() bool
	SearchByKey(arch, name, version string) (result *PackageList)
}

// PackageQuery is interface of predicate on Package
type PackageQuery interface {
	// Matches calculates match of condition against package
	Matches(pkg PackageLike) bool
	// Fast returns if search strategy is possible for this query
	Fast(list PackageCatalog) bool
	// Query performs search on package list
	Query(list PackageCatalog) *PackageList
	// String interface
	String() string
}

// OrQuery is L | R
type OrQuery struct {
	L, R PackageQuery
}

// AndQuery is L , R
type AndQuery struct {
	L, R PackageQuery
}

// NotQuery is ! Q
type NotQuery struct {
	Q PackageQuery
}

// FieldQuery is generic request against field
type FieldQuery struct {
	Field    string
	Relation int
	Value    string
	Regexp   *regexp.Regexp `codec:"-"`
}

// PkgQuery is search request against specific package
type PkgQuery struct {
	Pkg     string
	Version string
	Arch    string
}

// DependencyQuery is generic Debian-dependency like query
type DependencyQuery struct {
	Dep Dependency
}

// Matches if any of L, R matches
func (q *OrQuery) Matches(pkg PackageLike) bool {
	return q.L.Matches(pkg) || q.R.Matches(pkg)
}

// Fast is true only if both parts are fast
func (q *OrQuery) Fast(list PackageCatalog) bool {
	return q.L.Fast(list) && q.R.Fast(list)
}

// Query strategy depends on nodes
func (q *OrQuery) Query(list PackageCatalog) (result *PackageList) {
	if q.Fast(list) {
		result = q.L.Query(list)
		result.Append(q.R.Query(list))
	} else {
		result = list.Scan(q)
	}
	return
}

// String interface
func (q *OrQuery) String() string {
	return fmt.Sprintf("(%s) | (%s)", q.L, q.R)
}

// Matches if both of L, R matches
func (q *AndQuery) Matches(pkg PackageLike) bool {
	return q.L.Matches(pkg) && q.R.Matches(pkg)
}

// Fast is true if any of the parts are fast
func (q *AndQuery) Fast(list PackageCatalog) bool {
	return q.L.Fast(list) || q.R.Fast(list)
}

// Query strategy depends on nodes
func (q *AndQuery) Query(list PackageCatalog) (result *PackageList) {
	if !q.Fast(list) {
		result = list.Scan(q)
	} else {
		if q.L.Fast(list) {
			result = q.L.Query(list)
			result = result.Scan(q.R)
		} else {
			result = q.R.Query(list)
			result = result.Scan(q.L)
		}
	}
	return
}

// String interface
func (q *AndQuery) String() string {
	return fmt.Sprintf("(%s), (%s)", q.L, q.R)
}

// Matches if not matches
func (q *NotQuery) Matches(pkg PackageLike) bool {
	return !q.Q.Matches(pkg)
}

// Fast is false
func (q *NotQuery) Fast(list PackageCatalog) bool {
	return false
}

// Query strategy is scan always
func (q *NotQuery) Query(list PackageCatalog) (result *PackageList) {
	result = list.Scan(q)
	return
}

// String interface
func (q *NotQuery) String() string {
	return fmt.Sprintf("!(%s)", q.Q)
}

// Matches on generic field
func (q *FieldQuery) Matches(pkg PackageLike) bool {
	if q.Field == "$Version" {
		return pkg.MatchesDependency(Dependency{Pkg: pkg.GetName(), Relation: q.Relation, Version: q.Value, Regexp: q.Regexp})
	}
	if q.Field == "$Architecture" && q.Relation == VersionEqual {
		return pkg.MatchesArchitecture(q.Value)
	}

	field := pkg.GetField(q.Field)

	switch q.Relation {
	case VersionDontCare:
		return field != ""
	case VersionEqual:
		return field == q.Value
	case VersionGreater:
		return field > q.Value
	case VersionGreaterOrEqual:
		return field >= q.Value
	case VersionLess:
		return field < q.Value
	case VersionLessOrEqual:
		return field <= q.Value
	case VersionPatternMatch:
		matched, err := filepath.Match(q.Value, field)
		return err == nil && matched
	case VersionRegexp:
		if q.Regexp == nil {
			q.Regexp = regexp.MustCompile(q.Value)
		}
		return q.Regexp.FindStringIndex(field) != nil

	}
	panic("unknown relation")
}

// Query runs iteration through list
func (q *FieldQuery) Query(list PackageCatalog) (result *PackageList) {
	result = list.Scan(q)
	return
}

// Fast depends on the query
func (q *FieldQuery) Fast(list PackageCatalog) bool {
	return false
}

// String interface
func (q *FieldQuery) String() string {
	escape := func(val string) string {
		if strings.IndexAny(val, "()|,!{} \t\n") != -1 {
			return "'" + strings.Replace(strings.Replace(val, "\\", "\\\\", -1), "'", "\\'", -1) + "'"
		}
		return val
	}

	var op string
	switch q.Relation {
	case VersionEqual:
		op = "="
	case VersionGreater:
		op = ">>"
	case VersionLess:
		op = "<<"
	case VersionRegexp:
		op = "~"
	case VersionPatternMatch:
		op = "%"
	case VersionGreaterOrEqual:
		op = ">="
	case VersionLessOrEqual:
		op = "<="
	}
	return fmt.Sprintf("%s (%s %s)", escape(q.Field), op, escape(q.Value))
}

// Matches on dependency condition
func (q *DependencyQuery) Matches(pkg PackageLike) bool {
	return pkg.MatchesDependency(q.Dep)
}

// Fast is always true for dependency query
func (q *DependencyQuery) Fast(list PackageCatalog) bool {
	return list.SearchSupported()
}

// Query runs PackageList.Search
func (q *DependencyQuery) Query(list PackageCatalog) (result *PackageList) {
	if q.Fast(list) {
		result = NewPackageList()
		for _, pkg := range list.Search(q.Dep, true) {
			result.Add(pkg)
		}
	} else {
		result = list.Scan(q)
	}

	return
}

// String interface
func (q *DependencyQuery) String() string {
	return q.Dep.String()
}

// Matches on specific properties
func (q *PkgQuery) Matches(pkg PackageLike) bool {
	return pkg.GetName() == q.Pkg && pkg.GetVersion() == q.Version && pkg.GetArchitecture() == q.Arch
}

// Fast is always true for package query
func (q *PkgQuery) Fast(list PackageCatalog) bool {
	return true
}

// Query looks up specific package
func (q *PkgQuery) Query(list PackageCatalog) (result *PackageList) {
	return list.SearchByKey(q.Arch, q.Pkg, q.Version)
}

// String interface
func (q *PkgQuery) String() string {
	return fmt.Sprintf("%s_%s_%s", q.Pkg, q.Version, q.Arch)
}
