// goredo -- djb's redo implementation on pure Go
// Copyright (C) 2020-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 main

import (
	"strings"
)

func collectDeps(
	tgt *Tgt,
	level int,
	deps map[string]map[string]*Tgt,
	includeSrc bool,
) []*Tgt {
	if _, ok := DepCache[tgt.rel]; ok {
		return nil
	}
	dep, err := depRead(tgt)
	if err != nil {
		return nil
	}
	DepCache[tgt.rel] = dep
	var alwayses []*Tgt
	returnReady := false
	if dep.always {
		if dep.build == BuildUUID {
			tracef(CDebug, "ood: %s%s always, but already build",
				strings.Repeat(". ", level), tgt)
			returnReady = true
		} else {
			tracef(CDebug, "ood: %s%s always", strings.Repeat(". ", level), tgt)
			alwayses = append(alwayses, tgt)
			returnReady = true
		}
	}
	for _, ifchange := range dep.ifchanges {
		if ifchange.tgt.rel == tgt.rel {
			continue
		}
		if !includeSrc && isSrc(ifchange.tgt) {
			continue
		}
		if !returnReady {
			if m, ok := deps[ifchange.tgt.rel]; ok {
				m[tgt.rel] = tgt
			} else {
				deps[ifchange.tgt.rel] = map[string]*Tgt{tgt.rel: tgt}
			}
			alwayses = append(alwayses,
				collectDeps(ifchange.tgt, level+1, deps, includeSrc)...)
		}
	}
	return alwayses
}

func buildDependants(tgts []*Tgt) map[string]*Tgt {
	defer Jobs.Wait()
	tracef(CDebug, "collecting deps")
	seen := make(map[string]*Tgt)
	deps := make(map[string]map[string]*Tgt)
	for _, tgtInitial := range tgts {
		for _, tgt := range collectDeps(tgtInitial, 0, deps, false) {
			if tgt.rel != tgtInitial.rel {
				seen[tgt.rel] = tgt
			}
		}
	}
	TgtCache = make(map[string]*Tgt)
	IfchangeCache = nil
	if len(seen) == 0 {
		return seen
	}

	levelOrig := Level
	defer func() { Level = levelOrig }()
	Level = 1
	tracef(CDebug, "building %d alwayses: %v", len(seen), seen)
	errs := make(chan error, len(seen))
	ok := true
	okChecker := make(chan struct{})
	go func() {
		for err := range errs {
			ok = isOkRun(err) && ok
		}
		close(okChecker)
	}()
	for _, tgt := range seen {
		if err := runScript(tgt, errs, false, false); err != nil {
			tracef(CErr, "always run error: %s, skipping dependants", err)
			Jobs.Wait()
			close(errs)
			return nil
		}
	}
	Jobs.Wait()
	close(errs)
	<-okChecker
	if !ok {
		tracef(CDebug, "alwayses failed, skipping dependants")
		return nil
	}

	queueSrc := make([]*Tgt, 0, len(seen))
	for _, tgt := range seen {
		queueSrc = append(queueSrc, tgt)
	}

RebuildDeps:
	tracef(CDebug, "checking %d dependant targets: %v", len(queueSrc), queueSrc)
	queue := make(map[string]*Tgt)
	for _, tgt := range queueSrc {
		for _, dep := range deps[tgt.rel] {
			queue[dep.rel] = dep
		}
	}

	tracef(CDebug, "building %d dependant targets: %v", len(queue), queue)
	errs = make(chan error, len(queue))
	okChecker = make(chan struct{})
	jobs := 0
	queueSrc = make([]*Tgt, 0)
	go func() {
		for err := range errs {
			ok = isOkRun(err) && ok
		}
		close(okChecker)
	}()
	for _, tgt := range queue {
		ood, err := isOODWithTrace(tgt, 0, seen)
		if err != nil {
			tracef(CErr, "dependant error: %s, skipping dependants", err)
			return nil
		}
		if !ood {
			continue
		}
		if err := runScript(tgt, errs, false, false); err != nil {
			tracef(CErr, "dependant error: %s, skipping dependants", err)
			return nil
		}
		queueSrc = append(queueSrc, tgt)
		seen[tgt.rel] = tgt
		jobs++
	}
	Jobs.Wait()
	close(errs)
	<-okChecker
	if !ok {
		tracef(CDebug, "dependants failed, skipping them")
		return nil
	}
	if jobs == 0 {
		return seen
	}
	Level++
	goto RebuildDeps
}

func ifchange(tgts []*Tgt, forced, traced bool) (bool, error) {
	{
		// only unique elements
		m := make(map[string]*Tgt)
		for _, t := range tgts {
			m[t.a] = t
		}
		tgts = tgts[:0]
		for _, t := range m {
			tgts = append(tgts, t)
		}
	}

	jsInit()
	if !IsTopRedo {
		defer jsAcquire("ifchange exiting")
	}
	seen := buildDependants(tgts)
	if seen == nil {
		Jobs.Wait()
		return false, nil
	}
	oodTgtsClear()
	tracef(CDebug, "building %d targets: %v", len(tgts), tgts)
	var ood bool
	var err error
	ok := true
	okChecker := make(chan struct{})
	errs := make(chan error, len(tgts))
	go func() {
		for err := range errs {
			ok = isOkRun(err) && ok
		}
		close(okChecker)
	}()
	for _, tgt := range tgts {
		if _, exists := seen[tgt.rel]; exists {
			tracef(CDebug, "%s was already build as a dependant", tgt)
			continue
		}
		ood = true
		if !forced {
			ood, err = isOODWithTrace(tgt, 0, seen)
			if err != nil {
				Jobs.Wait()
				close(errs)
				return false, ErrLine(err)
			}
		}
		if !ood {
			continue
		}
		if isSrc(tgt) {
			tracef(CDebug, "%s is source, not redoing", tgt)
			continue
		}
		if err = runScript(tgt, errs, forced, traced); err != nil {
			Jobs.Wait()
			close(errs)
			return false, ErrLine(err)
		}
	}
	Jobs.Wait()
	close(errs)
	<-okChecker
	return ok, nil
}
