package drawio

import (
	"bufio"
	"errors"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path"
	"strconv"
	"strings"

	xmlparser "github.com/tamerh/xml-stream-parser"
)

// Создать нового экспортёра диаграмм
func New(opt ...Option) Exporter {
	options := &Options{
		Application: "drawio",
		Output:      "export",
		Format:      Format("pdf"),
	}
	for _, opt := range opt {
		opt.apply(options)
	}
	return NewWithOptions(options)
}

// Создать нового экспортёра с параметрами
func NewWithOptions(opts *Options) Exporter {
	exp := Exporter{opts: opts}
	if opts.openFile == nil {
		exp.openFile = func(name string) (io.ReadCloser, error) {
			return os.Open(name)
		}
	} else {
		exp.openFile = opts.openFile
	}
	if opts.readDir == nil {
		exp.readDir = func(name string) ([]os.DirEntry, error) {
			return os.ReadDir(name)
		}
	} else {
		exp.readDir = opts.readDir
	}
	return exp
}

// Экспортёр диаграмм
type Exporter struct {
	opts     *Options     // Параметры экспортёра диаграмм
	openFile OpenFileFunc // Открыть файл
	readDir  ReadDirFunc  // Прочитать папку на диске
}

// Экспорт диаграмм из указанный файлов или папок
func (exp Exporter) Export(fileOrDir ...string) error {
	var (
		commands = []*exec.Cmd{}
		err      error
		errs     = []error{}
		args     = exp.opts.Args()
		info     fs.FileInfo
	)
	for _, item := range fileOrDir {
		if info, err = os.Stat(item); err != nil {
			errs = append(errs, err)
			continue
		}
		var addCommands []*exec.Cmd
		if info.IsDir() {
			addCommands, err = exp.ExportDir(item, args)
		} else {
			addCommands, err = exp.ExportFile(item, args)
		}
		commands = append(commands, addCommands...)
		if err != nil {
			errs = append(errs, err)
		}
	}
	if err = RunSequence(commands...); err != nil {
		errs = append(errs, err)
	}
	return errors.Join(errs...)
}

// Экспорт диаграмм из всех файлов в папке
func (exp Exporter) ExportDir(
	dirPath string, args []string, subDir ...string,
) ([]*exec.Cmd, error) {
	var (
		commands = []*exec.Cmd{}
		err      error
		entries  []fs.DirEntry
		errs     []error
	)
	if entries, err = exp.readDir(dirPath); err != nil {
		return commands, err
	}
	for _, entry := range entries {
		name := entry.Name()
		var addCommands []*exec.Cmd
		if entry.IsDir() {
			if exp.opts.Recursive {
				deep := len(subDir)
				outDir := make([]string, deep+1)
				copy(outDir, subDir)
				outDir[deep] = name
				addCommands, err = exp.ExportDir(path.Join(dirPath, name), args, outDir...)
			} else {
				continue
			}
		} else {
			addCommands, err = exp.ExportFile(path.Join(dirPath, name), args, subDir...)
		}
		commands = append(commands, addCommands...)
		if err != nil {
			errs = append(errs, err)
		}
	}
	return commands, errors.Join(errs...)
}

// Экспорт файла с диаграммами
func (exp Exporter) ExportFile(
	filePath string, args []string, subDir ...string,
) ([]*exec.Cmd, error) {
	var (
		commands = []*exec.Cmd{}
		err      error
		file     io.ReadCloser
		diagrams []string
	)
	if file, err = exp.openFile(filePath); err != nil {
		return commands, err
	}
	defer file.Close()
	if diagrams, err = Diagrams(file); err != nil {
		return commands, err
	}
	outDirs := make([]string, 1, len(subDir)+1)
	outDirs[0] = exp.opts.OutDir()
	outDirs = append(outDirs, subDir...)
	output := path.Join(outDirs...)
	for i, name := range diagrams {
		outName := strings.TrimSuffix(path.Base(filePath), path.Ext(filePath))
		if !exp.opts.RemovePageSuffix || len(diagrams) > 1 {
			outName += "-" + name
		}
		outName += exp.opts.OutExt()
		drawioArgs := make([]string, len(args), len(args)+10)
		copy(drawioArgs, args)
		drawioArgs = append(drawioArgs,
			"--page-index", strconv.Itoa(i),
			"--output", path.Join(output, outName),
			"--export", filePath,
		)
		if exp.opts.EnableXvfb {
			drawioArgs = append(drawioArgs,
				"--disable-gpu", "--headless", "--no-sandbox")
		}
		cmd := exec.Command(exp.opts.App(), drawioArgs...)
		commands = append(commands, cmd)
	}
	return commands, err
}

// Разбирает данные и возвращает срез имён диаграмм в них
func Diagrams(reader io.Reader) ([]string, error) {
	var (
		result = []string{}
		br     = bufio.NewReader(reader)
		parser = xmlparser.NewXMLParser(
			br, "mxfile", "diagram",
		).ParseAttributesOnly("diagram")
	)
	for xml := range parser.Stream() {
		if xml.Err != nil {
			return result, xml.Err
		}
		if items, ok := xml.Childs["diagram"]; ok {
			for _, item := range items {
				result = append(result, item.Attrs["name"])
			}
		}
	}
	return result, nil
}