// Copyright 2017 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.

package main

import (
	"errors"
	"flag"
	"fmt"
	"hash/crc32"
	"io"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"android/soong/response"

	"github.com/google/blueprint/pathtools"

	"android/soong/jar"
	"android/soong/third_party/zip"
)

// Input zip: we can open it, close it, and obtain an array of entries
type InputZip interface {
	Name() string
	Open() error
	Close() error
	Entries() []*zip.File
	IsOpen() bool
}

// An entry that can be written to the output zip
type ZipEntryContents interface {
	String() string
	IsDir() bool
	CRC32() uint32
	Size() uint64
	WriteToZip(dest string, zw *zip.Writer) error
}

// a ZipEntryFromZip is a ZipEntryContents that pulls its content from another zip
// identified by the input zip and the index of the entry in its entries array
type ZipEntryFromZip struct {
	inputZip InputZip
	index    int
	name     string
	isDir    bool
	crc32    uint32
	size     uint64
}

func NewZipEntryFromZip(inputZip InputZip, entryIndex int) *ZipEntryFromZip {
	fi := inputZip.Entries()[entryIndex]
	newEntry := ZipEntryFromZip{inputZip: inputZip,
		index: entryIndex,
		name:  fi.Name,
		isDir: fi.FileInfo().IsDir(),
		crc32: fi.CRC32,
		size:  fi.UncompressedSize64,
	}
	return &newEntry
}

func (ze ZipEntryFromZip) String() string {
	return fmt.Sprintf("%s!%s", ze.inputZip.Name(), ze.name)
}

func (ze ZipEntryFromZip) IsDir() bool {
	return ze.isDir
}

func (ze ZipEntryFromZip) CRC32() uint32 {
	return ze.crc32
}

func (ze ZipEntryFromZip) Size() uint64 {
	return ze.size
}

func (ze ZipEntryFromZip) WriteToZip(dest string, zw *zip.Writer) error {
	if err := ze.inputZip.Open(); err != nil {
		return err
	}
	entry := ze.inputZip.Entries()[ze.index]
	entry.SetModTime(jar.DefaultTime)
	return zw.CopyFrom(entry, dest)
}

// a ZipEntryFromBuffer is a ZipEntryContents that pulls its content from a []byte
type ZipEntryFromBuffer struct {
	fh      *zip.FileHeader
	content []byte
}

func (be ZipEntryFromBuffer) String() string {
	return "internal buffer"
}

func (be ZipEntryFromBuffer) IsDir() bool {
	return be.fh.FileInfo().IsDir()
}

func (be ZipEntryFromBuffer) CRC32() uint32 {
	return crc32.ChecksumIEEE(be.content)
}

func (be ZipEntryFromBuffer) Size() uint64 {
	return uint64(len(be.content))
}

func (be ZipEntryFromBuffer) WriteToZip(dest string, zw *zip.Writer) error {
	w, err := zw.CreateHeaderAndroid(be.fh)
	if err != nil {
		return err
	}

	if !be.IsDir() {
		_, err = w.Write(be.content)
		if err != nil {
			return err
		}
	}

	return nil
}

// Processing state.
type OutputZip struct {
	outputWriter     *zip.Writer
	stripDirEntries  bool
	emulateJar       bool
	sortEntries      bool
	ignoreDuplicates bool
	excludeDirs      []string
	excludeFiles     []string
	sourceByDest     map[string]ZipEntryContents
}

func NewOutputZip(outputWriter *zip.Writer, sortEntries, emulateJar, stripDirEntries, ignoreDuplicates bool) *OutputZip {
	return &OutputZip{
		outputWriter:     outputWriter,
		stripDirEntries:  stripDirEntries,
		emulateJar:       emulateJar,
		sortEntries:      sortEntries,
		sourceByDest:     make(map[string]ZipEntryContents, 0),
		ignoreDuplicates: ignoreDuplicates,
	}
}

