package gohex

import (
	"bufio"
	"encoding/hex"
	"io"
	"sort"
)

// Constants definitions of IntelHex record types
const (
	_DATA_RECORD     byte = 0 // Record with data bytes
	_EOF_RECORD      byte = 1 // Record with end of file indicator
	_ADR_20_RECORD   byte = 2 // Record with extended 20-bit linear address
	_ADR_32_RECORD   byte = 4 // Record with extended 32-bit linear address
	_START_RECORD    byte = 5 // Record with start linear address
)

// Structure with binary data segment fields
type DataSegment struct {
	Address uint32 // Starting address of data segment
	Data    []byte // Data segment bytes
}

// Helper type for data segments sorting operations
type sortByAddress []*DataSegment

func (segs sortByAddress) Len() int           { return len(segs) }
func (segs sortByAddress) Swap(i, j int)      { segs[i], segs[j] = segs[j], segs[i] }
func (segs sortByAddress) Less(i, j int) bool { return segs[i].Address < segs[j].Address }

// Main structure with private fields of IntelHex parser
type Memory struct {
	dataSegments     []*DataSegment // Slice with pointers to DataSegments
	startAddress     uint32         // Start linear address
	extendedAddress  uint32         // Extended linear address
	eofFlag          bool           // End of file record exist flag
	startFlag        bool           // Start address record exist flag
	lineNum          uint           // Parser input line number
	firstAddressFlag bool           // Dump first address line
}

// Constructor of Memory structure
func NewMemory() *Memory {
	m := new(Memory)
	m.Clear()
	return m
}

// Method to getting start address from IntelHex data
func (m *Memory) GetStartAddress() (adr uint32, ok bool) {
	if m.startFlag {
		return m.startAddress, true
	}
	return 0, false
}

// Method to setting start address to IntelHex data
func (m *Memory) SetStartAddress(adr uint32) {
	m.startAddress = adr
	m.startFlag = true
}

// Method to getting data segments address from IntelHex data
func (m *Memory) GetDataSegments() []DataSegment {
	segs := []DataSegment{}
	for _, s := range m.dataSegments {
		segs = append(segs, *s)
	}
	return segs
}

// Method to clear memory structure
func (m *Memory) Clear() {
	m.startAddress = 0
	m.extendedAddress = 0
	m.lineNum = 0
	m.dataSegments = []*DataSegment{}
	m.startFlag = false
	m.eofFlag = false
	m.firstAddressFlag = false
}

func (seg *DataSegment) isOverlap(adr uint32, size uint32) bool {
	if ((adr >= seg.Address) && (adr < seg.Address+uint32(len(seg.Data)))) ||
		((adr < seg.Address) && (adr+size) > seg.Address) {
		return true
	}
	return false
}

func (m *Memory) removeSegment(index int) {
	size := len(m.dataSegments)

	if size == 0 {
		return
	} else if size == 1 {
		m.dataSegments = []*DataSegment{}
	} else {
		if index == 0 {
			m.dataSegments = m.dataSegments[1:]
		} else if index == size-1 {
			m.dataSegments = m.dataSegments[:index]
		} else {
			m.dataSegments = append(m.dataSegments[:index], m.dataSegments[index+1:]...)
		}
	}
}

func (m *Memory) findDataSegment(adr uint32) (seg *DataSegment, offset uint32, index int) {
	for i, s := range m.dataSegments {
		if s.isOverlap(adr, 1) == true {
			return s, adr - s.Address, i
		}
	}
	return nil, 0, 0
}

// Method to add binary data to memory (auto segmented and sorted)
func (m *Memory) AddBinary(adr uint32, bytes []byte) error {
	var segBefore *DataSegment = nil
	var segAfter *DataSegment = nil
	var segAfterIndex int
	for i, s := range m.dataSegments {
		if s.isOverlap(adr, uint32(len(bytes))) == true {
			return newParseError(_DATA_ERROR, "data segments overlap", m.lineNum)
		}

		if adr == s.Address+uint32(len(s.Data)) {
			segBefore = s
		}
		if adr+uint32(len(bytes)) == s.Address {
			segAfter, segAfterIndex = s, i
		}
	}

	if segBefore != nil && segAfter != nil {
		segBefore.Data = append(segBefore.Data, bytes...)
		segBefore.Data = append(segBefore.Data, segAfter.Data...)
		m.dataSegments = append(m.dataSegments[:segAfterIndex], m.dataSegments[segAfterIndex+1:]...)

	} else if segBefore != nil && segAfter == nil {
		segBefore.Data = append(segBefore.Data, bytes...)
	} else if segBefore == nil && segAfter != nil {
		segAfter.Address = adr
		segAfter.Data = append(bytes, segAfter.Data...)
	} else {
		m.dataSegments = append(m.dataSegments, &DataSegment{Address: adr, Data: bytes})
	}
	sort.Sort(sortByAddress(m.dataSegments))
	return nil
}

// Method to set binary data to memory (data overlapped will change, auto segmented and sorted)
func (m *Memory) SetBinary(adr uint32, bytes []byte) {
	for a, b := range bytes {
		currentAdr := adr + uint32(a)
		seg, offset, _ := m.findDataSegment(currentAdr)

		if seg != nil {
			seg.Data[offset] = b
		} else {
			m.AddBinary(currentAdr, []byte{b})
		}
	}
}

