// SiYuan - Build Your Eternal Digital Garden
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package model

import (
	"errors"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/88250/gulu"
	"github.com/88250/lute/ast"
	"github.com/88250/lute/parse"
	"github.com/siyuan-note/siyuan/kernel/cache"
	"github.com/siyuan-note/siyuan/kernel/sql"
	"github.com/siyuan-note/siyuan/kernel/treenode"
	"github.com/siyuan-note/siyuan/kernel/util"
)

func (tx *Transaction) doFoldHeading(operation *Operation) (ret *TxErr) {
	headingID := operation.ID
	tree, err := loadTreeByBlockID(headingID)
	if nil != err {
		return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
	}

	childrenIDs := []string{} // 这里不能用 nil，否则折叠下方没内容的标题时会内核中断 https://github.com/siyuan-note/siyuan/issues/3643
	heading := treenode.GetNodeInTree(tree, headingID)
	if nil == heading {
		return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
	}

	children := treenode.HeadingChildren(heading)
	for _, child := range children {
		childrenIDs = append(childrenIDs, child.ID)
		child.RemoveIALAttr("fold")
		child.SetIALAttr("heading-fold", "1")
	}
	heading.SetIALAttr("fold", "1")
	if err = tx.writeTree(tree); nil != err {
		return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID}
	}
	IncWorkspaceDataVer()

	cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL))
	for _, child := range children {
		cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL))
	}
	sql.UpsertTreeQueue(tree)
	operation.RetData = childrenIDs
	return
}

func (tx *Transaction) doUnfoldHeading(operation *Operation) (ret *TxErr) {
	headingID := operation.ID

	tree, err := loadTreeByBlockID(headingID)
	if nil != err {
		return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
	}

	heading := treenode.GetNodeInTree(tree, headingID)
	if nil == heading {
		return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
	}

	children := treenode.FoldedHeadingChildren(heading)
	for _, child := range children {
		child.RemoveIALAttr("heading-fold")
		child.RemoveIALAttr("fold")
	}
	heading.RemoveIALAttr("fold")
	if err = tx.writeTree(tree); nil != err {
		return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID}
	}
	IncWorkspaceDataVer()

	cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL))
	for _, child := range children {
		cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL))
	}
	sql.UpsertTreeQueue(tree)

	luteEngine := NewLute()
	operation.RetData = renderBlockDOMByNodes(children, luteEngine)
	return
}

func Doc2Heading(srcID, targetID string, after bool) (srcTreeBox, srcTreePath string, err error) {
	WaitForWritingFiles()

	srcTree, _ := loadTreeByBlockID(srcID)
	if nil == srcTree {
		err = ErrBlockNotFound
		return
	}

	subDir := filepath.Join(util.DataDir, srcTree.Box, strings.TrimSuffix(srcTree.Path, ".sy"))
	if gulu.File.IsDir(subDir) {
		if !util.IsEmptyDir(subDir) {
			err = errors.New(Conf.Language(20))
			return
		} else {
			os.Remove(subDir) // 移除空文件夹不会有副作用
		}
	}

	targetTree, _ := loadTreeByBlockID(targetID)
	if nil == targetTree {
		err = ErrBlockNotFound
		return
	}

	pivot := treenode.GetNodeInTree(targetTree, targetID)
	if nil == pivot {
		err = ErrBlockNotFound
		return
	}

	if ast.NodeListItem == pivot.Type {
		pivot = pivot.LastChild
	}

	pivotLevel := treenode.HeadingLevel(pivot)
	deltaLevel := pivotLevel - treenode.TopHeadingLevel(srcTree) + 1
	headingLevel := pivotLevel
	if ast.NodeHeading == pivot.Type { // 平级插入
		children := treenode.HeadingChildren(pivot)
		if after {
			if length := len(children); 0 < length {
				pivot = children[length-1]
			}
		}
	} else { // 子节点插入
		headingLevel++
		deltaLevel++
	}
	if 6 < headingLevel {
		headingLevel = 6
	}

	srcTree.Root.RemoveIALAttr("type")
	heading := &ast.Node{ID: srcTree.Root.ID, Type: ast.NodeHeading, HeadingLevel: headingLevel, KramdownIAL: srcTree.Root.KramdownIAL}
	heading.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(srcTree.Root.IALAttr("title"))})
	heading.Box = targetTree.Box
	heading.Path = targetTree.Path

	var nodes []*ast.Node
	if after {
		for c := srcTree.Root.LastChild; nil != c; c = c.Previous {
			nodes = append(nodes, c)
		}
	} else {
		for c := srcTree.Root.FirstChild; nil != c; c = c.Next {
			nodes = append(nodes, c)
		}
	}

	if !after {
		pivot.InsertBefore(heading)
	}

	for _, n := range nodes {
		if ast.NodeHeading == n.Type {
			n.HeadingLevel = n.HeadingLevel + deltaLevel
			if 6 < n.HeadingLevel {
				n.HeadingLevel = 6
			}
		}
		n.Box = targetTree.Box
		n.Path = targetTree.Path
		if after {
			pivot.InsertAfter(n)
		} else {
			pivot.InsertBefore(n)
		}
	}

	if after {
		pivot.InsertAfter(heading)
	}

	if contentPivot := treenode.GetNodeInTree(targetTree, targetID); nil != contentPivot && ast.NodeParagraph == contentPivot.Type && nil == contentPivot.FirstChild { // 插入到空的段落块下
		contentPivot.Unlink()
	}

	srcTreeBox, srcTreePath = srcTree.Box, srcTree.Path
	srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
	if err = indexWriteJSONQueue(srcTree); nil != err {
		return
	}

	targetTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
	err = indexWriteJSONQueue(targetTree)
	IncWorkspaceDataVer()
	RefreshBacklink(srcTree.ID)
	RefreshBacklink(targetTree.ID)
	return
}

