package document

import (
	"go.devnw.com/ooxml/schema/soo/wml"
)

// Paragraph is a paragraph within a document.
type Paragraph struct {
	d *Document
	x *wml.CT_P
}

// X returns the inner wrapped XML type.
func (p Paragraph) X() *wml.CT_P {
	return p.x
}

func (p Paragraph) ensurePPr() {
	if p.x.PPr == nil {
		p.x.PPr = wml.NewCT_PPr()
	}
}

// RemoveRun removes a child run from a paragraph.
func (p Paragraph) RemoveRun(r Run) {
	for _, c := range p.x.EG_PContent {
		for i, rc := range c.EG_ContentRunContent {
			if rc.R == r.x {
				copy(c.EG_ContentRunContent[i:], c.EG_ContentRunContent[i+1:])
				c.EG_ContentRunContent = c.EG_ContentRunContent[0 : len(c.EG_ContentRunContent)-1]
			}
			if rc.Sdt != nil && rc.Sdt.SdtContent != nil {
				for i, rc2 := range rc.Sdt.SdtContent.EG_ContentRunContent {
					if rc2.R == r.x {
						copy(rc.Sdt.SdtContent.EG_ContentRunContent[i:], rc.Sdt.SdtContent.EG_ContentRunContent[i+1:])
						rc.Sdt.SdtContent.EG_ContentRunContent = rc.Sdt.SdtContent.EG_ContentRunContent[0 : len(rc.Sdt.SdtContent.EG_ContentRunContent)-1]
					}
				}
			}
		}
	}
}

// Properties returns the paragraph properties.
func (p Paragraph) Properties() ParagraphProperties {
	p.ensurePPr()
	return ParagraphProperties{p.d, p.x.PPr}
}

// Style returns the style for a paragraph, or an empty string if it is unset.
func (p Paragraph) Style() string {
	if p.x.PPr != nil && p.x.PPr.PStyle != nil {
		return p.x.PPr.PStyle.ValAttr
	}
	return ""
}

// SetStyle sets the style of a paragraph and is identical to setting it on the
// paragraph's Properties()
func (p Paragraph) SetStyle(s string) {
	p.ensurePPr()
	if s == "" {
		p.x.PPr.PStyle = nil
	} else {
		p.x.PPr.PStyle = wml.NewCT_String()
		p.x.PPr.PStyle.ValAttr = s
	}
}

// AddRun adds a run to a paragraph.
func (p Paragraph) AddRun() Run {
	pc := wml.NewEG_PContent()
	p.x.EG_PContent = append(p.x.EG_PContent, pc)

	rc := wml.NewEG_ContentRunContent()
	pc.EG_ContentRunContent = append(pc.EG_ContentRunContent, rc)
	r := wml.NewCT_R()
	rc.R = r
	return Run{p.d, r}
}

// Runs returns all of the runs in a paragraph.
func (p Paragraph) Runs() []Run {
	ret := []Run{}
	for _, c := range p.x.EG_PContent {
		for _, rc := range c.EG_ContentRunContent {
			if rc.R != nil {
				ret = append(ret, Run{p.d, rc.R})
			}
			if rc.Sdt != nil && rc.Sdt.SdtContent != nil {
				for _, rc2 := range rc.Sdt.SdtContent.EG_ContentRunContent {
					if rc2.R != nil {
						ret = append(ret, Run{p.d, rc2.R})
					}
				}
			}
		}
	}
	return ret
}

// InsertRunAfter inserts a run in the paragraph after the relative run.
func (p Paragraph) InsertRunAfter(relativeTo Run) Run {
	return p.insertRun(relativeTo, false)
}

// InsertRunBefore inserts a run in the paragraph before the relative run.
func (p Paragraph) InsertRunBefore(relativeTo Run) Run {
	return p.insertRun(relativeTo, true)
}

