package rpm

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"syscall"
	"time"
)

// A Package is an rpm package file.
type Package struct {
	Lead      Lead
	Signature Header
	Header    Header
}

var _ Version = &Package{}

// Read reads an rpm package from r.
//
// When this function returns, the reader will be positioned at the start of the
// package payload. Use Package.PayloadFormat and Package.PayloadCompression to
// determine how to decompress and unarchive the payload.
func Read(r io.Reader) (*Package, error) {
	lead, err := readLead(r)
	if err != nil {
		return nil, err
	}
	sig, err := readHeader(r, true)
	if err != nil {
		return nil, err
	}
	hdr, err := readHeader(r, false)
	if err != nil {
		return nil, err
	}
	return &Package{
		Lead:      *lead,
		Signature: *sig,
		Header:    *hdr,
	}, nil
}

// Open opens an rpm package from the file system.
//
// Once the package headers are read, the underlying reader is closed and cannot
// be used to read the package payload. To read the package payload, open the
// package with os.Open and read the headers with Read. You may then use the
// same reader to read the payload.
func Open(name string) (*Package, error) {
	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return Read(bufio.NewReader(f))
}

// dependencies translates the given tag values into a slice of package
// relationships such as provides, conflicts, obsoletes and requires.
func (c *Package) dependencies(nevrsTagID, flagsTagID, namesTagID, versionsTagID int) []Dependency {
	// TODO: Implement NEVRS tags
	// TODO: error handling
	flgs := c.Header.GetTag(flagsTagID).Int64Slice()
	names := c.Header.GetTag(namesTagID).StringSlice()
	vers := c.Header.GetTag(versionsTagID).StringSlice()
	deps := make([]Dependency, len(names))
	for i := 0; i < len(names); i++ {
		epoch, ver, rel := parseVersion(vers[i])
		deps[i] = &dependency{
			flags:   int(flgs[i]),
			name:    names[i],
			epoch:   epoch,
			version: ver,
			release: rel,
		}
	}
	return deps
}

// String returns the package identifier in the form
// '[name]-[version]-[release].[architecture]'.
func (c *Package) String() string {
	return fmt.Sprintf("%s-%s-%s.%s", c.Name(), c.Version(), c.Release(), c.Architecture())
}

func (c *Package) GPGSignature() GPGSignature {
	return c.Signature.GetTag(1002).Bytes()
}

// For tag definitions, see:
// https://github.com/rpm-software-management/rpm/blob/master/include/rpm/rpmtag.h#L34

func (c *Package) Name() string {
	return c.Header.GetTag(1000).String()
}

func (c *Package) Version() string {
	return c.Header.GetTag(1001).String()
}

func (c *Package) Release() string {
	return c.Header.GetTag(1002).String()
}

func (c *Package) Epoch() int {
	return int(c.Header.GetTag(1003).Int64())
}

// HeaderRange returns the byte offsets of the RPM header.
func (c *Package) HeaderRange() (start, end int) {
	start = 96 + c.Signature.Size
	end = start + c.Header.Size
	return start, end
}

func (c *Package) Requires() []Dependency {
	return c.dependencies(5041, 1048, 1049, 1050)
}

func (c *Package) Provides() []Dependency {
	return c.dependencies(5042, 1112, 1047, 1113)
}

func (c *Package) Conflicts() []Dependency {
	return c.dependencies(5044, 1053, 1054, 1055)
}

func (c *Package) Obsoletes() []Dependency {
	return c.dependencies(5043, 1114, 1090, 1115)
}

func (c *Package) Suggests() []Dependency {
	return c.dependencies(5059, 5051, 5049, 5050)
}

func (c *Package) Enhances() []Dependency {
	return c.dependencies(5061, 5057, 5055, 5056)
}

func (c *Package) Recommends() []Dependency {
	return c.dependencies(5058, 5048, 5046, 5047)
}

func (c *Package) Supplements() []Dependency {
	return c.dependencies(5060, 5051, 5052, 5053)
}

