package crashlog

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"regexp"
	"strconv"
	"strings"

	"github.com/blacktop/ipsw/internal/utils"
)

// CrashLog is a crashlog object
type CrashLog struct {
	ReportVersion int
	HardwareModel string
	OSVersion     string
	OSBuild       string
	Process       string
	PID           int

	ExceptionType      string
	ExceptionSubtype   []string
	TerminationSignal  string
	TerminationReason  string
	TerminatingProcess string

	Images  []image
	Threads []thread

	CrashedThread int

	lines  []string
	closer io.Closer
}

type image struct {
	Name  string
	Start uint64
	End   uint64
	Slide uint64
	Arch  string
	UUID  string
	Path  string
}

type state map[string]uint64

// TODO: check that crashlog is arm64
func (s state) String() string {
	return fmt.Sprintf(
		"    x0: %#016x   x1: %#016x   x2: %#016x   x3: %#016x\n"+
			"    x4: %#016x   x5: %#016x   x6: %#016x   x7: %#016x\n"+
			"    x8: %#016x   x9: %#016x  x10: %#016x  x11: %#016x\n"+
			"   x12: %#016x  x13: %#016x  x14: %#016x  x15: %#016x\n"+
			"   x16: %#016x  x17: %#016x  x18: %#016x  x19: %#016x\n"+
			"   x20: %#016x  x21: %#016x  x22: %#016x  x23: %#016x\n"+
			"   x24: %#016x  x25: %#016x  x26: %#016x  x27: %#016x\n"+
			"   x28: %#016x   fp: %#016x   lr: %#016x\n"+
			"    sp: %#016x   pc: %#016x cpsr: %#08x\n"+
			"   esr: %#08x\n",
		s["x0"], s["x1"], s["x2"], s["x3"],
		s["x4"], s["x5"], s["x6"], s["x7"],
		s["x8"], s["x9"], s["x10"], s["x11"],
		s["x12"], s["x13"], s["x14"], s["x15"],
		s["x16"], s["x17"], s["x18"], s["x19"],
		s["x20"], s["x21"], s["x22"], s["x23"],
		s["x24"], s["x25"], s["x26"], s["x27"],
		s["x28"], s["fp"], s["lr"],
		s["sp"], s["pc"], s["cpsr"],
		s["esr"])
}

type thread struct {
	Number    int
	Name      string
	BackTrace []backtrace
	State     state

	lines []string
}

type backtrace struct {
	FrameNum int
	Image    *image
	Address  uint64
	LibAddr  uint64
	Offset   int
	Symbol   string
}

// Open opens the named file using os.Open and prepares it for use as a crashlog
func Open(name string) (*CrashLog, error) {

	crash := CrashLog{}

	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		crash.lines = append(crash.lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		fmt.Fprintln(os.Stderr, "reading standard input:", err)
	}

	if err := crash.getReportVersion(); err != nil {
		return nil, fmt.Errorf("failed to parse report version: %v", err)
	}
	if err := crash.getHardwareModel(); err != nil {
		return nil, fmt.Errorf("failed to parse hardware model: %v", err)
	}
	if err := crash.getOSVersion(); err != nil {
		return nil, fmt.Errorf("failed to parse os version: %v", err)
	}
	if err := crash.getProcess(); err != nil {
		return nil, fmt.Errorf("failed to parse process: %v", err)
	}
	if crash.ReportVersion != 104 && crash.ReportVersion != 105 {
		return nil, fmt.Errorf("unsupported crash log report version: %d", crash.ReportVersion)
	}

	if err := crash.getExceptionType(); err != nil {
		return nil, fmt.Errorf("failed to parse exception type: %v", err)
	}
	if err := crash.getExceptionSubtype(); err != nil {
		return nil, fmt.Errorf("failed to parse exception subtype: %v", err)
	}
	if err := crash.getTerminationSignal(); err != nil {
		return nil, fmt.Errorf("failed to parse termination signal: %v", err)
	}
	if err := crash.getTerminationReason(); err != nil {
		return nil, fmt.Errorf("failed to parse termination reason: %v", err)
	}
	if err := crash.getTerminatingProcess(); err != nil {
		return nil, fmt.Errorf("failed to parse terminating process: %v", err)
	}
	if err := crash.getTriggeringThread(); err != nil {
		return nil, fmt.Errorf("failed to parse triggering thread: %v", err)
	}

	if err := crash.getImages(); err != nil {
		return nil, fmt.Errorf("failed to parse images: %v", err)
	}
	if err := crash.getThreads(); err != nil {
		return nil, fmt.Errorf("failed to parse threads: %v", err)
	}
	if err := crash.getBackTraces(); err != nil {
		return nil, fmt.Errorf("failed to parse back traces: %v", err)
	}

	// TODO: prune unused images?

	crash.closer = f

	return &crash, nil
}

// Close closes the File.
func (c *CrashLog) Close() error {
	var err error

	if c.closer != nil {
		err = c.closer.Close()
		c.closer = nil
	}

	return err
}