func (p Paragraph) insertRun(relativeTo Run, before bool) Run {
	for _, c := range p.x.EG_PContent {
		for i, rc := range c.EG_ContentRunContent {
			if rc.R == relativeTo.X() {
				r := wml.NewCT_R()
				c.EG_ContentRunContent = append(c.EG_ContentRunContent, nil)
				if before {
					copy(c.EG_ContentRunContent[i+1:], c.EG_ContentRunContent[i:])
					c.EG_ContentRunContent[i] = wml.NewEG_ContentRunContent()
					c.EG_ContentRunContent[i].R = r
				} else {
					copy(c.EG_ContentRunContent[i+2:], c.EG_ContentRunContent[i+1:])
					c.EG_ContentRunContent[i+1] = wml.NewEG_ContentRunContent()
					c.EG_ContentRunContent[i+1].R = r
				}
				return Run{p.d, r}

			}
			if rc.Sdt != nil && rc.Sdt.SdtContent != nil {
				for _, rc2 := range rc.Sdt.SdtContent.EG_ContentRunContent {
					if rc2.R == relativeTo.X() {
						r := wml.NewCT_R()
						rc.Sdt.SdtContent.EG_ContentRunContent = append(rc.Sdt.SdtContent.EG_ContentRunContent, nil)
						if before {
							copy(rc.Sdt.SdtContent.EG_ContentRunContent[i+1:], rc.Sdt.SdtContent.EG_ContentRunContent[i:])
							rc.Sdt.SdtContent.EG_ContentRunContent[i] = wml.NewEG_ContentRunContent()
							rc.Sdt.SdtContent.EG_ContentRunContent[i].R = r
						} else {
							copy(rc.Sdt.SdtContent.EG_ContentRunContent[i+2:], rc.Sdt.SdtContent.EG_ContentRunContent[i+1:])
							rc.Sdt.SdtContent.EG_ContentRunContent[i+1] = wml.NewEG_ContentRunContent()
							rc.Sdt.SdtContent.EG_ContentRunContent[i+1].R = r
						}
						return Run{p.d, r}
					}
				}
			}
		}
	}
	return p.AddRun()
}

// AddHyperLink adds a new hyperlink to a parapgraph.
func (p Paragraph) AddHyperLink() HyperLink {
	pc := wml.NewEG_PContent()
	p.x.EG_PContent = append(p.x.EG_PContent, pc)

	pc.Hyperlink = wml.NewCT_Hyperlink()
	return HyperLink{p.d, pc.Hyperlink}
}

// AddBookmark adds a bookmark to a document that can then be used from a hyperlink. Name is a document
// unique name that identifies the bookmark so it can be referenced from hyperlinks.
func (p Paragraph) AddBookmark(name string) Bookmark {
	pc := wml.NewEG_PContent()
	rc := wml.NewEG_ContentRunContent()
	pc.EG_ContentRunContent = append(pc.EG_ContentRunContent, rc)

	relt := wml.NewEG_RunLevelElts()
	rc.EG_RunLevelElts = append(rc.EG_RunLevelElts, relt)

	markEl := wml.NewEG_RangeMarkupElements()
	bmStart := wml.NewCT_Bookmark()
	markEl.BookmarkStart = bmStart
	relt.EG_RangeMarkupElements = append(relt.EG_RangeMarkupElements, markEl)

	markEl = wml.NewEG_RangeMarkupElements()
	markEl.BookmarkEnd = wml.NewCT_MarkupRange()

	relt.EG_RangeMarkupElements = append(relt.EG_RangeMarkupElements, markEl)

	p.x.EG_PContent = append(p.x.EG_PContent, pc)

	bm := Bookmark{bmStart}
	bm.SetName(name)
	return bm
}

