// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package procfs

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

// For the proc file format details,
// see https://elixir.bootlin.com/linux/v4.17/source/net/unix/af_unix.c#L2815
// and https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/net.h#L48.

const (
	netUnixKernelPtrIdx = iota
	netUnixRefCountIdx
	_
	netUnixFlagsIdx
	netUnixTypeIdx
	netUnixStateIdx
	netUnixInodeIdx

	// Inode and Path are optional.
	netUnixStaticFieldsCnt = 6
)

const (
	netUnixTypeStream    = 1
	netUnixTypeDgram     = 2
	netUnixTypeSeqpacket = 5

	netUnixFlagListen = 1 << 16

	netUnixStateUnconnected  = 1
	netUnixStateConnecting   = 2
	netUnixStateConnected    = 3
	netUnixStateDisconnected = 4
)

var errInvalidKernelPtrFmt = errors.New("Invalid Num(the kernel table slot number) format")

// NetUnixType is the type of the type field.
type NetUnixType uint64

// NetUnixFlags is the type of the flags field.
type NetUnixFlags uint64

// NetUnixState is the type of the state field.
type NetUnixState uint64

// NetUnixLine represents a line of /proc/net/unix.
type NetUnixLine struct {
	KernelPtr string
	RefCount  uint64
	Protocol  uint64
	Flags     NetUnixFlags
	Type      NetUnixType
	State     NetUnixState
	Inode     uint64
	Path      string
}

// NetUnix holds the data read from /proc/net/unix.
type NetUnix struct {
	Rows []*NetUnixLine
}

// NewNetUnix returns data read from /proc/net/unix.
func NewNetUnix() (*NetUnix, error) {
	fs, err := NewFS(DefaultMountPoint)
	if err != nil {
		return nil, err
	}

	return fs.NewNetUnix()
}

// NewNetUnix returns data read from /proc/net/unix.
func (fs FS) NewNetUnix() (*NetUnix, error) {
	return NewNetUnixByPath(fs.proc.Path("net/unix"))
}

// NewNetUnixByPath returns data read from /proc/net/unix by file path.
// It might returns an error with partial parsed data, if an error occur after some data parsed.
func NewNetUnixByPath(path string) (*NetUnix, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return NewNetUnixByReader(f)
}

// NewNetUnixByReader returns data read from /proc/net/unix by a reader.
// It might returns an error with partial parsed data, if an error occur after some data parsed.
func NewNetUnixByReader(reader io.Reader) (*NetUnix, error) {
	nu := &NetUnix{
		Rows: make([]*NetUnixLine, 0, 32),
	}
	scanner := bufio.NewScanner(reader)
	// Omit the header line.
	scanner.Scan()
	header := scanner.Text()
	// From the man page of proc(5), it does not contain an Inode field,
	// but in actually it exists.
	// This code works for both cases.
	hasInode := strings.Contains(header, "Inode")

	minFieldsCnt := netUnixStaticFieldsCnt
	if hasInode {
		minFieldsCnt++
	}
	for scanner.Scan() {
		line := scanner.Text()
		item, err := nu.parseLine(line, hasInode, minFieldsCnt)
		if err != nil {
			return nu, err
		}
		nu.Rows = append(nu.Rows, item)
	}

	return nu, scanner.Err()
}

func (u *NetUnix) parseLine(line string, hasInode bool, minFieldsCnt int) (*NetUnixLine, error) {
	fields := strings.Fields(line)
	fieldsLen := len(fields)
	if fieldsLen < minFieldsCnt {
		return nil, fmt.Errorf(
			"Parse Unix domain failed: expect at least %d fields but got %d",
			minFieldsCnt, fieldsLen)
	}
	kernelPtr, err := u.parseKernelPtr(fields[netUnixKernelPtrIdx])
	if err != nil {
		return nil, fmt.Errorf("Parse Unix domain num(%s) failed: %s", fields[netUnixKernelPtrIdx], err)
	}
	users, err := u.parseUsers(fields[netUnixRefCountIdx])
	if err != nil {
		return nil, fmt.Errorf("Parse Unix domain ref count(%s) failed: %s", fields[netUnixRefCountIdx], err)
	}
	flags, err := u.parseFlags(fields[netUnixFlagsIdx])
	if err != nil {
		return nil, fmt.Errorf("Parse Unix domain flags(%s) failed: %s", fields[netUnixFlagsIdx], err)
	}
	typ, err := u.parseType(fields[netUnixTypeIdx])
	if err != nil {
		return nil, fmt.Errorf("Parse Unix domain type(%s) failed: %s", fields[netUnixTypeIdx], err)
	}
	state, err := u.parseState(fields[netUnixStateIdx])
	if err != nil {
		return nil, fmt.Errorf("Parse Unix domain state(%s) failed: %s", fields[netUnixStateIdx], err)
	}
	var inode uint64
	if hasInode {
		inodeStr := fields[netUnixInodeIdx]
		inode, err = u.parseInode(inodeStr)
		if err != nil {
			return nil, fmt.Errorf("Parse Unix domain inode(%s) failed: %s", inodeStr, err)
		}
	}

	nuLine := &NetUnixLine{
		KernelPtr: kernelPtr,
		RefCount:  users,
		Type:      typ,
		Flags:     flags,
		State:     state,
		Inode:     inode,
	}

	// Path field is optional.
	if fieldsLen > minFieldsCnt {
		pathIdx := netUnixInodeIdx + 1
		if !hasInode {
			pathIdx--
		}
		nuLine.Path = fields[pathIdx]
	}

	return nuLine, nil
}

func (u NetUnix) parseKernelPtr(str string) (string, error) {
	if !strings.HasSuffix(str, ":") {
		return "", errInvalidKernelPtrFmt
	}
	return str[:len(str)-1], nil
}

func (u NetUnix) parseUsers(hexStr string) (uint64, error) {
	return strconv.ParseUint(hexStr, 16, 32)
}

func (u NetUnix) parseType(hexStr string) (NetUnixType, error) {
	typ, err := strconv.ParseUint(hexStr, 16, 16)
	if err != nil {
		return 0, err
	}
	return NetUnixType(typ), nil
}

func (u NetUnix) parseFlags(hexStr string) (NetUnixFlags, error) {
	flags, err := strconv.ParseUint(hexStr, 16, 32)
	if err != nil {
		return 0, err
	}
	return NetUnixFlags(flags), nil
}

func (u NetUnix) parseState(hexStr string) (NetUnixState, error) {
	st, err := strconv.ParseInt(hexStr, 16, 8)
	if err != nil {
		return 0, err
	}
	return NetUnixState(st), nil
}

func (u NetUnix) parseInode(inodeStr string) (uint64, error) {
	return strconv.ParseUint(inodeStr, 10, 64)
}

func (t NetUnixType) String() string {
	switch t {
	case netUnixTypeStream:
		return "stream"
	case netUnixTypeDgram:
		return "dgram"
	case netUnixTypeSeqpacket:
		return "seqpacket"
	}
	return "unknown"
}

func (f NetUnixFlags) String() string {
	switch f {
	case netUnixFlagListen:
		return "listen"
	default:
		return "default"
	}
}

func (s NetUnixState) String() string {
	switch s {
	case netUnixStateUnconnected:
		return "unconnected"
	case netUnixStateConnecting:
		return "connecting"
	case netUnixStateConnected:
		return "connected"
	case netUnixStateDisconnected:
		return "disconnected"
	}
	return "unknown"
}
