package sboRepo

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"gitlab.com/M0M097/pkg/lib/utils"
)

// Package represents a Slackware package. Based on a path to the package
// directory, it can query the package's .info (and other) file for metadata.
// It also provides methods to create a doubly linked list of packages.
type Package struct {
	name, path string     // path/name = full path
	prev, next *Package   // doubly linked list for processes list
	dependees  []*Package // packages that depend on this package
}

// NewPackage returns a new Package instance with the given name and path.
// path should be the parent directory of the package directory, so that
// path/name/name.info is the full path to the package's .info file.
func NewPackage(name, path string) Package { return Package{name: name, path: path} }

// Dependencies returns a slice of dependencies for the package as defined in
// the REQUIRES field of the package's .info file.
func (pkg *Package) Dependencies() []string { return strings.Fields(pkg.queryInfoFile("REQUIRES")) }

// InfoFile returns the path to the package's .info file.
func (pkg *Package) InfoFile() string { return filepath.Join(pkg.Path(), pkg.Name()+".info") }

// Name returns the name of the package.
func (pkg *Package) Name() string { return pkg.name }

// Path returns the full path to the package directory.
func (pkg *Package) Path() string { return filepath.Join(pkg.path, pkg.Name()) }

// Readme returns the full path to the README file in the package directory.
func (pkg *Package) Readme() string { return filepath.Join(pkg.Path(), "README") }

// SlackBuild returns the full path to the SlackBuild script for the package.
func (pkg *Package) SlackBuild() string { return filepath.Join(pkg.Path(), pkg.Name()+".SlackBuild") }

// Version returns the version of the package as defined in the VERSION field
// of the package's .info file.
func (pkg *Package) Version() string { return pkg.queryInfoFile("VERSION") }

// SlackBuildVersion returns the full name of the package, which follows the
// convention of "name-version".
func (pkg *Package) FullName() string { return pkg.Name() + "-" + pkg.Version() }

// SlackDesc returns the full path to the slack-desc file in the package.
func (pkg *Package) SlackDesc() string { return filepath.Join(pkg.Path(), "slack-desc") }

// Dependees returns a slice of packages previously added by AddDependee.
// It can be used by functions which build a dependency graph.
func (pkg *Package) Dependees() []*Package { return pkg.dependees }

// IsReadmeRequired returns true if the package requires the README file to be read.
func (pkg *Package) IsReadmeRequired() bool {
	matched, err := regexp.MatchString("%README%", pkg.queryInfoFile("REQUIRES"))
	return matched && err == nil
}

// AllReadmes returns a space separted list of the full paths to all README
// files in the package directory as a string.
func (pkg *Package) AllReadmes() string {
	sb := strings.Builder{}
	re := regexp.MustCompile("README")
	path := pkg.Path()

	for _, file := range utils.ReadDir(path) {
		fn := file.Name()
		if re.MatchString(fn) {
			sb.WriteString(filepath.Join(path, fn) + " ")
		}
	}

	return sb.String()[:sb.Len()-1]
}

// DownloadUrls returns a slice of download URLs for the package. It first
// checks for the x86_64 specific download URL and falls back to the generic
// download URL if the x86_64 specific one is not found.
func (pkg *Package) DownloadUrls() []string {
	download_x86_64 := pkg.queryInfoFile("DOWNLOAD_x86_64")
	if download_x86_64 != "" {
		return strings.Fields(download_x86_64)
	}
	return strings.Fields(pkg.queryInfoFile("DOWNLOAD"))
}

// Md5sums returns a slice of MD5 checksums for the package. It first checks for
// the x86_64 specific MD5 checksum and falls back to the generic MD5 checksum
// if the x86_64 specific one is not found.
func (pkg *Package) Md5sums() []string {
	md5sum_x86_64 := pkg.queryInfoFile("MD5SUM_x86_64")
	if md5sum_x86_64 != "" {
		return strings.Fields(md5sum_x86_64)
	}
	return strings.Fields(pkg.queryInfoFile("MD5SUM"))
}

// DownloadNames returns a slice of download file names for the package. It
// extracts the base name from each download URL returned by DownloadUrls.
func (pkg *Package) DownloadNames() []string {
	urls := pkg.DownloadUrls()
	names := make([]string, len(urls))
	for i, url := range urls {
		names[i] = filepath.Base(url)
	}
	return names
}

