// Copyright 2017 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 xfs

import (
	"bufio"
	"fmt"
	"io"
	"strings"

	"github.com/prometheus/procfs/internal/util"
)

// ParseStats parses a Stats from an input io.Reader, using the format
// found in /proc/fs/xfs/stat.
func ParseStats(r io.Reader) (*Stats, error) {
	const (
		// Fields parsed into stats structures.
		fieldExtentAlloc = "extent_alloc"
		fieldAbt         = "abt"
		fieldBlkMap      = "blk_map"
		fieldBmbt        = "bmbt"
		fieldDir         = "dir"
		fieldTrans       = "trans"
		fieldIg          = "ig"
		fieldLog         = "log"
		fieldPushAil     = "push_ail"
		fieldXstrat      = "xstrat"
		fieldRw          = "rw"
		fieldAttr        = "attr"
		fieldIcluster    = "icluster"
		fieldVnodes      = "vnodes"
		fieldBuf         = "buf"
		fieldXpc         = "xpc"
		fieldAbtb2       = "abtb2"
		fieldAbtc2       = "abtc2"
		fieldBmbt2       = "bmbt2"
		fieldIbt2        = "ibt2"
		//fieldFibt2 = "fibt2"
		fieldQm    = "qm"
		fieldDebug = "debug"
		// Unimplemented at this time due to lack of documentation.
		//fieldRmapbt = "rmapbt"
		//fieldRefcntbt = "refcntbt"

	)

	var xfss Stats

	s := bufio.NewScanner(r)
	for s.Scan() {
		// Expect at least a string label and a single integer value, ex:
		//   - abt 0
		//   - rw 1 2
		ss := strings.Fields(string(s.Bytes()))
		if len(ss) < 2 {
			continue
		}
		label := ss[0]

		// Extended precision counters are uint64 values.
		if label == fieldXpc {
			us, err := util.ParseUint64s(ss[1:])
			if err != nil {
				return nil, err
			}

			xfss.ExtendedPrecision, err = extendedPrecisionStats(us)
			if err != nil {
				return nil, err
			}

			continue
		}

		// All other counters are uint32 values.
		us, err := util.ParseUint32s(ss[1:])
		if err != nil {
			return nil, err
		}

		switch label {
		case fieldExtentAlloc:
			xfss.ExtentAllocation, err = extentAllocationStats(us)
		case fieldAbt:
			xfss.AllocationBTree, err = btreeStats(us)
		case fieldBlkMap:
			xfss.BlockMapping, err = blockMappingStats(us)
		case fieldBmbt:
			xfss.BlockMapBTree, err = btreeStats(us)
		case fieldDir:
			xfss.DirectoryOperation, err = directoryOperationStats(us)
		case fieldTrans:
			xfss.Transaction, err = transactionStats(us)
		case fieldIg:
			xfss.InodeOperation, err = inodeOperationStats(us)
		case fieldLog:
			xfss.LogOperation, err = logOperationStats(us)
		case fieldRw:
			xfss.ReadWrite, err = readWriteStats(us)
		case fieldAttr:
			xfss.AttributeOperation, err = attributeOperationStats(us)
		case fieldIcluster:
			xfss.InodeClustering, err = inodeClusteringStats(us)
		case fieldVnodes:
			xfss.Vnode, err = vnodeStats(us)
		case fieldBuf:
			xfss.Buffer, err = bufferStats(us)
		case fieldPushAil:
			xfss.PushAil, err = pushAilStats(us)
		case fieldXstrat:
			xfss.Xstrat, err = xStratStats(us)
		case fieldAbtb2:
			xfss.BtreeAllocBlocks2, err = btreeAllocBlocks2Stats(us)
		case fieldAbtc2:
			xfss.BtreeAllocContig2, err = btreeAllocContig2Stats(us)
		case fieldBmbt2:
			xfss.BtreeBlockMap2, err = btreeBlockMap2Stats(us)
		case fieldIbt2:
			xfss.BtreeInode2, err = btreeInode2Stats(us)
		//case fieldFibt2:
		case fieldQm:
			xfss.QuotaManager, err = quotaManagerStats(us)
		case fieldDebug:
			xfss.Debug, err = debugStats(us)
		}
		if err != nil {
			return nil, err
		}
	}

	return &xfss, s.Err()
}

