// Copyright 2015 The rkt Authors
//
// 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 (
	"bufio"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	"github.com/hashicorp/errwrap"
	"github.com/rkt/rkt/tools/common"
)

// processor processes words from files generated by make
type processor interface {
	kind() string
	setWordsNumber(number int)
	processWord(word string) error
	getList() []string
	remove(name string) error
}

// fileProcessor is the simplest from all processor
// implementations. Nothing special needs to be done for regular
// files, so it simply appends all the data passed to it.
type fileProcessor struct {
	list []string
}

// symlinkProcessor prepares symlinks for removal - it makes sure that
// the directory part of a path to symlink has no symlinks in it. This
// is to make sure, that if a symlink is removed, then we can remove
// also other symlinks that were previously accessible via the just
// removed symlink.
type symlinkProcessor struct {
	list []string
}

// dirProcessor, like symlinkProcessor, evaluates symlinks in paths it
// receives. It also sorts the directories, so the leaves are removed
// first.
type dirProcessor struct {
	set map[string]struct{}
}

type items struct {
	proc processor
	list []string
}

type kindData struct {
	filename string
	option   string
	proc     processor
}

func main() {
	allItems := getItems()

	failed := false
	for _, i := range allItems {
		for _, el := range i.list {
			if err := i.proc.remove(el); err != nil {
				if os.IsNotExist(err) {
					continue
				}
				common.Warn("Failed to remove %s %q: %v", i.proc.kind(), el, err)
				failed = true
				if infos, err := ioutil.ReadDir(el); err == nil {
					var contents []string
					for _, fi := range infos {
						contents = append(contents, fi.Name())
					}
					common.Warn("  Contents of the directory: %s", strings.Join(contents, ", "))
				}
			}
		}
	}
	if failed {
		os.Exit(1)
	}
}

func getItems() []items {
	kinds := []*kindData{
		{
			option: "files",
			proc:   &fileProcessor{},
		},
		{
			option: "symlinks",
			proc:   &symlinkProcessor{},
		},
		{
			option: "dirs",
			proc:   &dirProcessor{},
		},
	}

	for _, k := range kinds {
		flag.StringVar(&k.filename, k.option, "", fmt.Sprintf("List of %s", k.option))
	}
	flag.Parse()
	for _, k := range kinds {
		if k.filename == "" {
			common.Die("No --%s parameter passed", k.option)
		}
	}

	allItems := make([]items, 0, len(kinds))
	for _, k := range kinds {
		l, err := getList(k.filename, k.proc)
		if err != nil {
			common.Die("Failed to get %s list: %v", k.proc.kind(), err)
		}
		allItems = append(allItems, items{
			proc: k.proc,
			list: l,
		})
	}

	return allItems
}

func getList(file string, proc processor) ([]string, error) {
	fd, err := os.Open(file)
	if err != nil {
		return nil, errwrap.Wrap(fmt.Errorf("failed to open %s list %q", proc.kind(), file), err)
	}
	defer fd.Close()
	sc := bufio.NewScanner(fd)
	sc.Split(bufio.ScanWords)
	if !sc.Scan() {
		if err := sc.Err(); err != nil {
			return nil, errwrap.Wrap(fmt.Errorf("failed to read %s list %q", proc.kind(), file), err)
		}
		return nil, fmt.Errorf("failed to parse %s list %q: premature end of file", proc.kind(), file)
	}
	num, err := strconv.Atoi(sc.Text())
	if err != nil {
		return nil, errwrap.Wrap(fmt.Errorf("failed to parse first line of %s list %q", proc.kind(), file), err)
	}
	proc.setWordsNumber(num)
	for sc.Scan() {
		if err := proc.processWord(sc.Text()); err != nil {
			return nil, errwrap.Wrap(fmt.Errorf("failed to process %s from %s list %q", sc.Text(), proc.kind(), file), err)
		}
	}
	if err := sc.Err(); err != nil {
		return nil, errwrap.Wrap(fmt.Errorf("failed to read %s list %q", proc.kind(), file), err)
	}
	return proc.getList(), nil
}

// file processor

func (proc *fileProcessor) kind() string {
	return "file"
}

func (proc *fileProcessor) remove(name string) error {
	return os.Remove(name)
}

func (proc *fileProcessor) setWordsNumber(number int) {
	proc.list = make([]string, 0, number)
}

func (proc *fileProcessor) processWord(file string) error {
	if file != "" {
		proc.list = append(proc.list, file)
	}
	return nil
}

func (proc *fileProcessor) getList() []string {
	return proc.list
}

// symlink processor

func (proc *symlinkProcessor) kind() string {
	return "symlink"
}

func (proc *symlinkProcessor) remove(name string) error {
	return os.Remove(name)
}

func (proc *symlinkProcessor) setWordsNumber(number int) {
	proc.list = make([]string, 0, number)
}

func (proc *symlinkProcessor) processWord(symlink string) error {
	if symlink == "" {
		return nil
	}
	symlinkDir := filepath.Dir(symlink)
	symlinkBase := filepath.Base(symlink)
	realSymlinkDir, err := filepath.EvalSymlinks(symlinkDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return errwrap.Wrap(fmt.Errorf("failed to evaluate symlinks for symlink dir %q", symlinkDir), err)
	}
	proc.list = append(proc.list, filepath.Join(realSymlinkDir, symlinkBase))
	return nil
}

func (proc *symlinkProcessor) getList() []string {
	return proc.list
}

// dir processor

func (proc *dirProcessor) kind() string {
	return "directory"
}

func (proc *dirProcessor) remove(name string) error {
	return os.RemoveAll(name)
}

func (proc *dirProcessor) setWordsNumber(number int) {
	proc.set = make(map[string]struct{}, number)
}

func (proc *dirProcessor) processWord(dir string) error {
	if dir == "" {
		return nil
	}
	realDir, err := filepath.EvalSymlinks(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return errwrap.Wrap(fmt.Errorf("failed to evaluate symlinks for dir %q", dir), err)
	}
	proc.set[realDir] = struct{}{}
	return nil
}

func (proc *dirProcessor) getList() []string {
	list := make([]string, 0, len(proc.set))
	for d := range proc.set {
		list = append(list, d)
	}
	sort.Sort(sort.Reverse(sort.StringSlice(list)))
	return list
}