// AddFootnote will create a new footnote and attach it to the Paragraph in the
// location at the end of the previous run (footnotes create their own run within
// the paragraph). The text given to the function is simply a convenience helper,
// paragraphs and runs can always be added to the text of the footnote later.
func (p Paragraph) AddFootnote(text string) Footnote {
	// ensure we use a proper IdAttr for the new footnote
	var fnIDHeight int64
	if p.d.HasFootnotes() {
		for _, f := range p.d.Footnotes() {
			if f.id() > fnIDHeight {
				fnIDHeight = f.id()
			}
		}
		fnIDHeight++
	} else {
		fnIDHeight = 0
		p.d.footNotes = &wml.Footnotes{}
		p.d.footNotes.CT_Footnotes = wml.CT_Footnotes{}
		p.d.footNotes.Footnote = make([]*wml.CT_FtnEdn, 0)
	}

	// create a new footnote and footnote reference
	fn := wml.NewCT_FtnEdn()       // doc.footNotes.CT_Footnotes.Footnote[0]
	fnRef := wml.NewCT_FtnEdnRef() // p.Runs()[0].X().EG_RunInnerContent[0].FootnoteReference
	fnRef.IdAttr = fnIDHeight

	// add new footnote to document
	p.d.footNotes.CT_Footnotes.Footnote = append(p.d.footNotes.CT_Footnotes.Footnote, fn)

	// Add the newly created footnote reference to a new run on the parent paragraph
	run := p.AddRun()
	runP := run.Properties()
	runP.SetStyle("FootnoteAnchor")
	run.x.EG_RunInnerContent = []*wml.EG_RunInnerContent{wml.NewEG_RunInnerContent()}
	run.x.EG_RunInnerContent[0].FootnoteReference = fnRef

	// formulate the new fnobject's inners
	fnObj := Footnote{p.d, fn}
	fnObj.x.IdAttr = fnIDHeight
	fnObj.x.EG_BlockLevelElts = []*wml.EG_BlockLevelElts{wml.NewEG_BlockLevelElts()}
	paraInner := fnObj.AddParagraph()
	paraInner.Properties().SetStyle("Footnote")
	paraInner.x.PPr.RPr = wml.NewCT_ParaRPr()

	runInner := paraInner.AddRun()
	runInner.AddTab()
	runInner.AddText(text)

	return fnObj
}

// RemoveFootnote removes a footnote from both the paragraph and the document
// the requested footnote must be anchored on the paragraph being referenced.
func (p Paragraph) RemoveFootnote(id int64) {
	fns := p.d.footNotes
	var i int
	for i1, fn := range fns.CT_Footnotes.Footnote {
		if fn.IdAttr == id {
			i = i1
		}
	}
	i = 0
	fns.CT_Footnotes.Footnote[i] = nil
	fns.CT_Footnotes.Footnote[i] = fns.CT_Footnotes.Footnote[len(fns.CT_Footnotes.Footnote)-1]
	fns.CT_Footnotes.Footnote = fns.CT_Footnotes.Footnote[:len(fns.CT_Footnotes.Footnote)-1]

	var r Run
	for _, r1 := range p.Runs() {
		if ok, fnID := r1.IsFootnote(); ok {
			if fnID == id {
				r = r1
			}
		}
	}
	p.RemoveRun(r)
}

// AddEndnote will create a new endnote and attach it to the Paragraph in the
// location at the end of the previous run (endnotes create their own run within
// the paragraph. The text given to the function is simply a convenience helper,
// paragraphs and runs can always be added to the text of the endnote later.
func (p Paragraph) AddEndnote(text string) Endnote {
	// ensure we use a proper IdAttr for the new endnote
	var enIDHeight int64
	if p.d.HasEndnotes() {
		for _, f := range p.d.Endnotes() {
			if f.id() > enIDHeight {
				enIDHeight = f.id()
			}
		}
		enIDHeight++
	} else {
		enIDHeight = 0
		p.d.endNotes = &wml.Endnotes{}
	}

	// create a new endnote and endnote reference
	en := wml.NewCT_FtnEdn()       // doc.endNotes.CT_Endnotes.Endnote[0]
	enRef := wml.NewCT_FtnEdnRef() // p.Runs()[0].X().EG_RunInnerContent[0].EndnoteReference
	enRef.IdAttr = enIDHeight

	// add new endnote to document
	p.d.endNotes.CT_Endnotes.Endnote = append(p.d.endNotes.CT_Endnotes.Endnote, en)

	// Add the newly created endnote reference to a new run on the parent paragraph
	run := p.AddRun()
	runP := run.Properties()
	runP.SetStyle("EndnoteAnchor")
	run.x.EG_RunInnerContent = []*wml.EG_RunInnerContent{wml.NewEG_RunInnerContent()}
	run.x.EG_RunInnerContent[0].EndnoteReference = enRef

	// formulate the new enobject's inners
	enObj := Endnote{p.d, en}
	enObj.x.IdAttr = enIDHeight
	enObj.x.EG_BlockLevelElts = []*wml.EG_BlockLevelElts{wml.NewEG_BlockLevelElts()}
	paraInner := enObj.AddParagraph()
	paraInner.Properties().SetStyle("Endnote")
	paraInner.x.PPr.RPr = wml.NewCT_ParaRPr()

	runInner := paraInner.AddRun()
	runInner.AddTab()
	runInner.AddText(text)

	return enObj
}

