// Copyright 2014 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 bootstrap

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	"github.com/google/blueprint"
)

const logFileName = ".ninja_log"

// removeAbandonedFilesUnder removes any files that appear in the Ninja log, and
// are prefixed with one of the `under` entries, but that are not currently
// build targets, or in `exempt`
func removeAbandonedFilesUnder(ctx *blueprint.Context, config *Config,
	srcDir string, under, exempt []string) error {

	if len(under) == 0 {
		return nil
	}

	ninjaBuildDir, err := ctx.NinjaBuildDir()
	if err != nil {
		return err
	}

	targetRules, err := ctx.AllTargets()
	if err != nil {
		return fmt.Errorf("error determining target list: %s", err)
	}

	replacer := strings.NewReplacer(
		"@@SrcDir@@", srcDir,
		"@@BuildDir@@", BuildDir)
	ninjaBuildDir = replacer.Replace(ninjaBuildDir)
	targets := make(map[string]bool)
	for target := range targetRules {
		replacedTarget := replacer.Replace(target)
		targets[filepath.Clean(replacedTarget)] = true
	}
	for _, target := range exempt {
		replacedTarget := replacer.Replace(target)
		targets[filepath.Clean(replacedTarget)] = true
	}

	filePaths, err := parseNinjaLog(ninjaBuildDir, under)
	if err != nil {
		return err
	}

	for _, filePath := range filePaths {
		isTarget := targets[filePath]
		if !isTarget {
			err = removeFileAndEmptyDirs(filePath)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func parseNinjaLog(ninjaBuildDir string, under []string) ([]string, error) {
	logFilePath := filepath.Join(ninjaBuildDir, logFileName)
	logFile, err := os.Open(logFilePath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	defer logFile.Close()

	scanner := bufio.NewScanner(logFile)

	// Check that the first line indicates that this is a Ninja log version 5
	const expectedFirstLine = "# ninja log v5"
	if !scanner.Scan() || scanner.Text() != expectedFirstLine {
		return nil, errors.New("unrecognized ninja log format")
	}

	var filePaths []string
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "#") {
			continue
		}

		const fieldSeperator = "\t"
		fields := strings.Split(line, fieldSeperator)

		const precedingFields = 3
		const followingFields = 1

		if len(fields) < precedingFields+followingFields+1 {
			return nil, fmt.Errorf("log entry has too few fields: %q", line)
		}

		start := precedingFields
		end := len(fields) - followingFields
		filePath := strings.Join(fields[start:end], fieldSeperator)

		for _, dir := range under {
			if strings.HasPrefix(filePath, dir) {
				filePaths = append(filePaths, filePath)
				break
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return filePaths, nil
}

func removeFileAndEmptyDirs(path string) error {
	err := os.Remove(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		pathErr := err.(*os.PathError)
		switch pathErr.Err {
		case syscall.ENOTEMPTY, syscall.EEXIST, syscall.ENOTDIR:
			return nil
		}
		return err
	}
	fmt.Printf("removed old ninja-created file %s because it has no rule to generate it\n", path)

	path, err = filepath.Abs(path)
	if err != nil {
		return err
	}

	cwd, err := os.Getwd()
	if err != nil {
		return err
	}

	for dir := filepath.Dir(path); dir != cwd; dir = filepath.Dir(dir) {
		err = os.Remove(dir)
		if err != nil {
			pathErr := err.(*os.PathError)
			switch pathErr.Err {
			case syscall.ENOTEMPTY, syscall.EEXIST:
				// We've come to a nonempty directory, so we're done.
				return nil
			default:
				return err
			}
		}
	}

	return nil
}