// extentAllocationStats builds an ExtentAllocationStats from a slice of uint32s.
func extentAllocationStats(us []uint32) (ExtentAllocationStats, error) {
	if l := len(us); l != 4 {
		return ExtentAllocationStats{}, fmt.Errorf("incorrect number of values for XFS extent allocation stats: %d", l)
	}

	return ExtentAllocationStats{
		ExtentsAllocated: us[0],
		BlocksAllocated:  us[1],
		ExtentsFreed:     us[2],
		BlocksFreed:      us[3],
	}, nil
}

// btreeStats builds a BTreeStats from a slice of uint32s.
func btreeStats(us []uint32) (BTreeStats, error) {
	if l := len(us); l != 4 {
		return BTreeStats{}, fmt.Errorf("incorrect number of values for XFS btree stats: %d", l)
	}

	return BTreeStats{
		Lookups:         us[0],
		Compares:        us[1],
		RecordsInserted: us[2],
		RecordsDeleted:  us[3],
	}, nil
}

// BlockMappingStat builds a BlockMappingStats from a slice of uint32s.
func blockMappingStats(us []uint32) (BlockMappingStats, error) {
	if l := len(us); l != 7 {
		return BlockMappingStats{}, fmt.Errorf("incorrect number of values for XFS block mapping stats: %d", l)
	}

	return BlockMappingStats{
		Reads:                us[0],
		Writes:               us[1],
		Unmaps:               us[2],
		ExtentListInsertions: us[3],
		ExtentListDeletions:  us[4],
		ExtentListLookups:    us[5],
		ExtentListCompares:   us[6],
	}, nil
}

// DirectoryOperationStats builds a DirectoryOperationStats from a slice of uint32s.
func directoryOperationStats(us []uint32) (DirectoryOperationStats, error) {
	if l := len(us); l != 4 {
		return DirectoryOperationStats{}, fmt.Errorf("incorrect number of values for XFS directory operation stats: %d", l)
	}

	return DirectoryOperationStats{
		Lookups:  us[0],
		Creates:  us[1],
		Removes:  us[2],
		Getdents: us[3],
	}, nil
}

// TransactionStats builds a TransactionStats from a slice of uint32s.
func transactionStats(us []uint32) (TransactionStats, error) {
	if l := len(us); l != 3 {
		return TransactionStats{}, fmt.Errorf("incorrect number of values for XFS transaction stats: %d", l)
	}

	return TransactionStats{
		Sync:  us[0],
		Async: us[1],
		Empty: us[2],
	}, nil
}

// InodeOperationStats builds an InodeOperationStats from a slice of uint32s.
func inodeOperationStats(us []uint32) (InodeOperationStats, error) {
	if l := len(us); l != 7 {
		return InodeOperationStats{}, fmt.Errorf("incorrect number of values for XFS inode operation stats: %d", l)
	}

	return InodeOperationStats{
		Attempts:        us[0],
		Found:           us[1],
		Recycle:         us[2],
		Missed:          us[3],
		Duplicate:       us[4],
		Reclaims:        us[5],
		AttributeChange: us[6],
	}, nil
}

// LogOperationStats builds a LogOperationStats from a slice of uint32s.
func logOperationStats(us []uint32) (LogOperationStats, error) {
	if l := len(us); l != 5 {
		return LogOperationStats{}, fmt.Errorf("incorrect number of values for XFS log operation stats: %d", l)
	}

	return LogOperationStats{
		Writes:            us[0],
		Blocks:            us[1],
		NoInternalBuffers: us[2],
		Force:             us[3],
		ForceSleep:        us[4],
	}, nil
}

// pushAilStats handles push_ail stats.
func pushAilStats(us []uint32) (PushAilStats, error) {
	if l := len(us); l != 10 {
		return PushAilStats{}, fmt.Errorf("incorrect number of values for XFS push ail stats: %d", l)
	}

	return PushAilStats{
		TryLogspace:   us[0],
		SleepLogspace: us[1],
		Pushes:        us[2],
		Success:       us[3],
		PushBuf:       us[4],
		Pinned:        us[5],
		Locked:        us[6],
		Flushing:      us[7],
		Restarts:      us[8],
		Flush:         us[9],
	}, nil
}