func (oz *OutputZip) setExcludeDirs(excludeDirs []string) {
	oz.excludeDirs = make([]string, len(excludeDirs))
	for i, dir := range excludeDirs {
		oz.excludeDirs[i] = filepath.Clean(dir)
	}
}

func (oz *OutputZip) setExcludeFiles(excludeFiles []string) {
	oz.excludeFiles = excludeFiles
}

// Adds an entry with given name whose source is given ZipEntryContents. Returns old ZipEntryContents
// if entry with given name already exists.
func (oz *OutputZip) addZipEntry(name string, source ZipEntryContents) (ZipEntryContents, error) {
	if existingSource, exists := oz.sourceByDest[name]; exists {
		return existingSource, nil
	}
	oz.sourceByDest[name] = source
	// Delay writing an entry if entries need to be rearranged.
	if oz.emulateJar || oz.sortEntries {
		return nil, nil
	}
	return nil, source.WriteToZip(name, oz.outputWriter)
}

// Adds an entry for the manifest (META-INF/MANIFEST.MF from the given file
func (oz *OutputZip) addManifest(manifestPath string) error {
	if !oz.stripDirEntries {
		if _, err := oz.addZipEntry(jar.MetaDir, ZipEntryFromBuffer{jar.MetaDirFileHeader(), nil}); err != nil {
			return err
		}
	}
	contents, err := ioutil.ReadFile(manifestPath)
	if err == nil {
		fh, buf, err := jar.ManifestFileContents(contents)
		if err == nil {
			_, err = oz.addZipEntry(jar.ManifestFile, ZipEntryFromBuffer{fh, buf})
		}
	}
	return err
}

// Adds an entry with given name and contents read from given file
func (oz *OutputZip) addZipEntryFromFile(name string, path string) error {
	buf, err := ioutil.ReadFile(path)
	if err == nil {
		fh := &zip.FileHeader{
			Name:               name,
			Method:             zip.Store,
			UncompressedSize64: uint64(len(buf)),
		}
		fh.SetMode(0700)
		fh.SetModTime(jar.DefaultTime)
		_, err = oz.addZipEntry(name, ZipEntryFromBuffer{fh, buf})
	}
	return err
}

func (oz *OutputZip) addEmptyEntry(entry string) error {
	var emptyBuf []byte
	fh := &zip.FileHeader{
		Name:               entry,
		Method:             zip.Store,
		UncompressedSize64: uint64(len(emptyBuf)),
	}
	fh.SetMode(0700)
	fh.SetModTime(jar.DefaultTime)
	_, err := oz.addZipEntry(entry, ZipEntryFromBuffer{fh, emptyBuf})
	return err
}

// Returns true if given entry is to be excluded
func (oz *OutputZip) isEntryExcluded(name string) bool {
	for _, dir := range oz.excludeDirs {
		dir = filepath.Clean(dir)
		patterns := []string{
			dir + "/",      // the directory itself
			dir + "/**/*",  // files recursively in the directory
			dir + "/**/*/", // directories recursively in the directory
		}

		for _, pattern := range patterns {
			match, err := pathtools.Match(pattern, name)
			if err != nil {
				panic(fmt.Errorf("%s: %s", err.Error(), pattern))
			}
			if match {
				if oz.emulateJar {
					// When merging jar files, don't strip META-INF/MANIFEST.MF even if stripping META-INF is
					// requested.
					// TODO(ccross): which files does this affect?
					if name != jar.MetaDir && name != jar.ManifestFile {
						return true
					}
				}
				return true
			}
		}
	}

	for _, pattern := range oz.excludeFiles {
		match, err := pathtools.Match(pattern, name)
		if err != nil {
			panic(fmt.Errorf("%s: %s", err.Error(), pattern))
		}
		if match {
			return true
		}
	}
	return false
}