func (c *CrashLog) String() string {
	return fmt.Sprintf(
		"Process:             %s [%d]\n"+
			"Hardware Model:      %s\n"+
			"OS Version:          %s\n"+
			"BuildID:             %s\n\n"+
			"Exception Type:      %s\n"+
			"Exception Subtype:\n%s\n\n"+
			"Termination Signal:  %s\n"+
			"Termination Reason:  %s\n"+
			"Terminating Process: %s\n"+
			"Triggered by Thread: %d\n",
		c.Process, c.PID,
		c.HardwareModel,
		c.OSVersion, c.OSBuild,
		c.ExceptionType,
		strings.Join(c.ExceptionSubtype, "\n"),
		c.TerminationSignal,
		c.TerminationReason,
		c.TerminatingProcess,
		c.CrashedThread,
	)
}

// getReportVersion fills out the report version
func (c *CrashLog) getReportVersion() error {
	re := regexp.MustCompile(`(\d+)`)

	for _, line := range c.lines {
		if strings.HasPrefix(line, "Report Version:") {
			version := re.FindString(line)
			i, err := strconv.Atoi(version)
			if err != nil {
				return err
			}
			c.ReportVersion = i
		}
	}

	return nil
}

// getHardwareModel parses hardware model in the crashlog
func (c *CrashLog) getHardwareModel() error {
	re := regexp.MustCompile(`Hardware Model:\s+(?P<model>\S+)`)

	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.HardwareModel = matches[re.SubexpIndex("model")]
			return nil
		}
	}

	return nil
}

// getOSVersion parses OS version and build ID in the crashlog
func (c *CrashLog) getOSVersion() error {
	re := regexp.MustCompile(`^OS Version:\s+.+\s(?P<version>[0-9.]+)\s+\((?P<build>(\w+))\)$`)
	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.OSVersion = matches[re.SubexpIndex("version")]
			c.OSBuild = matches[re.SubexpIndex("build")]
			return nil
		}
	}

	return nil
}

// getProcess parses the crashing process in the crashlog
func (c *CrashLog) getProcess() error {
	re := regexp.MustCompile(`^Process:\s+(?P<proc>\S+)\s\[(?P<pid>\d+)\]$`)
	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.Process = matches[re.SubexpIndex("proc")]
			pid, err := strconv.Atoi(matches[re.SubexpIndex("pid")])
			if err != nil {
				return err
			}
			c.PID = pid
			return nil
		}
	}

	return nil
}

// getExceptionType parses exception type in the crashlog
func (c *CrashLog) getExceptionType() error {
	re := regexp.MustCompile(`^Exception Type:\s+(?P<type>.+)$`)

	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.ExceptionType = matches[re.SubexpIndex("type")]
			return nil
		}
	}

	return nil
}

// getExceptionSubtype parses exception subtype in the crashlog
func (c *CrashLog) getExceptionSubtype() error {
	re := regexp.MustCompile(`Exception Subtype:\s+(?P<stype>.+)`)

	found := false
	for _, line := range c.lines {
		if strings.HasPrefix(line, "Exception Subtype:") {
			found = true
		}
		if found {
			if len(line) == 0 {
				return nil
			}
			if re.MatchString(line) {
				matches := re.FindStringSubmatch(line)
				c.ExceptionSubtype = append(c.ExceptionSubtype, matches[re.SubexpIndex("stype")])
			} else {
				c.ExceptionSubtype = append(c.ExceptionSubtype, line)
			}
		}
	}

	return nil
}

// getTerminationSignal parses termination signal in the crashlog
func (c *CrashLog) getTerminationSignal() error {
	re := regexp.MustCompile(`^Termination Signal:\s+(?P<signal>.+)$`)

	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.TerminationSignal = matches[re.SubexpIndex("signal")]
			return nil
		}
	}

	return nil
}

// getHardwareModel parses termination reason in the crashlog
func (c *CrashLog) getTerminationReason() error {
	re := regexp.MustCompile(`^Termination Reason:\s+(?P<reason>.+)$`)

	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.TerminationReason = matches[re.SubexpIndex("reason")]
			return nil
		}
	}

	return nil
}

// getTerminatingProcess parses terminating process in the crashlog
func (c *CrashLog) getTerminatingProcess() error {
	re := regexp.MustCompile(`^Terminating Process:\s+(?P<proc>.+)$`)

	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			c.TerminatingProcess = matches[re.SubexpIndex("proc")]
			return nil
		}
	}

	return nil
}

// getTriggeringThread parses triggering thread in the crashlog
func (c *CrashLog) getTriggeringThread() error {
	re := regexp.MustCompile(`Triggered by Thread:\s+(?P<thread>\d+)`)

	for _, line := range c.lines {
		if re.MatchString(line) {
			matches := re.FindStringSubmatch(line)
			thread, err := strconv.Atoi(matches[re.SubexpIndex("thread")])
			if err != nil {
				return err
			}
			c.CrashedThread = thread
			return nil
		}
	}

	return nil
}