func Heading2Doc(srcHeadingID, targetBoxID, targetPath string) (srcRootBlockID, newTargetPath string, err error) {
	WaitForWritingFiles()

	srcTree, _ := loadTreeByBlockID(srcHeadingID)
	if nil == srcTree {
		err = ErrBlockNotFound
		return
	}
	srcRootBlockID = srcTree.Root.ID

	headingBlock, err := getBlock(srcHeadingID)
	if nil != err {
		return
	}
	if nil == headingBlock {
		err = ErrBlockNotFound
		return
	}
	headingNode := treenode.GetNodeInTree(srcTree, srcHeadingID)
	if nil == headingNode {
		err = ErrBlockNotFound
		return
	}

	box := Conf.Box(targetBoxID)
	headingText := sql.GetRefText(headingNode.ID)
	headingText = util.FilterFileName(headingText)

	moveToRoot := "/" == targetPath
	toHP := path.Join("/", headingText)
	toFolder := "/"

	if !moveToRoot {
		toBlock := treenode.GetBlockTreeRootByPath(targetBoxID, targetPath)
		if nil == toBlock {
			err = ErrBlockNotFound
			return
		}
		toHP = path.Join(toBlock.HPath, headingText)
		toFolder = path.Join(path.Dir(targetPath), toBlock.ID)
	}

	newTargetPath = path.Join(toFolder, srcHeadingID+".sy")
	if !box.Exist(toFolder) {
		if err = box.MkdirAll(toFolder); nil != err {
			return
		}
	}

	// 折叠标题转换为文档时需要自动展开下方块 https://github.com/siyuan-note/siyuan/issues/2947
	children := treenode.FoldedHeadingChildren(headingNode)
	for _, child := range children {
		child.RemoveIALAttr("heading-fold")
		child.RemoveIALAttr("fold")
	}
	headingNode.RemoveIALAttr("fold")

	luteEngine := NewLute()
	newTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument, ID: srcHeadingID}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
	children = treenode.HeadingChildren(headingNode)
	for _, c := range children {
		newTree.Root.AppendChild(c)
	}
	newTree.ID = srcHeadingID
	newTree.Path = newTargetPath
	newTree.HPath = toHP
	headingNode.SetIALAttr("type", "doc")
	headingNode.SetIALAttr("id", srcHeadingID)
	headingNode.SetIALAttr("title", headingText)
	newTree.Root.KramdownIAL = headingNode.KramdownIAL

	topLevel := treenode.TopHeadingLevel(newTree)
	for c := newTree.Root.FirstChild; nil != c; c = c.Next {
		if ast.NodeHeading == c.Type {
			c.HeadingLevel = c.HeadingLevel - topLevel + 1
			if 6 < c.HeadingLevel {
				c.HeadingLevel = 6
			}
		}
	}

	headingNode.Unlink()
	srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
	if err = indexWriteJSONQueue(srcTree); nil != err {
		return "", "", err
	}

	newTree.Box, newTree.Path = targetBoxID, newTargetPath
	newTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
	if err = indexWriteJSONQueue(newTree); nil != err {
		return "", "", err
	}
	IncWorkspaceDataVer()
	RefreshBacklink(srcTree.ID)
	RefreshBacklink(newTree.ID)
	return
}