// RemoveEndnote removes a endnote from both the paragraph and the document
// the requested endnote must be anchored on the paragraph being referenced.
func (p Paragraph) RemoveEndnote(id int64) {
	ens := p.d.endNotes
	var i int
	for i1, en := range ens.CT_Endnotes.Endnote {
		if en.IdAttr == id {
			i = i1
		}
	}
	i = 0
	ens.CT_Endnotes.Endnote[i] = nil
	ens.CT_Endnotes.Endnote[i] = ens.CT_Endnotes.Endnote[len(ens.CT_Endnotes.Endnote)-1]
	ens.CT_Endnotes.Endnote = ens.CT_Endnotes.Endnote[:len(ens.CT_Endnotes.Endnote)-1]

	var r Run
	for _, r1 := range p.Runs() {
		if ok, enID := r1.IsEndnote(); ok {
			if enID == id {
				r = r1
			}
		}
	}
	p.RemoveRun(r)
}

// SetNumberingLevel sets the numbering level of a paragraph.  If used, then the
// NumberingDefinition must also be set via SetNumberingDefinition or
// SetNumberingDefinitionByID.
func (p Paragraph) SetNumberingLevel(listLevel int) {
	p.ensurePPr()
	if p.x.PPr.NumPr == nil {
		p.x.PPr.NumPr = wml.NewCT_NumPr()
	}
	lvl := wml.NewCT_DecimalNumber()
	lvl.ValAttr = int64(listLevel)
	p.x.PPr.NumPr.Ilvl = lvl
}

// SetNumberingDefinition sets the numbering definition ID via a NumberingDefinition
// defined in numbering.xml
func (p Paragraph) SetNumberingDefinition(nd NumberingDefinition) {
	p.ensurePPr()
	if p.x.PPr.NumPr == nil {
		p.x.PPr.NumPr = wml.NewCT_NumPr()
	}
	lvl := wml.NewCT_DecimalNumber()

	numID := int64(-1)
	for _, n := range p.d.Numbering.x.Num {
		if n.AbstractNumId != nil && n.AbstractNumId.ValAttr == nd.AbstractNumberID() {
			numID = n.NumIdAttr
		}
	}
	if numID == -1 {
		num := wml.NewCT_Num()
		p.d.Numbering.x.Num = append(p.d.Numbering.x.Num, num)
		num.NumIdAttr = int64(len(p.d.Numbering.x.Num))
		num.AbstractNumId = wml.NewCT_DecimalNumber()
		num.AbstractNumId.ValAttr = nd.AbstractNumberID()
	}

	lvl.ValAttr = numID
	p.x.PPr.NumPr.NumId = lvl
}

// SetNumberingDefinitionByID sets the numbering definition ID directly, which must
// match an ID defined in numbering.xml
func (p Paragraph) SetNumberingDefinitionByID(abstractNumberID int64) {
	p.ensurePPr()
	if p.x.PPr.NumPr == nil {
		p.x.PPr.NumPr = wml.NewCT_NumPr()
	}
	lvl := wml.NewCT_DecimalNumber()
	lvl.ValAttr = int64(abstractNumberID)
	p.x.PPr.NumPr.NumId = lvl
}
