/* Copyright 2017 The Bazel Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package rule

import (
	"sort"
	"strings"

	bzl "github.com/bazelbuild/buildtools/build"
)

// PlatformConstraint represents a constraint_setting target for a particular
// OS/arch combination.
//
// DEPRECATED: do not use outside language/go.
type PlatformConstraint struct {
	Platform
	ConstraintPrefix string
}

func (p PlatformConstraint) String() string {
	pStr := p.Platform.String()
	if pStr == "" {
		return ""
	}
	return p.ConstraintPrefix + pStr
}

// PlatformStrings contains a set of strings associated with a buildable
// target in a package. This is used to store source file names,
// import paths, and flags.
//
// Strings are stored in four sets: generic strings, OS-specific strings,
// arch-specific strings, and OS-and-arch-specific strings. A string may not
// be duplicated within a list or across sets; however, a string may appear
// in more than one list within a set (e.g., in "linux" and "windows" within
// the OS set). Strings within each list should be sorted, though this may
// not be relied upon.
//
// DEPRECATED: do not use outside language/go. This type is Go-specific and
// should be moved to the Go extension.
type PlatformStrings struct {
	// Generic is a list of strings not specific to any platform.
	Generic []string

	// OS is a map from an OS constraint to OS-specific strings.
	OS map[string][]string

	// Arch is a map from an architecture constraint to
	// architecture-specific strings.
	Arch map[string][]string

	// Platform is a map from platform constraints to OS and
	// architecture-specific strings.
	Platform map[PlatformConstraint][]string
}

var _ BzlExprValue = (*PlatformStrings)(nil)

// HasExt returns whether this set contains a file with the given extension.
func (ps *PlatformStrings) HasExt(ext string) bool {
	return ps.firstExtFile(ext) != ""
}

func (ps *PlatformStrings) IsEmpty() bool {
	return len(ps.Generic) == 0 && len(ps.OS) == 0 && len(ps.Arch) == 0 && len(ps.Platform) == 0
}

// Flat returns all the strings in the set, sorted and de-duplicated.
func (ps *PlatformStrings) Flat() []string {
	unique := make(map[string]struct{})
	// TODO: refactor to for-iterator loop after Go 1.23 is the minimum version.
	ps.Each()(func(s string) bool {
		unique[s] = struct{}{}
		return true
	})
	flat := make([]string, 0, len(unique))
	for s := range unique {
		flat = append(flat, s)
	}
	sort.Strings(flat)
	return flat
}

func (ps *PlatformStrings) firstExtFile(ext string) string {
	for _, f := range ps.Generic {
		if strings.HasSuffix(f, ext) {
			return f
		}
	}
	for _, fs := range ps.OS {
		for _, f := range fs {
			if strings.HasSuffix(f, ext) {
				return f
			}
		}
	}
	for _, fs := range ps.Arch {
		for _, f := range fs {
			if strings.HasSuffix(f, ext) {
				return f
			}
		}
	}
	for _, fs := range ps.Platform {
		for _, f := range fs {
			if strings.HasSuffix(f, ext) {
				return f
			}
		}
	}
	return ""
}

// Map applies a function that processes individual strings to the strings
// in "ps" and returns a new PlatformStrings with the result. Empty strings
// returned by the function are dropped.
func (ps *PlatformStrings) Map(f func(s string) (string, error)) (PlatformStrings, []error) {
	var errors []error
	mapSlice := func(ss []string) ([]string, error) {
		rs := make([]string, 0, len(ss))
		for _, s := range ss {
			if r, err := f(s); err != nil {
				errors = append(errors, err)
			} else if r != "" {
				rs = append(rs, r)
			}
		}
		return rs, nil
	}
	result, _ := ps.MapSlice(mapSlice)
	return result, errors
}

// MapSlice applies a function that processes slices of strings to the strings
// in "ps" and returns a new PlatformStrings with the results.
func (ps *PlatformStrings) MapSlice(f func([]string) ([]string, error)) (PlatformStrings, []error) {
	var errors []error

	mapSlice := func(ss []string) []string {
		rs, err := f(ss)
		if err != nil {
			errors = append(errors, err)
			return nil
		}
		return rs
	}

	mapStringMap := func(m map[string][]string) map[string][]string {
		if m == nil {
			return nil
		}
		rm := make(map[string][]string)
		for k, ss := range m {
			ss = mapSlice(ss)
			if len(ss) > 0 {
				rm[k] = ss
			}
		}
		if len(rm) == 0 {
			return nil
		}
		return rm
	}

	mapPlatformMap := func(m map[PlatformConstraint][]string) map[PlatformConstraint][]string {
		if m == nil {
			return nil
		}
		rm := make(map[PlatformConstraint][]string)
		for k, ss := range m {
			ss = mapSlice(ss)
			if len(ss) > 0 {
				rm[k] = ss
			}
		}
		if len(rm) == 0 {
			return nil
		}
		return rm
	}

	result := PlatformStrings{
		Generic:  mapSlice(ps.Generic),
		OS:       mapStringMap(ps.OS),
		Arch:     mapStringMap(ps.Arch),
		Platform: mapPlatformMap(ps.Platform),
	}
	return result, errors
}

// Each returns an iterator (satisfying iter.Seq) over the strings in
// PlatformStrings. No sorting or deduplication is performed here.
func (ps *PlatformStrings) Each() func(yield func(string) bool) {
	return func(yield func(string) bool) {
		for _, s := range ps.Generic {
			if !yield(s) {
				return
			}
		}
		for _, ss := range ps.OS {
			for _, s := range ss {
				if !yield(s) {
					return
				}
			}
		}
		for _, ss := range ps.Arch {
			for _, s := range ss {
				if !yield(s) {
					return
				}
			}
		}
		for _, ss := range ps.Platform {
			for _, s := range ss {
				if !yield(s) {
					return
				}
			}
		}
	}
}

func (ps PlatformStrings) BzlExpr() bzl.Expr {
	var pieces []bzl.Expr
	if len(ps.Generic) > 0 {
		pieces = append(pieces, ExprFromValue(ps.Generic))
	}
	if len(ps.OS) > 0 {
		pieces = append(pieces, platformStringsOSArchDictExpr(ps.OS))
	}
	if len(ps.Arch) > 0 {
		pieces = append(pieces, platformStringsOSArchDictExpr(ps.Arch))
	}
	if len(ps.Platform) > 0 {
		pieces = append(pieces, platformStringsPlatformDictExpr(ps.Platform))
	}
	if len(pieces) == 0 {
		return &bzl.ListExpr{}
	} else if len(pieces) == 1 {
		return pieces[0]
	} else {
		e := pieces[0]
		if list, ok := e.(*bzl.ListExpr); ok {
			list.ForceMultiLine = true
		}
		for _, piece := range pieces[1:] {
			e = &bzl.BinaryExpr{X: e, Y: piece, Op: "+"}
		}
		return e
	}
}

func platformStringsOSArchDictExpr(m map[string][]string) bzl.Expr {
	s := make(SelectStringListValue)
	for key, value := range m {
		s[key] = value
	}
	s["//conditions:default"] = nil
	return s.BzlExpr()
}

func platformStringsPlatformDictExpr(m map[PlatformConstraint][]string) bzl.Expr {
	s := make(SelectStringListValue)
	for key, value := range m {
		s[key.String()] = value
	}
	s["//conditions:default"] = nil
	return s.BzlExpr()
}