// Creates a zip entry whose contents is an entry from the given input zip.
func (oz *OutputZip) copyEntry(inputZip InputZip, index int) error {
	entry := NewZipEntryFromZip(inputZip, index)
	if oz.stripDirEntries && entry.IsDir() {
		return nil
	}
	existingEntry, err := oz.addZipEntry(entry.name, entry)
	if err != nil {
		return err
	}
	if existingEntry == nil {
		return nil
	}

	// File types should match
	if existingEntry.IsDir() != entry.IsDir() {
		return fmt.Errorf("Directory/file mismatch at %v from %v and %v\n",
			entry.name, existingEntry, entry)
	}

	if oz.ignoreDuplicates ||
		// Skip manifest and module info files that are not from the first input file
		(oz.emulateJar && entry.name == jar.ManifestFile || entry.name == jar.ModuleInfoClass) ||
		// Identical entries
		(existingEntry.CRC32() == entry.CRC32() && existingEntry.Size() == entry.Size()) ||
		// Directory entries
		entry.IsDir() {
		return nil
	}

	return fmt.Errorf("Duplicate path %v found in %v and %v\n", entry.name, existingEntry, inputZip.Name())
}

func (oz *OutputZip) entriesArray() []string {
	entries := make([]string, len(oz.sourceByDest))
	i := 0
	for entry := range oz.sourceByDest {
		entries[i] = entry
		i++
	}
	return entries
}

func (oz *OutputZip) jarSorted() []string {
	entries := oz.entriesArray()
	sort.SliceStable(entries, func(i, j int) bool { return jar.EntryNamesLess(entries[i], entries[j]) })
	return entries
}

func (oz *OutputZip) alphanumericSorted() []string {
	entries := oz.entriesArray()
	sort.Strings(entries)
	return entries
}

func (oz *OutputZip) writeEntries(entries []string) error {
	for _, entry := range entries {
		source, _ := oz.sourceByDest[entry]
		if err := source.WriteToZip(entry, oz.outputWriter); err != nil {
			return err
		}
	}
	return nil
}

func (oz *OutputZip) getUninitializedPythonPackages(inputZips []InputZip) ([]string, error) {
	// the runfiles packages needs to be populated with "__init__.py".
	// the runfiles dirs have been treated as packages.
	var allPackages []string // Using a slice to preserve input order.
	seenPkgs := make(map[string]bool)
	initedPackages := make(map[string]bool)
	getPackage := func(path string) string {
		ret := filepath.Dir(path)
		// filepath.Dir("abc") -> "." and filepath.Dir("/abc") -> "/".
		if ret == "." || ret == "/" {
			return ""
		}
		return ret
	}

	// put existing __init__.py files to a set first. This set is used for preventing
	// generated __init__.py files from overwriting existing ones.
	for _, inputZip := range inputZips {
		if err := inputZip.Open(); err != nil {
			return nil, err
		}
		for _, file := range inputZip.Entries() {
			pyPkg := getPackage(file.Name)
			baseName := filepath.Base(file.Name)
			if baseName == "__init__.py" || baseName == "__init__.pyc" {
				if _, found := initedPackages[pyPkg]; found {
					panic(fmt.Errorf("found __init__.py path duplicates during pars merging: %q", file.Name))
				}
				initedPackages[pyPkg] = true
			}
			for pyPkg != "" {
				if _, found := seenPkgs[pyPkg]; found {
					break
				}
				seenPkgs[pyPkg] = true
				allPackages = append(allPackages, pyPkg)
				pyPkg = getPackage(pyPkg)
			}
		}
	}
	noInitPackages := make([]string, 0)
	for _, pyPkg := range allPackages {
		if _, found := initedPackages[pyPkg]; !found {
			noInitPackages = append(noInitPackages, pyPkg)
		}
	}
	return noInitPackages, nil
}

// An InputZip owned by the InputZipsManager. Opened ManagedInputZip's are chained in the open order.
type ManagedInputZip struct {
	owner        *InputZipsManager
	realInputZip InputZip
	older        *ManagedInputZip
	newer        *ManagedInputZip
}

// Maintains the array of ManagedInputZips, keeping track of open input ones. When an InputZip is opened,
// may close some other InputZip to limit the number of open ones.
type InputZipsManager struct {
	inputZips     []*ManagedInputZip
	nOpenZips     int
	maxOpenZips   int
	openInputZips *ManagedInputZip
}