// xStratStats handles xstrat stats.
func xStratStats(us []uint32) (XstratStats, error) {
	if l := len(us); l != 2 {
		return XstratStats{}, fmt.Errorf("incorrect number of values for XFS  xstrat stats: %d", l)
	}

	return XstratStats{
		Quick: us[0],
		Split: us[1],
	}, nil
}

// readWriteStats handles rw stats.
func readWriteStats(us []uint32) (ReadWriteStats, error) {
	if l := len(us); l != 2 {
		return ReadWriteStats{}, fmt.Errorf("incorrect number of values for XFS read write stats: %d", l)
	}

	return ReadWriteStats{
		Write: us[0],
		Read:  us[1],
	}, nil
}

// AttributeOperationStats builds an AttributeOperationStats from a slice of uint32s.
func attributeOperationStats(us []uint32) (AttributeOperationStats, error) {
	if l := len(us); l != 4 {
		return AttributeOperationStats{}, fmt.Errorf("incorrect number of values for XFS attribute operation stats: %d", l)
	}

	return AttributeOperationStats{
		Get:    us[0],
		Set:    us[1],
		Remove: us[2],
		List:   us[3],
	}, nil
}

// InodeClusteringStats builds an InodeClusteringStats from a slice of uint32s.
func inodeClusteringStats(us []uint32) (InodeClusteringStats, error) {
	if l := len(us); l != 3 {
		return InodeClusteringStats{}, fmt.Errorf("incorrect number of values for XFS inode clustering stats: %d", l)
	}

	return InodeClusteringStats{
		Iflush:     us[0],
		Flush:      us[1],
		FlushInode: us[2],
	}, nil
}

// VnodeStats builds a VnodeStats from a slice of uint32s.
func vnodeStats(us []uint32) (VnodeStats, error) {
	// The attribute "Free" appears to not be available on older XFS
	// stats versions.  Therefore, 7 or 8 elements may appear in
	// this slice.
	l := len(us)
	if l != 7 && l != 8 {
		return VnodeStats{}, fmt.Errorf("incorrect number of values for XFS vnode stats: %d", l)
	}

	s := VnodeStats{
		Active:   us[0],
		Allocate: us[1],
		Get:      us[2],
		Hold:     us[3],
		Release:  us[4],
		Reclaim:  us[5],
		Remove:   us[6],
	}

	// Skip adding free, unless it is present. The zero value will
	// be used in place of an actual count.
	if l == 7 {
		return s, nil
	}

	s.Free = us[7]
	return s, nil
}

// BufferStats builds a BufferStats from a slice of uint32s.
func bufferStats(us []uint32) (BufferStats, error) {
	if l := len(us); l != 9 {
		return BufferStats{}, fmt.Errorf("incorrect number of values for XFS buffer stats: %d", l)
	}

	return BufferStats{
		Get:             us[0],
		Create:          us[1],
		GetLocked:       us[2],
		GetLockedWaited: us[3],
		BusyLocked:      us[4],
		MissLocked:      us[5],
		PageRetries:     us[6],
		PageFound:       us[7],
		GetRead:         us[8],
	}, nil
}

// ExtendedPrecisionStats builds an ExtendedPrecisionStats from a slice of uint32s.
func extendedPrecisionStats(us []uint64) (ExtendedPrecisionStats, error) {
	if l := len(us); l != 3 {
		return ExtendedPrecisionStats{}, fmt.Errorf("incorrect number of values for XFS extended precision stats: %d", l)
	}

	return ExtendedPrecisionStats{
		FlushBytes: us[0],
		WriteBytes: us[1],
		ReadBytes:  us[2],
	}, nil
}

