// Copyright 2021 Google Inc. 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.

// merge_module_info_json is a utility that merges module_info.json files generated by
// Soong and Make.

package main

import (
	"android/soong/response"
	"cmp"
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"slices"
)

var (
	out      = flag.String("o", "", "output file")
	listFile = flag.String("l", "", "input file list file")
)

func usage() {
	fmt.Fprintf(os.Stderr, "usage: %s -o <output file> <input files>\n", os.Args[0])
	flag.PrintDefaults()
	fmt.Fprintln(os.Stderr, "merge_module_info_json reads input files that each contain an array of json objects")
	fmt.Fprintln(os.Stderr, "and writes them out as a single json array to the output file.")

	os.Exit(2)
}

func main() {
	flag.Usage = usage
	flag.Parse()

	if *out == "" {
		fmt.Fprintf(os.Stderr, "%s: error: -o is required\n", os.Args[0])
		usage()
	}

	inputs := flag.Args()
	if *listFile != "" {
		listFileInputs, err := readListFile(*listFile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed to read list file %s: %s", *listFile, err)
			os.Exit(1)
		}
		inputs = append(inputs, listFileInputs...)
	}

	err := mergeJsonObjects(*out, inputs)

	if err != nil {
		fmt.Fprintf(os.Stderr, "%s: error: %s\n", os.Args[0], err.Error())
		os.Exit(1)
	}
}

func readListFile(file string) ([]string, error) {
	f, err := os.Open(*listFile)
	if err != nil {
		return nil, err
	}
	return response.ReadRspFile(f)
}

func mergeJsonObjects(output string, inputs []string) error {
	combined := make(map[string]any)
	for _, input := range inputs {
		objects, err := decodeObjectFromJson(input)
		if err != nil {
			return err
		}

		for _, object := range objects {
			for k, v := range object {
				if old, exists := combined[k]; exists {
					v = combine(old, v)
				}
				combined[k] = v
			}
		}
	}

	f, err := os.Create(output)
	if err != nil {
		return fmt.Errorf("failed to open output file: %w", err)
	}
	encoder := json.NewEncoder(f)
	encoder.SetIndent("", "  ")
	err = encoder.Encode(combined)
	if err != nil {
		return fmt.Errorf("failed to encode to output file: %w", err)
	}

	return nil
}

func decodeObjectFromJson(input string) ([]map[string]any, error) {
	f, err := os.Open(input)
	if err != nil {
		return nil, fmt.Errorf("failed to open input file: %w", err)
	}

	decoder := json.NewDecoder(f)
	var object any
	err = decoder.Decode(&object)
	if err != nil {
		return nil, fmt.Errorf("failed to parse input file %q: %w", input, err)
	}

	switch o := object.(type) {
	case []any:
		var ret []map[string]any
		for _, arrayElement := range o {
			if m, ok := arrayElement.(map[string]any); ok {
				ret = append(ret, m)
			} else {
				return nil, fmt.Errorf("unknown JSON type in array %T", arrayElement)
			}
		}
		return ret, nil

	case map[string]any:
		return []map[string]any{o}, nil
	}

	return nil, fmt.Errorf("unknown JSON type %T", object)
}

func combine(old, new any) any {
	//	fmt.Printf("%#v %#v\n", old, new)
	switch oldTyped := old.(type) {
	case map[string]any:
		if newObject, ok := new.(map[string]any); ok {
			return combineObjects(oldTyped, newObject)
		} else {
			panic(fmt.Errorf("expected map[string]any, got %#v", new))
		}
	case []any:
		if newArray, ok := new.([]any); ok {
			return combineArrays(oldTyped, newArray)
		} else {
			panic(fmt.Errorf("expected []any, got %#v", new))
		}
	case string:
		if newString, ok := new.(string); ok {
			if oldTyped != newString {
				panic(fmt.Errorf("strings %q and %q don't match", oldTyped, newString))
			}
			return oldTyped
		} else {
			panic(fmt.Errorf("expected []any, got %#v", new))
		}
	default:
		panic(fmt.Errorf("can't combine type %T", old))
	}
}

func combineObjects(old, new map[string]any) map[string]any {
	for k, newField := range new {
		// HACK: Don't merge "test_config" field.  This matches the behavior in base_rules.mk that overwrites
		// instead of appending ALL_MODULES.$(my_register_name).TEST_CONFIG, keeping the
		if k == "test_config" {
			old[k] = newField
			continue
		}
		if oldField, exists := old[k]; exists {
			oldField = combine(oldField, newField)
			old[k] = oldField
		} else {
			old[k] = newField
		}
	}

	return old
}

func combineArrays(old, new []any) []any {
	containsNonStrings := false
	for _, oldElement := range old {
		switch oldElement.(type) {
		case string:
		default:
			containsNonStrings = true
		}
	}
	for _, newElement := range new {
		found := false
		for _, oldElement := range old {
			if oldElement == newElement {
				found = true
				break
			}
		}
		if !found {
			switch newElement.(type) {
			case string:
			default:
				containsNonStrings = true
			}
			old = append(old, newElement)
		}
	}
	if !containsNonStrings {
		slices.SortFunc(old, func(a, b any) int {
			return cmp.Compare(a.(string), b.(string))
		})
	}
	return old
}
