package os

import "core:container/queue"

/*
A recursive directory walker.

Note that none of the fields should be accessed directly.
*/
Walker :: struct {
	todo:      queue.Queue(string),
	skip_dir:  bool,
	err: struct {
		path: [dynamic]byte,
		err:  Error,
	},
	iter: Read_Directory_Iterator,
}

walker_init_path :: proc(w: ^Walker, path: string) {
	cloned_path, err := clone_string(path, file_allocator())
	if err != nil {
		walker_set_error(w, path, err)
		return
	}

	walker_clear(w)

	if _, err = queue.push(&w.todo, cloned_path); err != nil {
		walker_set_error(w, cloned_path, err)
		return
	}
}

walker_init_file :: proc(w: ^Walker, f: ^File) {
	handle, err := clone(f)
	if err != nil {
		path, _ := clone_string(name(f), file_allocator())
		walker_set_error(w, path, err)
		return
	}

	walker_clear(w)

	read_directory_iterator_init(&w.iter, handle)
}

/*
Initializes a walker, either using a path or a file pointer to a directory the walker will start at.

You are allowed to repeatedly call this to reuse it for later walks.

For an example on how to use the walker, see `walker_walk`.
*/
walker_init :: proc {
	walker_init_path,
	walker_init_file,
}

@(require_results)
walker_create_path :: proc(path: string) -> (w: Walker) {
	walker_init_path(&w, path)
	return
}

@(require_results)
walker_create_file :: proc(f: ^File) -> (w: Walker) {
	walker_init_file(&w, f)
	return
}

/*
Creates a walker, either using a path or a file pointer to a directory the walker will start at.

For an example on how to use the walker, see `walker_walk`.
*/
walker_create :: proc {
	walker_create_path,
	walker_create_file,
}

/*
Returns the last error that occurred during the walker's operations.

Can be called while iterating, or only at the end to check if anything failed.
*/
@(require_results)
walker_error :: proc(w: ^Walker) -> (path: string, err: Error) {
	return string(w.err.path[:]), w.err.err
}

@(private)
walker_set_error :: proc(w: ^Walker, path: string, err: Error) {
	if err == nil {
		return
	}

	resize(&w.err.path, len(path))
	copy(w.err.path[:], path)

	w.err.err = err
}

@(private)
walker_clear :: proc(w: ^Walker) {
	w.iter.f = nil
	w.skip_dir = false

	w.err.path.allocator = file_allocator()
	clear(&w.err.path)

	w.todo.data.allocator = file_allocator()
	for path in queue.pop_front_safe(&w.todo) {
		delete(path, file_allocator())
	}
}

walker_destroy :: proc(w: ^Walker) {
	walker_clear(w)
	queue.destroy(&w.todo)
	delete(w.err.path)
	read_directory_iterator_destroy(&w.iter)
}

// Marks the current directory to be skipped (not entered into).
walker_skip_dir :: proc(w: ^Walker) {
	w.skip_dir = true
}

/*
Returns the next file info in the iterator, files are iterated in breadth-first order.

If an error occurred opening a directory, you may get zero'd info struct and
`walker_error` will return the error.

Example:
	package main

	import "core:fmt"
	import "core:strings"
	import "core:os"

	main :: proc() {
		w := os.walker_create("core")
		defer os.walker_destroy(&w)

		for info in os.walker_walk(&w) {
			// Optionally break on the first error:
			// _ = walker_error(&w) or_break

			// Or, handle error as we go:
			if path, err := os.walker_error(&w); err != nil {
				fmt.eprintfln("failed walking %s: %s", path, err)
				continue
			}

			// Or, do not handle errors during iteration, and just check the error at the end.



			// Skip a directory:
			if strings.has_suffix(info.fullpath, ".git") {
				os.walker_skip_dir(&w)
				continue
			}

			fmt.printfln("%#v", info)
		}

		// Handle error if one happened during iteration at the end:
		if path, err := os.walker_error(&w); err != nil {
			fmt.eprintfln("failed walking %s: %v", path, err)
		}
	}
*/
@(require_results)
walker_walk :: proc(w: ^Walker) -> (fi: File_Info, ok: bool) {
	if w.skip_dir {
		w.skip_dir = false
		if skip, sok := queue.pop_back_safe(&w.todo); sok {
			delete(skip, file_allocator())
		}
	}

	if w.iter.f == nil {
		if queue.len(w.todo) == 0 {
			return
		}

		next := queue.pop_front(&w.todo)

		handle, err := open(next)
		if err != nil {
			walker_set_error(w, next, err)
			return {}, true
		}

		read_directory_iterator_init(&w.iter, handle)

		delete(next, file_allocator())
	}

	info, _, iter_ok := read_directory_iterator(&w.iter)

	if path, err := read_directory_iterator_error(&w.iter); err != nil {
		walker_set_error(w, path, err)
	}

	if !iter_ok {
		close(w.iter.f)
		w.iter.f = nil
		return walker_walk(w)
	}

	if info.type == .Directory {
		path, err := clone_string(info.fullpath, file_allocator())
		if err != nil {
			walker_set_error(w, "", err)
			return
		}

		_, err = queue.push_back(&w.todo, path)
		if err != nil {
			walker_set_error(w, path, err)
			return
		}
	}

	return info, iter_ok
}