// Files returns file information for each file that is installed by this RPM
// package.
func (c *Package) Files() []FileInfo {
	ixs := c.Header.GetTag(1116).Int64Slice()
	names := c.Header.GetTag(1117).StringSlice()
	dirs := c.Header.GetTag(1118).StringSlice()
	modes := c.Header.GetTag(1030).Int64Slice()
	sizes := c.Header.GetTag(1028).Int64Slice()
	times := c.Header.GetTag(1034).Int64Slice()
	flags := c.Header.GetTag(1037).Int64Slice()
	owners := c.Header.GetTag(1039).StringSlice()
	groups := c.Header.GetTag(1040).StringSlice()
	digests := c.Header.GetTag(1035).StringSlice()
	linknames := c.Header.GetTag(1036).StringSlice()
	a := make([]FileInfo, len(names))
	for i := 0; i < len(names); i++ {
		a[i] = FileInfo{
			name:     dirs[ixs[i]] + names[i],
			mode:     fileModeFromInt64(modes[i]),
			size:     sizes[i],
			modTime:  time.Unix(times[i], 0),
			flags:    flags[i],
			owner:    owners[i],
			group:    groups[i],
			digest:   digests[i],
			linkname: linknames[i],
		}
	}
	return a
}

// fileModeFromInt64 converts the 16 bit value returned from a typical
// unix/linux stat call to the bitmask that go uses to produce an os
// neutral representation.  It is incorrect to just cast the 16 bit
// value directly to a os.FileMode.  The result of stat is 4 bits to
// specify the type of the object, this is a value in the range 0 to
// 15, rather than a bitfield, 3 bits to note suid, sgid and sticky,
// and 3 sets of 3 bits for rwx permissions for user, group and other.
// An os.FileMode has the same 9 bits for permissions, but rather than
// using an enum for the type it has individual bits.  As a concrete
// example, a block device has the 1<<26 bit set (os.ModeDevice) in
// the os.FileMode, but has type 0x6000 (syscall.S_IFBLK). A regular
// file is represented in os.FileMode by not having any of the bits in
// os.ModeType set (i.e. is not a directory, is not a symlink, is not
// a named pipe...) whilst a regular file has value syscall.S_IFREG
// (0x8000) in the mode field from stat.
func fileModeFromInt64(mode int64) os.FileMode {
	fm := os.FileMode(mode & 0777)
	switch mode & syscall.S_IFMT {
	case syscall.S_IFBLK:
		fm |= os.ModeDevice
	case syscall.S_IFCHR:
		fm |= os.ModeDevice | os.ModeCharDevice
	case syscall.S_IFDIR:
		fm |= os.ModeDir
	case syscall.S_IFIFO:
		fm |= os.ModeNamedPipe
	case syscall.S_IFLNK:
		fm |= os.ModeSymlink
	case syscall.S_IFREG:
		// nothing to do
	case syscall.S_IFSOCK:
		fm |= os.ModeSocket
	}
	if mode&syscall.S_ISGID != 0 {
		fm |= os.ModeSetgid
	}
	if mode&syscall.S_ISUID != 0 {
		fm |= os.ModeSetuid
	}
	if mode&syscall.S_ISVTX != 0 {
		fm |= os.ModeSticky
	}
	return fm
}

func (c *Package) Summary() string {
	return strings.Join(c.Header.GetTag(1004).StringSlice(), "\n")
}

func (c *Package) Description() string {
	return strings.Join(c.Header.GetTag(1005).StringSlice(), "\n")
}

func (c *Package) BuildTime() time.Time {
	return time.Unix(c.Header.GetTag(1006).Int64(), 0)
}

func (c *Package) BuildHost() string {
	return c.Header.GetTag(1007).String()
}

func (c *Package) InstallTime() time.Time {
	return time.Unix(c.Header.GetTag(1008).Int64(), 0)
}

