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

// The lint binary ensures the source tree is correctly formatted.
package main

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

// isBlacklisted returns true if the given filename should not be linted.  The candidate must be
// given as a relative path to the workspace directory.
func isBlacklisted(candidate string) bool {
	// Skip hidden files as we don't need to run checks on them.  (This is not strictly
	// true, but it's simpler this way for now and the risk is low given that the hidden
	// files we have are trivial.)
	if strings.HasPrefix(candidate, ".") {
		return true
	}

	// Skip the Rust build directory.
	if strings.HasPrefix(candidate, "target/") {
		return true
	}

	base := filepath.Base(candidate)
	ext := filepath.Ext(candidate)

	// Only worry about non-generated files.
	if base == "Cargo.lock" || base == "Makefile" || base == "go.mod" || base == "go.sum" {
		return true
	}

	// Skip plist files, which even though are manually written, they have quite a few
	// exceptions.
	if ext == ".plist" {
		return true
	}

	return false
}

// collectFiles scans the given directory recursively and returns the paths to all regular files
// within it as relative paths to the given directory.
func collectFiles(dir string) ([]string, error) {
	files := make([]string, 0)

	collector := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relative, err := filepath.Rel(dir, path)
		if err != nil {
			panic(fmt.Sprintf("%s is not within %s but it should have been", path, dir))
		}
		if info.Mode()&os.ModeType == 0 {
			files = append(files, relative)
		}

		return err
	}

	return files, filepath.Walk(dir, collector)
}

func main() {
	verbose := flag.Bool("verbose", false, "Enables extra logging")
	workspace := flag.String("workspace", ".", "Path to the directory where the source tree lives; used to find source files (symlinks are followed) and to resolve relative paths to sources")
	flag.Parse()

	if *verbose {
		log.SetOutput(os.Stderr)
	} else {
		log.SetOutput(ioutil.Discard)
	}

	var relFiles []string
	if len(flag.Args()) == 0 {
		log.Printf("Searching for source files in %s", *workspace)
		var err error
		relFiles, err = collectFiles(*workspace)
		if err != nil {
			fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
			os.Exit(1)
		}
	} else {
		for _, file := range flag.Args() {
			if filepath.IsAbs(file) {
				fmt.Fprintf(os.Stderr, "ERROR: Explicitly-provided file names must be relative; %s was not\n", file)
				os.Exit(1)
			}
		}
		relFiles = flag.Args()
	}

	files := make([]string, 0, len(relFiles))
	for _, file := range relFiles {
		if isBlacklisted(file) {
			log.Printf("Skipping linting of %s because it is blacklisted", file)
		} else {
			files = append(files, filepath.Join(*workspace, file))
		}
	}

	failed := false
	for _, file := range files {
		if !checkAll(*workspace, file) {
			failed = true
		}
	}
	if failed {
		os.Exit(1)
	}
}
