// meta4ra -- Metalink 4.0 utilities
// Copyright (C) 2021-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package internal

import (
	"bytes"
	"errors"
	"io"
	"os/exec"
	"strings"
)

type Hasher struct {
	Names []string
	Cmds  []*exec.Cmd
	Ins   []io.WriteCloser
	Outs  []io.ReadCloser
}

func NewHasher(hashers string) (*Hasher, error) {
	h := Hasher{}
	for hc := range strings.SplitSeq(hashers, "\n") {
		if hc == "" {
			continue
		}
		cols := strings.SplitN(hc, " ", 2)
		name, cmdline := cols[0], cols[1]
		if cmdline == BuiltinCmd {
			newHash, exists := BuiltinHashers[name]
			if !exists {
				return nil, errors.New("no builtin hash: " + name)
			}
			b := &BuiltinHasher{h: newHash()}
			h.Names = append(h.Names, name)
			h.Ins = append(h.Ins, b)
			h.Outs = append(h.Outs, b)
			h.Cmds = append(h.Cmds, nil)
		} else {
			cmd := exec.Command("/bin/sh", "-e", "-c", cmdline)
			in, err := cmd.StdinPipe()
			if err != nil {
				return &h, err
			}
			out, err := cmd.StdoutPipe()
			if err != nil {
				return &h, err
			}
			h.Names = append(h.Names, name)
			h.Ins = append(h.Ins, in)
			h.Outs = append(h.Outs, out)
			h.Cmds = append(h.Cmds, cmd)
		}
	}
	return &h, nil
}

func (h *Hasher) Start() (err error) {
	for _, cmd := range h.Cmds {
		if cmd == nil {
			continue
		}
		err = cmd.Start()
		if err != nil {
			return
		}
	}
	return nil
}

func (h *Hasher) Stop() {
	for _, cmd := range h.Cmds {
		if cmd == nil || cmd.Process == nil {
			continue
		}
		cmd.Process.Kill()
		cmd.Process.Release()
	}
}

func (h *Hasher) Write(p []byte) (n int, rerr error) {
	errs := make(chan error)
	for _, in := range h.Ins {
		go func(in io.WriteCloser) {
			_, err := io.Copy(in, bytes.NewReader(p))
			errs <- err
		}(in)
	}
	for range len(h.Names) {
		if err := <-errs; err != nil {
			rerr = err
		}
	}
	n = len(p)
	return
}

func (h *Hasher) Sums() (sums []Hash, err error) {
	sums = make([]Hash, 0, len(h.Names))
	for i, name := range h.Names {
		if err = h.Ins[i].Close(); err != nil {
			return
		}
		var out []byte
		out, err = io.ReadAll(h.Outs[i])
		if err != nil {
			return
		}
		if cmd := h.Cmds[i]; cmd != nil {
			if err = h.Cmds[i].Wait(); err != nil {
				return
			}
		}
		cols := strings.Fields(strings.TrimSuffix(string(out), "\n"))
		sums = append(sums, Hash{Type: name, Hash: cols[0]})
	}
	return
}
