/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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

//
// The bundler turns files into resources contained in go code.
//
// This is useful for serving HTML and Javascript files from a self-contained binary.
//

import (
	"bufio"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
)

const APACHE_HEADER = `/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
`

const GENERATED_CODE_COMMENT = "// THIS IS GENERATED CODE.  DO NOT EDIT."

var SEP string = string(os.PathSeparator)

// Return true if a file contains a given string.
func fileContainsString(path, line string) (bool, error) {
	file, err := os.Open(path)
	if err != nil {
		return false, err
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		if strings.Contains(scanner.Text(), line) {
			return true, nil
		}
	}
	if err := scanner.Err(); err != nil {
		return false, err
	}
	return false, nil
}

// Converts a source file path to a destination file path
func sfileToDfile(sfile string) string {
	return strings.Replace(sfile, SEP, "__", -1) + ".go"
}

// Delete generated files that are in dfiles but not sfiles.
// sfiles and dfiles must be sorted by file name.
func deleteUnusedDst(sfiles []string, dst string, dfiles []os.FileInfo) error {
	s := 0
	for d := range dfiles {
		fullDst := dst + SEP + dfiles[d].Name()
		generated, err := fileContainsString(fullDst, GENERATED_CODE_COMMENT)
		if err != nil {
			return err
		}
		if !generated {
			// Skip this destination file, since it is not generated.
			continue
		}
		found := false
		for {
			if s >= len(sfiles) {
				break
			}
			tgt := sfileToDfile(sfiles[s])
			if tgt == dfiles[d].Name() {
				found = true
				break
			}
			if tgt > dfiles[d].Name() {
				break
			}
			s++
		}
		if !found {
			log.Printf("Removing %s\n", fullDst)
			err := os.Remove(fullDst)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func createBundleFile(pkg, src, sfile, dst string) error {
	// Open destination file and write header.
	tgt := sfileToDfile(sfile)
	fullDst := dst + SEP + tgt
	out, err := os.Create(fullDst)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = out.WriteString(APACHE_HEADER)
	if err != nil {
		return err
	}
	_, err = out.WriteString("\n" + GENERATED_CODE_COMMENT + "\n")
	if err != nil {
		return err
	}
	_, err = out.WriteString(fmt.Sprintf("\npackage %s\n", pkg))
	if err != nil {
		return err
	}
	_, err = out.WriteString(fmt.Sprintf("var _ = addResource(\"%s\", `\n", tgt[:len(tgt)-3]))
	if err != nil {
		return err
	}

	// Open source file and create scanner.
	fullSrc := src + SEP + sfile
	in, err := os.Open(fullSrc)
	if err != nil {
		return err
	}
	defer in.Close()
	reader := bufio.NewReader(in)
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		_, err := out.WriteString(strings.Replace(scanner.Text(), "`", "` + \"`\" + `", -1) + "\n")
		if err != nil {
			return err
		}
	}
	_, err = out.WriteString("`)\n")
	if err != nil {
		return err
	}
	err = out.Close()
	if err != nil {
		return err
	}
	return nil
}

var src = flag.String("src", "", "Source path for bundled resources.")
var dst = flag.String("dst", "", "Destination path for bundled resources.")
var pkg = flag.String("pkg", "resource", "Package name to use for bundled resources")

func main() {
	flag.Parse()
	if *src == "" {
		log.Fatal("You must supply a src directory to bundle.")
	}
	if *dst == "" {
		log.Fatal("You must supply a dst directory for output.")
	}
	sfiles := make([]string, 0, 32)
	absSrc, err := filepath.Abs(filepath.Clean(*src))
	if err != nil {
		log.Fatalf("Error getting absolute path for %s: %s\n", *src, err.Error())
	}
	err = filepath.Walk(absSrc, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		var suffix string
		suffix, err = filepath.Rel(absSrc, path)
		if err != nil {
			return err
		}
		sfiles = append(sfiles, suffix)
		return nil
	})
	if err != nil {
		log.Fatalf("Error listing files in src directory %s: %s\n", absSrc, err.Error())
	}
	var dfiles []os.FileInfo
	dfiles, err = ioutil.ReadDir(*dst)
	if err != nil {
		log.Fatalf("Error listing files in output directory %s: %s\n", *dst, err.Error())
	}
	deleteUnusedDst(sfiles, *dst, dfiles)
	for s := range sfiles {
		err = createBundleFile(*pkg, absSrc, sfiles[s], *dst)
		if err != nil {
			log.Fatalf("Error creating bundle file for %s in %s: %s\n",
				sfiles[s], *dst, err.Error())
		}
		log.Printf("Bundled %s as %s\n", absSrc, absSrc+SEP+sfileToDfile(sfiles[s]))
	}
}