func quotaManagerStats(us []uint32) (QuotaManagerStats, error) {
	// The "Unused" attribute first appears in Linux 4.20
	// As a result either 8 or 9 elements may appear in this slice depending on
	// the kernel version.
	l := len(us)
	if l != 8 && l != 9 {
		return QuotaManagerStats{}, fmt.Errorf("incorrect number of values for XFS quota stats: %d", l)
	}

	s := QuotaManagerStats{
		Reclaims:      us[0],
		ReclaimMisses: us[1],
		DquoteDups:    us[2],
		CacheMisses:   us[3],
		CacheHits:     us[4],
		Wants:         us[5],
		ShakeReclaims: us[6],
		InactReclaims: us[7],
	}

	if l > 8 {
		s.Unused = us[8]
	}

	return s, nil
}

func debugStats(us []uint32) (DebugStats, error) {
	if l := len(us); l != 1 {
		return DebugStats{}, fmt.Errorf("incorrect number of values for XFS debug stats: %d", l)
	}

	return DebugStats{
		Enabled: us[0],
	}, nil
}

// btreeAllocBlocks2Stats handles abtb2 stats.
func btreeAllocBlocks2Stats(us []uint32) (BtreeAllocBlocks2Stats, error) {
	if l := len(us); l != 15 {
		return BtreeAllocBlocks2Stats{}, fmt.Errorf("incorrect number of values for abtb2 stats: %d", 1)
	}

	return BtreeAllocBlocks2Stats{
		Lookup:    us[0],
		Compare:   us[1],
		Insrec:    us[2],
		Delrec:    us[3],
		NewRoot:   us[4],
		KillRoot:  us[5],
		Increment: us[6],
		Decrement: us[7],
		Lshift:    us[8],
		Rshift:    us[9],
		Split:     us[10],
		Join:      us[11],
		Alloc:     us[12],
		Free:      us[13],
		Moves:     us[14],
	}, nil
}

// btreeAllocContig2Stats handles abtc2 stats.
func btreeAllocContig2Stats(us []uint32) (BtreeAllocContig2Stats, error) {
	if l := len(us); l != 15 {
		return BtreeAllocContig2Stats{}, fmt.Errorf("incorrect number of values for abtc2 stats: %d", 1)
	}

	return BtreeAllocContig2Stats{
		Lookup:    us[0],
		Compare:   us[1],
		Insrec:    us[2],
		Delrec:    us[3],
		NewRoot:   us[4],
		KillRoot:  us[5],
		Increment: us[6],
		Decrement: us[7],
		Lshift:    us[8],
		Rshift:    us[9],
		Split:     us[10],
		Join:      us[11],
		Alloc:     us[12],
		Free:      us[13],
		Moves:     us[14],
	}, nil
}

// btreeBlockMap2Stats handles bmbt2 stats.
func btreeBlockMap2Stats(us []uint32) (BtreeBlockMap2Stats, error) {
	if l := len(us); l != 15 {
		return BtreeBlockMap2Stats{}, fmt.Errorf("incorrect number of values for bmbt2 stats: %d", 1)
	}

	return BtreeBlockMap2Stats{
		Lookup:    us[0],
		Compare:   us[1],
		Insrec:    us[2],
		Delrec:    us[3],
		NewRoot:   us[4],
		KillRoot:  us[5],
		Increment: us[6],
		Decrement: us[7],
		Lshift:    us[8],
		Rshift:    us[9],
		Split:     us[10],
		Join:      us[11],
		Alloc:     us[12],
		Free:      us[13],
		Moves:     us[14],
	}, nil
}

// btreeInode2Stats handles ibt2 stats.
func btreeInode2Stats(us []uint32) (BtreeInode2Stats, error) {
	if l := len(us); l != 15 {
		return BtreeInode2Stats{}, fmt.Errorf("incorrect number of values for ibt2 stats: %d", 1)
	}

	return BtreeInode2Stats{
		Lookup:    us[0],
		Compare:   us[1],
		Insrec:    us[2],
		Delrec:    us[3],
		NewRoot:   us[4],
		KillRoot:  us[5],
		Increment: us[6],
		Decrement: us[7],
		Lshift:    us[8],
		Rshift:    us[9],
		Split:     us[10],
		Join:      us[11],
		Alloc:     us[12],
		Free:      us[13],
		Moves:     us[14],
	}, nil
}