// getImages parses all the binary images in the crashlog
func (c *CrashLog) getImages() error {
	re := regexp.MustCompile(`(?P<start>\w+)\s-\s+(?P<end>\w+)\s+(?P<name>.+)\s+(?P<arch>rmv[4-8][tfsk]?|arm64\S*|i386|x86_64\S)\s+\<?(?P<uuid>[[:xdigit:]]{32})?\>?\s* (?P<path>\/.*)\s*$`)
	found := false
	for _, line := range c.lines {
		if strings.HasPrefix(line, "Binary Images") {
			found = true
			continue
		}
		if found {
			if len(line) == 0 || strings.EqualFold(line, "EOF") {
				return nil
			}
			if re.MatchString(line) {
				matches := re.FindStringSubmatch(line)
				start, err := utils.ConvertStrToInt(matches[re.SubexpIndex("start")])
				if err != nil {
					return err
				}
				end, err := utils.ConvertStrToInt(matches[re.SubexpIndex("end")])
				if err != nil {
					return err
				}
				c.Images = append(c.Images, image{
					Name:  matches[re.SubexpIndex("name")],
					Start: start,
					End:   end,
					Arch:  matches[re.SubexpIndex("arch")],
					UUID:  matches[re.SubexpIndex("uuid")],
					Path:  matches[re.SubexpIndex("path")],
				})
			}
		}
	}

	return nil
}

// getThreads parses all the threads in the crashlog
func (c *CrashLog) getThreads() error {
	var t thread

	re := regexp.MustCompile(`Thread\s+(?P<num>\d+)\s?(Highlighted|Crashed)?(name:\s+(?P<name>.*))?`)
	stateRE := regexp.MustCompile(`\s+(?P<reg>\w+): (?P<addr>0x\w+)`)

	found := false
	isThreadState := false

	for _, line := range c.lines {
		if strings.HasPrefix(line, "Thread ") && !strings.Contains(line, "Thread State") {
			found = true
		} else if strings.Contains(line, "Thread State") {
			isThreadState = true
			if c.CrashedThread >= 0 && c.CrashedThread < len(c.Threads) {
				c.Threads[c.CrashedThread].State = make(map[string]uint64)
			}
		}

		if found {
			if len(line) == 0 {
				c.Threads = append(c.Threads, t)
				t = thread{}
				found = false
				continue
			}
			if re.MatchString(line) {
				matches := re.FindStringSubmatch(line)
				num, err := strconv.Atoi(matches[re.SubexpIndex("num")])
				if err != nil {
					return err
				}
				t.Number = num
				if re.SubexpIndex("name") >= 0 {
					if len(matches[re.SubexpIndex("name")]) > 0 {
						t.Name = matches[re.SubexpIndex("name")]
					}
				}
			} else {
				t.lines = append(t.lines, line)
			}
		}

		if isThreadState {
			if len(line) == 0 {
				isThreadState = false
				continue
			}
			if stateRE.MatchString(line) {
				matches := stateRE.FindAllStringSubmatch(line, -1)
				for _, match := range matches {
					reg := match[stateRE.SubexpIndex("reg")]
					addr, err := utils.ConvertStrToInt(match[stateRE.SubexpIndex("addr")])
					if err != nil {
						return err
					}
					if c.CrashedThread >= 0 && c.CrashedThread < len(c.Threads) && c.Threads[c.CrashedThread].State != nil {
						c.Threads[c.CrashedThread].State[reg] = addr
					}
				}
			}
		}

	}

	return nil
}

// getBackTraces parses all thread backtraces in the crashlog
func (c *CrashLog) getBackTraces() error {

	re := regexp.MustCompile(`^(?P<num>\d+)\s+(?P<image>\S.*?)\s+(?P<addr>0x\w+)\s+(?P<libaddr>0x\w+)\s+\+\s+(?P<off>\d+)?.*$`)

	for idx, thread := range c.Threads {
		for _, btline := range thread.lines {
			if re.MatchString(btline) {
				matches := re.FindStringSubmatch(btline)
				imageName := matches[re.SubexpIndex("image")]
				imgIdx, ok := -1, false
				for iidx, img := range c.Images {
					if img.Name == imageName {
						imgIdx, ok = iidx, true
						break
					}
				}
				if !ok {
					continue // empty image table or unmatched image: don't index out of range or mis-attribute to image 0
				}
				num, err := strconv.Atoi(matches[re.SubexpIndex("num")])
				if err != nil {
					return err
				}
				addr, err := utils.ConvertStrToInt(matches[re.SubexpIndex("addr")])
				if err != nil {
					return err
				}
				libAddr, err := utils.ConvertStrToInt(matches[re.SubexpIndex("libaddr")])
				if err != nil {
					return err
				}
				off, err := strconv.Atoi(matches[re.SubexpIndex("off")])
				if err != nil {
					return err
				}
				c.Threads[idx].BackTrace = append(c.Threads[idx].BackTrace, backtrace{
					FrameNum: num,
					Address:  addr,
					LibAddr:  libAddr,
					Offset:   off,
					Image:    &c.Images[imgIdx],
				})
			}
		}
	}

	return nil
}
