package line

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"

	"github.com/derekparker/delve/dwarf/util"
)

type Location struct {
	File    string
	Line    int
	Address uint64
	Delta   int
}

type StateMachine struct {
	dbl             *DebugLineInfo
	file            string
	line            int
	address         uint64
	column          uint
	isStmt          bool
	basicBlock      bool
	endSeq          bool
	lastWasStandard bool
	lastDelta       int
}

type opcodefn func(*StateMachine, *bytes.Buffer)

// Special opcodes
const (
	DW_LNS_copy             = 1
	DW_LNS_advance_pc       = 2
	DW_LNS_advance_line     = 3
	DW_LNS_set_file         = 4
	DW_LNS_set_column       = 5
	DW_LNS_negate_stmt      = 6
	DW_LNS_set_basic_block  = 7
	DW_LNS_const_add_pc     = 8
	DW_LNS_fixed_advance_pc = 9
)

// Extended opcodes
const (
	DW_LINE_end_sequence = 1
	DW_LINE_set_address  = 2
	DW_LINE_define_file  = 3
)

var standardopcodes = map[byte]opcodefn{
	DW_LNS_copy:             copyfn,
	DW_LNS_advance_pc:       advancepc,
	DW_LNS_advance_line:     advanceline,
	DW_LNS_set_file:         setfile,
	DW_LNS_set_column:       setcolumn,
	DW_LNS_negate_stmt:      negatestmt,
	DW_LNS_set_basic_block:  setbasicblock,
	DW_LNS_const_add_pc:     constaddpc,
	DW_LNS_fixed_advance_pc: fixedadvancepc,
}

var extendedopcodes = map[byte]opcodefn{
	DW_LINE_end_sequence: endsequence,
	DW_LINE_set_address:  setaddress,
	DW_LINE_define_file:  definefile,
}

func newStateMachine(dbl *DebugLineInfo) *StateMachine {
	return &StateMachine{dbl: dbl, file: dbl.FileNames[0].Name, line: 1}
}

// Returns all PCs for a given file/line. Useful for loops where the 'for' line
// could be split amongst 2 PCs.
func (dbl *DebugLines) AllPCsForFileLine(f string, l int) (pcs []uint64) {
	var (
		foundFile bool
		lastAddr  uint64
		lineInfo  = dbl.GetLineInfo(f)
		sm        = newStateMachine(lineInfo)
		buf       = bytes.NewBuffer(lineInfo.Instructions)
	)

	for b, err := buf.ReadByte(); err == nil; b, err = buf.ReadByte() {
		findAndExecOpcode(sm, buf, b)
		if foundFile && sm.file != f {
			return
		}
		if sm.line == l && sm.file == f && sm.address != lastAddr {
			foundFile = true
			pcs = append(pcs, sm.address)
			line := sm.line
			// Keep going until we're on a different line. We only care about
			// when a line comes back around (i.e. for loop) so get to next line,
			// and try to find the line we care about again.
			for b, err := buf.ReadByte(); err == nil; b, err = buf.ReadByte() {
				findAndExecOpcode(sm, buf, b)
				if line < sm.line {
					break
				}
			}
		}
	}
	return
}

var NoSourceError = errors.New("no source available")

func (dbl *DebugLines) AllPCsBetween(begin, end uint64, filename string) ([]uint64, error) {
	lineInfo := dbl.GetLineInfo(filename)
	if lineInfo == nil {
		return nil, NoSourceError
	}
	var (
		pcs      []uint64
		lastaddr uint64
		sm       = newStateMachine(lineInfo)
		buf      = bytes.NewBuffer(lineInfo.Instructions)
	)

	for b, err := buf.ReadByte(); err == nil; b, err = buf.ReadByte() {
		findAndExecOpcode(sm, buf, b)
		if sm.address > end {
			break
		}
		if sm.address >= begin && sm.address > lastaddr {
			lastaddr = sm.address
			pcs = append(pcs, sm.address)
		}
	}
	return pcs, nil
}