// Size specifies the disk space consumed by installation of the package.
func (c *Package) Size() uint64 {
	if i := uint64(c.Header.GetTag(5009).Int64()); i > 0 {
		return i
	}
	return uint64(c.Header.GetTag(1009).Int64())
}

// ArchiveSize specifies the size of the archived payload of the package in
// bytes.
func (c *Package) ArchiveSize() uint64 {
	if i := uint64(c.Signature.GetTag(271).Int64()); i > 0 {
		return i
	}
	if i := uint64(c.Signature.GetTag(1007).Int64()); i > 0 {
		return i
	}
	if i := uint64(c.Header.GetTag(271).Int64()); i > 0 {
		return i
	}
	return uint64(c.Header.GetTag(1046).Int64())
}

func (c *Package) Distribution() string {
	return c.Header.GetTag(1010).String()
}

func (c *Package) Vendor() string {
	return c.Header.GetTag(1011).String()
}

func (c *Package) GIFImage() []byte {
	return c.Header.GetTag(1012).Bytes()
}

func (c *Package) XPMImage() []byte {
	return c.Header.GetTag(1013).Bytes()
}

func (c *Package) License() string {
	return c.Header.GetTag(1014).String()
}

func (c *Package) Packager() string {
	return c.Header.GetTag(1015).String()
}

func (c *Package) Groups() []string {
	return c.Header.GetTag(1016).StringSlice()
}

func (c *Package) ChangeLog() []string {
	return c.Header.GetTag(1017).StringSlice()
}

func (c *Package) Source() []string {
	return c.Header.GetTag(1018).StringSlice()
}

func (c *Package) Patch() []string {
	return c.Header.GetTag(1019).StringSlice()
}

func (c *Package) URL() string {
	return c.Header.GetTag(1020).String()
}

func (c *Package) OperatingSystem() string {
	return c.Header.GetTag(1021).String()
}

func (c *Package) Architecture() string {
	return c.Header.GetTag(1022).String()
}

func (c *Package) PreInstallScript() string {
	return c.Header.GetTag(1023).String()
}

func (c *Package) PostInstallScript() string {
	return c.Header.GetTag(1024).String()
}

func (c *Package) PreUninstallScript() string {
	return c.Header.GetTag(1025).String()
}

func (c *Package) PostUninstallScript() string {
	return c.Header.GetTag(1026).String()
}

func (c *Package) OldFilenames() []string {
	return c.Header.GetTag(1027).StringSlice()
}

func (c *Package) Icon() []byte {
	return c.Header.GetTag(1043).Bytes()
}

func (c *Package) SourceRPM() string {
	return c.Header.GetTag(1044).String()
}

func (c *Package) RPMVersion() string {
	return c.Header.GetTag(1064).String()
}

func (c *Package) Platform() string {
	return c.Header.GetTag(1132).String()
}

// PayloadFormat returns the name of the format used for the package payload.
// Typically cpio.
func (c *Package) PayloadFormat() string {
	return c.Header.GetTag(1124).String()
}

// PayloadCompression returns the name of the compression used for the package
// payload. Typically xz.
func (c *Package) PayloadCompression() string {
	return c.Header.GetTag(1125).String()
}

// Sort sorts a slice of packages lexically by name ascending and then by
// version descending. Version is evaluated first by epoch, then by version
// string, then by release.
func Sort(x []*Package) { sort.Sort(PackageSlice(x)) }

// PackageSlice implements sort.Interface for a slice of packages. Packages are
// sorted lexically by name ascending and then by version descending. Version is
// evaluated first by epoch, then by version string, then by release.
type PackageSlice []*Package

// Sort is a convenience method: x.Sort() calls sort.Sort(x).
func (x PackageSlice) Sort() { sort.Sort(x) }

func (x PackageSlice) Len() int { return len(x) }

func (x PackageSlice) Less(i, j int) bool {
	a, b := x[i].Name(), x[j].Name()
	if a == b {
		return Compare(x[i], x[j]) == 1
	}
	return a < b
}

func (x PackageSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }

var _ sort.Interface = PackageSlice{}