func (miz *ManagedInputZip) unlink() {
	olderMiz := miz.older
	newerMiz := miz.newer
	if newerMiz.older != miz || olderMiz.newer != miz {
		panic(fmt.Errorf("removing %p:%#v: broken list between %p:%#v and %p:%#v",
			miz, miz, newerMiz, newerMiz, olderMiz, olderMiz))
	}
	olderMiz.newer = newerMiz
	newerMiz.older = olderMiz
	miz.newer = nil
	miz.older = nil
}

func (miz *ManagedInputZip) link(olderMiz *ManagedInputZip) {
	if olderMiz.newer != nil || olderMiz.older != nil {
		panic(fmt.Errorf("inputZip is already open"))
	}
	oldOlderMiz := miz.older
	if oldOlderMiz.newer != miz {
		panic(fmt.Errorf("broken list between %p:%#v and %p:%#v", miz, miz, oldOlderMiz, oldOlderMiz))
	}
	miz.older = olderMiz
	olderMiz.older = oldOlderMiz
	oldOlderMiz.newer = olderMiz
	olderMiz.newer = miz
}

func NewInputZipsManager(nInputZips, maxOpenZips int) *InputZipsManager {
	if maxOpenZips < 3 {
		panic(fmt.Errorf("open zips limit should be above 3"))
	}
	// In the fake element .older points to the most recently opened InputZip, and .newer points to the oldest.
	head := new(ManagedInputZip)
	head.older = head
	head.newer = head
	return &InputZipsManager{
		inputZips:     make([]*ManagedInputZip, 0, nInputZips),
		maxOpenZips:   maxOpenZips,
		openInputZips: head,
	}
}

// InputZip factory
func (izm *InputZipsManager) Manage(inz InputZip) InputZip {
	iz := &ManagedInputZip{owner: izm, realInputZip: inz}
	izm.inputZips = append(izm.inputZips, iz)
	return iz
}

// Opens or reopens ManagedInputZip.
func (izm *InputZipsManager) reopen(miz *ManagedInputZip) error {
	if miz.realInputZip.IsOpen() {
		if miz != izm.openInputZips {
			miz.unlink()
			izm.openInputZips.link(miz)
		}
		return nil
	}
	if izm.nOpenZips >= izm.maxOpenZips {
		if err := izm.close(izm.openInputZips.older); err != nil {
			return err
		}
	}
	if err := miz.realInputZip.Open(); err != nil {
		return err
	}
	izm.openInputZips.link(miz)
	izm.nOpenZips++
	return nil
}

func (izm *InputZipsManager) close(miz *ManagedInputZip) error {
	if miz.IsOpen() {
		err := miz.realInputZip.Close()
		izm.nOpenZips--
		miz.unlink()
		return err
	}
	return nil
}

// Checks that openInputZips deque is valid
func (izm *InputZipsManager) checkOpenZipsDeque() {
	nReallyOpen := 0
	el := izm.openInputZips
	for {
		elNext := el.older
		if elNext.newer != el {
			panic(fmt.Errorf("Element:\n  %p: %v\nNext:\n  %p %v", el, el, elNext, elNext))
		}
		if elNext == izm.openInputZips {
			break
		}
		el = elNext
		if !el.IsOpen() {
			panic(fmt.Errorf("Found unopened element"))
		}
		nReallyOpen++
		if nReallyOpen > izm.nOpenZips {
			panic(fmt.Errorf("found %d open zips, should be %d", nReallyOpen, izm.nOpenZips))
		}
	}
	if nReallyOpen > izm.nOpenZips {
		panic(fmt.Errorf("found %d open zips, should be %d", nReallyOpen, izm.nOpenZips))
	}
}

func (miz *ManagedInputZip) Name() string {
	return miz.realInputZip.Name()
}

func (miz *ManagedInputZip) Open() error {
	return miz.owner.reopen(miz)
}