func findAndExecOpcode(sm *StateMachine, buf *bytes.Buffer, b byte) {
	switch {
	case b == 0:
		execExtendedOpcode(sm, b, buf)
	case b < sm.dbl.Prologue.OpcodeBase:
		execStandardOpcode(sm, b, buf)
	default:
		execSpecialOpcode(sm, b)
	}
}

func execSpecialOpcode(sm *StateMachine, instr byte) {
	var (
		opcode  = uint8(instr)
		decoded = opcode - sm.dbl.Prologue.OpcodeBase
	)

	if sm.dbl.Prologue.InitialIsStmt == uint8(1) {
		sm.isStmt = true
	}

	sm.lastDelta = int(sm.dbl.Prologue.LineBase + int8(decoded%sm.dbl.Prologue.LineRange))
	sm.line += sm.lastDelta
	sm.address += uint64(decoded / sm.dbl.Prologue.LineRange)
	sm.basicBlock = false
	sm.lastWasStandard = false
}

func execExtendedOpcode(sm *StateMachine, instr byte, buf *bytes.Buffer) {
	_, _ = util.DecodeULEB128(buf)
	b, _ := buf.ReadByte()
	fn, ok := extendedopcodes[b]
	if !ok {
		panic(fmt.Sprintf("Encountered unknown extended opcode %#v\n", b))
	}
	sm.lastWasStandard = false

	fn(sm, buf)
}

func execStandardOpcode(sm *StateMachine, instr byte, buf *bytes.Buffer) {
	fn, ok := standardopcodes[instr]
	if !ok {
		panic(fmt.Sprintf("Encountered unknown standard opcode %#v\n", instr))
	}
	sm.lastWasStandard = true

	fn(sm, buf)
}

func copyfn(sm *StateMachine, buf *bytes.Buffer) {
	sm.basicBlock = false
}

func advancepc(sm *StateMachine, buf *bytes.Buffer) {
	addr, _ := util.DecodeULEB128(buf)
	sm.address += addr * uint64(sm.dbl.Prologue.MinInstrLength)
}

func advanceline(sm *StateMachine, buf *bytes.Buffer) {
	line, _ := util.DecodeSLEB128(buf)
	sm.line += int(line)
	sm.lastDelta = int(line)
}

func setfile(sm *StateMachine, buf *bytes.Buffer) {
	i, _ := util.DecodeULEB128(buf)
	sm.file = sm.dbl.FileNames[i-1].Name
}

func setcolumn(sm *StateMachine, buf *bytes.Buffer) {
	c, _ := util.DecodeULEB128(buf)
	sm.column = uint(c)
}

func negatestmt(sm *StateMachine, buf *bytes.Buffer) {
	sm.isStmt = !sm.isStmt
}

func setbasicblock(sm *StateMachine, buf *bytes.Buffer) {
	sm.basicBlock = true
}

func constaddpc(sm *StateMachine, buf *bytes.Buffer) {
	sm.address += (255 / uint64(sm.dbl.Prologue.LineRange))
}

func fixedadvancepc(sm *StateMachine, buf *bytes.Buffer) {
	var operand uint16
	binary.Read(buf, binary.LittleEndian, &operand)

	sm.address += uint64(operand)
}

func endsequence(sm *StateMachine, buf *bytes.Buffer) {
	sm.endSeq = true
}

func setaddress(sm *StateMachine, buf *bytes.Buffer) {
	var addr uint64

	binary.Read(buf, binary.LittleEndian, &addr)

	sm.address = addr
}

func definefile(sm *StateMachine, buf *bytes.Buffer) {
	var (
		_, _ = util.ParseString(buf)
		_, _ = util.DecodeULEB128(buf)
		_, _ = util.DecodeULEB128(buf)
		_, _ = util.DecodeULEB128(buf)
	)

	// Don't do anything here yet.
}