// IsUpToDate checks if the package is up to date by comparing the SlackBuild
// script in the package directory with the one in the documentation directory.
func (pkg *Package) IsUpToDate(docDir, kernelVersion string) bool {
	fullPath := filepath.Join(docDir, pkg.FullName())

	if !utils.Exists(fullPath) {
		if fullPath += "_" + kernelVersion; !utils.Exists(fullPath) {
			return false
		}
	}

	return bytes.Equal(
		utils.ReadFile(pkg.SlackBuild()),
		utils.ReadFile(filepath.Join(fullPath, pkg.Name()+".SlackBuild")),
	)
}

// AddDependee adds a package that depends on this package to the list of
// dependees. This is used to build a dependency graph.
func (pkg *Package) AddDependee(dependee *Package) {
	pkg.dependees = append(pkg.dependees, dependee)
}

func (pkg *Package) queryInfoFile(key string) string {
	file := utils.Open(pkg.InfoFile())
	defer file.Close()

	for scanner := bufio.NewScanner(file); scanner.Scan(); {

		if s := strings.Split(scanner.Text(), "\""); s[0] == key+"=" {
			for strings.HasSuffix(s[1], "\\") { // line continuation
				scanner.Scan()
				s[1] = s[1][:len(s[1])-1] + strings.Trim(scanner.Text(), "\"")
			}
			return s[1]
		}

	}

	panic(pkg.Name() + ": " + key + " not found in " + pkg.InfoFile())
}

// FindBuildPkg searches for the latest build package in the given temporary
// directory that matches the package's full name and returns its full path.
func (pkg *Package) FindBuildPkg(tmpDir string) string {
	var (
		re               = regexp.MustCompile(pkg.FullName() + ".*_SBo.*")
		modificationTime time.Time
		pkgName          string
	)
	for _, slackpkg := range utils.ReadDir(tmpDir) {
		if re.MatchString(slackpkg.Name()) {
			if t := utils.ModTime(slackpkg); t.After(modificationTime) {
				modificationTime = t
				pkgName = slackpkg.Name()
			}
		}
	}
	if pkgName == "" {
		panic(pkg.Name() + ": No package to install")
	}
	return filepath.Join(tmpDir, pkgName)
}

// Description returns the name plus the description of the package as it is
// found in the first line of the slack-desc file.
func (pkg *Package) Description() string {
	file := utils.Open(pkg.SlackDesc())
	defer file.Close()

	prefix := pkg.Name() + ": "
	lenPrefix := len(prefix)

	for scanner := bufio.NewScanner(file); scanner.Scan(); {
		if line := scanner.Text(); strings.HasPrefix(line, prefix) {
			return line[lenPrefix:]
		}
	}

	// We fallback to the name if for some reason the slack-desc file is
	// ill-formatted.
	return pkg.Name()
}

// IterateDependees iterates over the dependees (previously set by AddDependee)
// of the package and their dependees recursively and applies the function `f`
// to each dependee. This is useful for traversing the dependency graph.
func (pkg *Package) IterateDependees(f func(*Package)) {
	for _, dependee := range pkg.Dependees() {
		f(dependee)
		dependee.IterateDependees(f)
	}
}

// Write writes the package name to the given io.Writer.
func (pkg *Package) Write(w io.Writer) { fmt.Fprintln(w, pkg.Name()) }

// List related functions

// IsInList returns true if the package is in a doubly linked list.
func (pkg *Package) IsInList() bool { return pkg.prev != nil }

// Appends the package to the doubly linked list. Takes the tail of the list as
// an argument and returns the new tail.
func (pkg *Package) Append(tail *Package) *Package {
	tail.next, pkg.prev = pkg, tail
	return pkg // new tail
}

// Del delets the package from the doubly linked list. Works also for the head
// or tail of the list.
func (pkg *Package) Del() {
	if pkg.prev != nil {
		pkg.prev.next = pkg.next
	}
	if pkg.next != nil {
		pkg.next.prev = pkg.prev
	}
	pkg.prev = nil
}