func (miz *ManagedInputZip) Close() error {
	return miz.owner.close(miz)
}

func (miz *ManagedInputZip) IsOpen() bool {
	return miz.realInputZip.IsOpen()
}

func (miz *ManagedInputZip) Entries() []*zip.File {
	if !miz.IsOpen() {
		panic(fmt.Errorf("%s: is not open", miz.Name()))
	}
	return miz.realInputZip.Entries()
}

// Actual processing.
func mergeZips(inputZips []InputZip, writer *zip.Writer, manifest, pyMain string,
	sortEntries, emulateJar, emulatePar, stripDirEntries, ignoreDuplicates bool,
	excludeFiles, excludeDirs []string, zipsToNotStrip map[string]bool) error {

	out := NewOutputZip(writer, sortEntries, emulateJar, stripDirEntries, ignoreDuplicates)
	out.setExcludeFiles(excludeFiles)
	out.setExcludeDirs(excludeDirs)
	if manifest != "" {
		if err := out.addManifest(manifest); err != nil {
			return err
		}
	}
	if pyMain != "" {
		if err := out.addZipEntryFromFile("__main__.py", pyMain); err != nil {
			return err
		}
	}

	if emulatePar {
		noInitPackages, err := out.getUninitializedPythonPackages(inputZips)
		if err != nil {
			return err
		}
		for _, uninitializedPyPackage := range noInitPackages {
			if err = out.addEmptyEntry(filepath.Join(uninitializedPyPackage, "__init__.py")); err != nil {
				return err
			}
		}
	}

	var jarServices jar.Services

	// Finally, add entries from all the input zips.
	for _, inputZip := range inputZips {
		_, copyFully := zipsToNotStrip[inputZip.Name()]
		if err := inputZip.Open(); err != nil {
			return err
		}

		for i, entry := range inputZip.Entries() {
			if emulateJar && jarServices.IsServiceFile(entry) {
				// If this is a jar, collect service files to combine  instead of adding them to the zip.
				err := jarServices.AddServiceFile(entry)
				if err != nil {
					return err
				}
				continue
			}
			if copyFully || !out.isEntryExcluded(entry.Name) {
				if err := out.copyEntry(inputZip, i); err != nil {
					return err
				}
			}
		}
		// Unless we need to rearrange the entries, the input zip can now be closed.
		if !(emulateJar || sortEntries) {
			if err := inputZip.Close(); err != nil {
				return err
			}
		}
	}

	if emulateJar {
		// Combine all the service files into a single list of combined service files and add them to the zip.
		for _, serviceFile := range jarServices.ServiceFiles() {
			_, err := out.addZipEntry(serviceFile.Name, ZipEntryFromBuffer{
				fh:      serviceFile.FileHeader,
				content: serviceFile.Contents,
			})
			if err != nil {
				return err
			}
		}
		return out.writeEntries(out.jarSorted())
	} else if sortEntries {
		return out.writeEntries(out.alphanumericSorted())
	}
	return nil
}

// Process command line
type fileList []string

func (f *fileList) String() string {
	return `""`
}

func (f *fileList) Set(name string) error {
	*f = append(*f, filepath.Clean(name))

	return nil
}

type zipsToNotStripSet map[string]bool

func (s zipsToNotStripSet) String() string {
	return `""`
}

func (s zipsToNotStripSet) Set(path string) error {
	s[path] = true
	return nil
}

var (
	sortEntries      = flag.Bool("s", false, "sort entries (defaults to the order from the input zip files)")
	emulateJar       = flag.Bool("j", false, "sort zip entries using jar ordering (META-INF first)")
	emulatePar       = flag.Bool("p", false, "merge zip entries based on par format")
	excludeDirs      fileList
	excludeFiles     fileList
	zipsToNotStrip   = make(zipsToNotStripSet)
	stripDirEntries  = flag.Bool("D", false, "strip directory entries from the output zip file")
	manifest         = flag.String("m", "", "manifest file to insert in jar")
	pyMain           = flag.String("pm", "", "__main__.py file to insert in par")
	prefix           = flag.String("prefix", "", "A file to prefix to the zip file")
	ignoreDuplicates = flag.Bool("ignore-duplicates", false, "take each entry from the first zip it exists in and don't warn")
)

