package api

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
)

type PrintGoroutinesFlags uint8

const (
	PrintGoroutinesStack PrintGoroutinesFlags = 1 << iota
	PrintGoroutinesLabels
	PrintGoroutinesExec
)

type FormatGoroutineLoc int

const (
	FglRuntimeCurrent = FormatGoroutineLoc(iota)
	FglUserCurrent
	FglGo
	FglStart
)

const (
	maxGroupMembers    = 5
	maxGoroutineGroups = 50
)

// The number of goroutines we're going to request on each RPC call
const goroutineBatchSize = 10000

func ParseGoroutineArgs(argstr string) ([]ListGoroutinesFilter, GoroutineGroupingOptions, FormatGoroutineLoc, PrintGoroutinesFlags, int, int, string, error) {
	args := strings.Split(argstr, " ")
	var filters []ListGoroutinesFilter
	var group GoroutineGroupingOptions
	var fgl = FglUserCurrent
	var flags PrintGoroutinesFlags
	var depth = 10
	var batchSize = goroutineBatchSize
	var cmd string

	group.MaxGroupMembers = maxGroupMembers
	group.MaxGroups = maxGoroutineGroups

	for i := 0; i < len(args); i++ {
		arg := args[i]
		switch arg {
		case "-u":
			fgl = FglUserCurrent
		case "-r":
			fgl = FglRuntimeCurrent
		case "-g":
			fgl = FglGo
		case "-s":
			fgl = FglStart
		case "-l":
			flags |= PrintGoroutinesLabels
		case "-t":
			flags |= PrintGoroutinesStack
			// optional depth argument
			if i+1 < len(args) && len(args[i+1]) > 0 {
				n, err := strconv.Atoi(args[i+1])
				if err == nil {
					depth = n
					i++
				}
			}

		case "-w", "-with":
			filter, err := readGoroutinesFilter(args, &i)
			if err != nil {
				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
			}
			filters = append(filters, *filter)

		case "-wo", "-without":
			filter, err := readGoroutinesFilter(args, &i)
			if err != nil {
				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
			}
			filter.Negated = true
			filters = append(filters, *filter)

		case "-group":
			var err error
			group.GroupBy, err = readGoroutinesFilterKind(args, i+1)
			if err != nil {
				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
			}
			i++
			if group.GroupBy == GoroutineLabel {
				if i+1 >= len(args) {
					return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
				}
				group.GroupByKey = args[i+1]
				i++
			}
			batchSize = 0 // grouping only works well if run on all goroutines

		case "-chan":
			i++
			if i >= len(args) {
				return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", errors.New("not enough arguments after -chan")
			}
			filters = append(filters, ListGoroutinesFilter{Kind: GoroutineWaitingOnChannel, Arg: args[i]})

		case "-exec":
			flags |= PrintGoroutinesExec
			cmd = strings.Join(args[i+1:], " ")
			i = len(args)

		case "":
			// nothing to do
		default:
			return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", fmt.Errorf("wrong argument: '%s'", arg)
		}
	}
	return filters, group, fgl, flags, depth, batchSize, cmd, nil
}

func readGoroutinesFilterKind(args []string, i int) (GoroutineField, error) {
	if i >= len(args) {
		return GoroutineFieldNone, fmt.Errorf("%s must be followed by an argument", args[i-1])
	}

	switch args[i] {
	case "curloc":
		return GoroutineCurrentLoc, nil
	case "userloc":
		return GoroutineUserLoc, nil
	case "goloc":
		return GoroutineGoLoc, nil
	case "startloc":
		return GoroutineStartLoc, nil
	case "label":
		return GoroutineLabel, nil
	case "running":
		return GoroutineRunning, nil
	case "user":
		return GoroutineUser, nil
	default:
		return GoroutineFieldNone, fmt.Errorf("unrecognized argument to %s %s", args[i-1], args[i])
	}
}

func readGoroutinesFilter(args []string, pi *int) (*ListGoroutinesFilter, error) {
	r := new(ListGoroutinesFilter)
	var err error
	r.Kind, err = readGoroutinesFilterKind(args, *pi+1)
	if err != nil {
		return nil, err
	}
	*pi++
	switch r.Kind {
	case GoroutineRunning, GoroutineUser:
		return r, nil
	}
	if *pi+1 >= len(args) {
		return nil, fmt.Errorf("%s %s needs to be followed by an expression", args[*pi-1], args[*pi])
	}
	r.Arg = args[*pi+1]
	*pi++

	return r, nil
}

type ExamineMemoryArgs struct {
	Operand string
	Count   int64
	Size    int64
	IsExpr  bool
	Format  byte
	RawOut  bool
}

func ParseExamineMemoryArg(argstr string) (*ExamineMemoryArgs, error) {
	// default args
	out := ExamineMemoryArgs{
		Format: byte('x'),
		Count:  int64(1),
		Size:   int64(1),
		IsExpr: false,
		RawOut: false,
	}

	var (
		ok   bool
		args = strings.Split(argstr, " ")
	)

	// nextArg returns the next argument that is not an empty string, if any, and
	// advances the args slice to the position after that.
	nextArg := func() string {
		for len(args) > 0 {
			arg := args[0]
			args = args[1:]
			if arg != "" {
				return arg
			}
		}
		return ""
	}

loop:
	for {
		switch cmd := nextArg(); cmd {
		case "":
			// no more arguments
			break loop
		case "-fmt":
			arg := nextArg()
			if arg == "" {
				return nil, errors.New("expected argument after -fmt")
			}
			if arg == "raw" {
				out.RawOut = true
			} else {
				fmtMapToPriFmt := map[string]byte{
					"oct":         'o',
					"octal":       'o',
					"hex":         'x',
					"hexadecimal": 'x',
					"dec":         'd',
					"decimal":     'd',
					"bin":         'b',
					"binary":      'b',
				}
				out.Format, ok = fmtMapToPriFmt[arg]
				if !ok {
					return nil, fmt.Errorf("%q is not a valid format", arg)
				}
			}
		case "-count", "-len":
			arg := nextArg()
			if arg == "" {
				return nil, errors.New("expected argument after -count/-len")
			}
			var err error
			out.Count, err = strconv.ParseInt(arg, 0, 64)
			if err != nil || out.Count <= 0 {
				return nil, errors.New("count/len must be a positive integer")
			}
		case "-size":
			arg := nextArg()
			if arg == "" {
				return nil, errors.New("expected argument after -size")
			}
			var err error
			out.Size, err = strconv.ParseInt(arg, 0, 64)
			if err != nil || out.Size <= 0 || out.Size > 8 {
				return nil, errors.New("size must be a positive integer (<=8)")
			}
		case "-x":
			out.IsExpr = true
			// remaining args are going to be interpreted as expression
			out.Operand = strings.Join(args, " ")
			break loop
		default:
			if len(args) > 0 {
				return nil, fmt.Errorf("unknown option %q", args[0])
			}
			out.Operand = cmd
			break loop // only one arg left to be evaluated as a uint
		}
	}

	if len(out.Operand) == 0 {
		return nil, errors.New("no address specified")
	}

	return &out, nil
}