// Method to remove binary data from memory (auto segmented and sorted)
func (m *Memory) RemoveBinary(adr uint32, size uint32) {
	adrEnd := adr + size
	for currentAdr := adr; currentAdr < adrEnd; currentAdr++ {
		seg, offset, index := m.findDataSegment(currentAdr)

		if seg == nil {
			continue
		}

		if offset == 0 {
			seg.Address += 1
			if len(seg.Data) > 1 {
				seg.Data = seg.Data[1:]
			} else {
				m.removeSegment(index)
			}
		} else if offset == uint32(len(seg.Data)-1) {
			if len(seg.Data) > 1 {
				seg.Data = seg.Data[:offset]
			} else {
				m.removeSegment(index)
			}
		} else {
			newSeg := DataSegment{Address: seg.Address + offset + 1, Data: seg.Data[offset+1:]}
			seg.Data = seg.Data[:offset]
			m.dataSegments = append(m.dataSegments, &newSeg)
		}
	}
	sort.Sort(sortByAddress(m.dataSegments))
}

func (m *Memory) parseIntelHexRecord(bytes []byte) error {
	if len(bytes) < 5 {
		return newParseError(_DATA_ERROR, "not enought data bytes", m.lineNum)
	}
	err := checkSum(bytes)
	if err != nil {
		return newParseError(_CHECKSUM_ERROR, err.Error(), m.lineNum)
	}
	err = checkRecordSize(bytes)
	if err != nil {
		return newParseError(_DATA_ERROR, err.Error(), m.lineNum)
	}
	switch record_type := bytes[3]; record_type {
	case _DATA_RECORD:
		a, data := getDataLine(bytes)
		adr := uint32(a) + m.extendedAddress
		err = m.AddBinary(adr, data)
		if err != nil {
			return err
		}
	case _EOF_RECORD:
		err = checkEOF(bytes)
		if err != nil {
			return newParseError(_RECORD_ERROR, err.Error(), m.lineNum)
		}
		m.eofFlag = true
	case _ADR_20_RECORD:
		fallthrough
	case _ADR_32_RECORD:
		m.extendedAddress, err = getExtendedAddress(bytes)
		if err != nil {
			return newParseError(_RECORD_ERROR, err.Error(), m.lineNum)
		}
	case _START_RECORD:
		if m.startFlag == true {
			return newParseError(_DATA_ERROR, "multiple start address lines", m.lineNum)
		}
		m.startAddress, err = getStartAddress(bytes)
		if err != nil {
			return newParseError(_RECORD_ERROR, err.Error(), m.lineNum)
		}
		m.startFlag = true
	}
	return nil
}

func (m *Memory) parseIntelHexLine(line string) error {
	if len(line) == 0 {
		return nil
	}
	if line[0] != ':' {
		return newParseError(_SYNTAX_ERROR, "no colon char on the first line character", m.lineNum)
	}
	bytes, err := hex.DecodeString(line[1:])
	if err != nil {
		return newParseError(_SYNTAX_ERROR, err.Error(), m.lineNum)
	}
	return m.parseIntelHexRecord(bytes)
}

// Method to parsing IntelHex data and add into memory
func (m *Memory) ParseIntelHex(reader io.Reader) error {
	scanner := bufio.NewScanner(reader)
	m.Clear()
	for scanner.Scan() {
		m.lineNum++
		line := scanner.Text()
		err := m.parseIntelHexLine(line)
		if err != nil {
			return err
		}
	}
	if err := scanner.Err(); err != nil {
		return newParseError(_SYNTAX_ERROR, err.Error(), m.lineNum)
	}
	if m.eofFlag == false {
		return newParseError(_DATA_ERROR, "no end of file line", m.lineNum)
	}

	return nil
}

func (m *Memory) dumpDataSegment(writer io.Writer, s *DataSegment, lineLength byte) error {
	lineAdr := s.Address
	lineData := []byte{}
	for byteAdr := s.Address; byteAdr < s.Address+uint32(len(s.Data)); byteAdr++ {
		if ((byteAdr & 0xFFFF0000) != m.extendedAddress) || (m.firstAddressFlag == false) {
			m.firstAddressFlag = true
			if len(lineData) != 0 {
				err := writeDataLine(writer, &lineAdr, byteAdr, &lineData)
				if err != nil {
					return err
				}
			}
			m.extendedAddress = (byteAdr & 0xFFFF0000)
			writeExtendedAddressLine(writer, m.extendedAddress)
		}
		if len(lineData) >= int(lineLength) {
			err := writeDataLine(writer, &lineAdr, byteAdr, &lineData)
			if err != nil {
				return err
			}
		}
		lineData = append(lineData, s.Data[byteAdr-s.Address])
	}

	if len(lineData) != 0 {
		return writeDataLine(writer, &lineAdr, 0, &lineData)
	}
	return nil
}

// Method to dumping IntelHex data previously loaded into memory
func (m *Memory) DumpIntelHex(writer io.Writer, lineLength byte) error {
	if m.startFlag {
		err := writeStartAddressLine(writer, m.startAddress)
		if err != nil {
			return err
		}
	}

	m.firstAddressFlag = false
	m.extendedAddress = 0
	for _, s := range m.dataSegments {
		err := m.dumpDataSegment(writer, s, lineLength)
		if err != nil {
			return err
		}
	}

	return writeEofLine(writer)
}

// Method to load binary data previously loaded into memory
func (m *Memory) ToBinary(address uint32, size uint32, padding byte) []byte {
	data := make([]byte, size)

	i := uint32(0)
	for i < size {
		ok := false
		for _, s := range m.dataSegments {
			if (address >= s.Address) && (address < s.Address+uint32(len(s.Data))) {
				data[i] = s.Data[address-s.Address]
				i++
				address++
				ok = true
				break
			}
		}
		if ok == false {
			data[i] = padding
			i++
			address++
		}
	}

	return data
}