func init() {
	flag.Var(&excludeDirs, "stripDir", "directories to be excluded from the output zip, accepts wildcards")
	flag.Var(&excludeFiles, "stripFile", "files to be excluded from the output zip, accepts wildcards")
	flag.Var(&zipsToNotStrip, "zipToNotStrip", "the input zip file which is not applicable for stripping")
}

type FileInputZip struct {
	name   string
	reader *zip.ReadCloser
}

func (fiz *FileInputZip) Name() string {
	return fiz.name
}

func (fiz *FileInputZip) Close() error {
	if fiz.IsOpen() {
		reader := fiz.reader
		fiz.reader = nil
		return reader.Close()
	}
	return nil
}

func (fiz *FileInputZip) Entries() []*zip.File {
	if !fiz.IsOpen() {
		panic(fmt.Errorf("%s: is not open", fiz.Name()))
	}
	return fiz.reader.File
}

func (fiz *FileInputZip) IsOpen() bool {
	return fiz.reader != nil
}

func (fiz *FileInputZip) Open() error {
	if fiz.IsOpen() {
		return nil
	}
	var err error
	if fiz.reader, err = zip.OpenReader(fiz.Name()); err != nil {
		return fmt.Errorf("%s: %s", fiz.Name(), err.Error())
	}
	return nil
}

func main() {
	flag.Usage = func() {
		fmt.Fprintln(os.Stderr, "usage: merge_zips [-jpsD] [-m manifest] [--prefix script] [-pm __main__.py] OutputZip [inputs...]")
		flag.PrintDefaults()
	}

	// parse args
	flag.Parse()
	args := flag.Args()
	if len(args) < 1 {
		flag.Usage()
		os.Exit(1)
	}
	outputPath := args[0]
	inputs := make([]string, 0)
	for _, input := range args[1:] {
		if input[0] == '@' {
			f, err := os.Open(strings.TrimPrefix(input[1:], "@"))
			if err != nil {
				log.Fatal(err)
			}

			rspInputs, err := response.ReadRspFile(f)
			f.Close()
			if err != nil {
				log.Fatal(err)
			}
			inputs = append(inputs, rspInputs...)
		} else {
			inputs = append(inputs, input)
		}
	}

	log.SetFlags(log.Lshortfile)

	// make writer
	outputZip, err := os.Create(outputPath)
	if err != nil {
		log.Fatal(err)
	}
	defer outputZip.Close()

	var offset int64
	if *prefix != "" {
		prefixFile, err := os.Open(*prefix)
		if err != nil {
			log.Fatal(err)
		}
		offset, err = io.Copy(outputZip, prefixFile)
		if err != nil {
			log.Fatal(err)
		}
	}

	writer := zip.NewWriter(outputZip)
	defer func() {
		err := writer.Close()
		if err != nil {
			log.Fatal(err)
		}
	}()
	writer.SetOffset(offset)

	if *manifest != "" && !*emulateJar {
		log.Fatal(errors.New("must specify -j when specifying a manifest via -m"))
	}

	if *pyMain != "" && !*emulatePar {
		log.Fatal(errors.New("must specify -p when specifying a Python __main__.py via -pm"))
	}

	// do merge
	inputZipsManager := NewInputZipsManager(len(inputs), 1000)
	inputZips := make([]InputZip, len(inputs))
	for i, input := range inputs {
		inputZips[i] = inputZipsManager.Manage(&FileInputZip{name: input})
	}
	err = mergeZips(inputZips, writer, *manifest, *pyMain, *sortEntries, *emulateJar, *emulatePar,
		*stripDirEntries, *ignoreDuplicates, []string(excludeFiles), []string(excludeDirs),
		map[string]bool(zipsToNotStrip))
	if err != nil {
		log.Fatal(err)
	}
}
