/*
 * Copyright 2023 Alexander Garver
 *
 * 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.
 */

//! # Tagalyzer
//!
//! Tagalyzer is a text-analysis library intended to aid writers in finding appropriate tags for
//! their content. As of now, it counts words in a list of files, performing certain
//! character-based filtering in an attempt to remove formatting characters and punctuation.
//!
//! ## Examples
//!
//! To see a basic example without installing the CLI tool, try running one of these:
//!
//! ```bash
//! cargo run -- README.md
//! ```
//!
//! ```bash
//! cargo run -- --help
//! ```
//!
//! ## Library Usage
//!
//! There are two main ways to use this library. The simpler one is to use the `FileProcessor`
//! struct:
//!
//! ```
//! use tagalyzer::prelude::*;
//!
//! let fp = FileProcessor::default();
//! let word_counts = fp.count_words_in_file("README.md").unwrap();
//!
//! assert!(word_counts.get("words").unwrap() > &0)
//! ```
//!
//! Alternatively, you can instantiate a `Tagalyzer` from an instance of `Args` by supplying a faux
//! CLI input with as many files as you want.
//!
//! ```
//! use tagalyzer::prelude::*;
//!
//! let mock_stdin = "tagalyzer LICENSE-APACHE LICENSE-MIT";
//! let args = Args::parse_from(mock_stdin.split_whitespace());
//! let mut tag = Tagalyzer::from(&args);
//!
//! let _ = tag.count_all_files().unwrap();
//! print!("{tag}");
//! ```

mod args;
mod file_processor;
mod statistics;

use std::{fmt::Display, io::Error as IoError};

pub use args::prelude::*;
pub use file_processor::prelude::*;
pub use statistics::prelude::*;

pub mod prelude {
	pub use super::*;
}

/// # Tagalyzer
///
/// This is the base class of the library. It orchestrates the consistent analysis across and
/// between different text files.
///
/// At the moment, it simply populates a list of files to perform analysis on and creates a
/// template processor, then uses that processor to iterate over the list of files in serial.
///
/// In the future, there will be an option to handle this analysis concurrently, likely utilizing
/// a thread pool to avoid necessitating an async/await runtime.
#[derive(Debug, Clone)]
pub struct Tagalyzer {
	/// # File Queue
	///
	/// The queue of files that need to be processed, stored as `FileStatistics<Unsorted>` to
	/// utilize a consistent data structure and compile-time type safety.
	file_queue: Vec<FileStatistics<Unsorted>>,
	/// # Sorted Files
	///
	/// Files that have been sorted. Again, `FileStatistics<Sorted>` ensures type safety and a
	/// consistent data structure.
	sorted_files: Vec<FileStatistics<Sorted>>,
	/// # File Processor
	///
	/// An instance of the `FileProcessor` struct that is used as a "source of truth" for how
	/// analysis is to be performed. This ensures consistent and repeatable analysis.
	file_processor: FileProcessor,
}

impl Tagalyzer {
	/// # Count All Files
	///
	/// Iterates over the file queue and uses the file processor to perform the same analysis on
	/// each one, then sorts the statistics and adds it to the sorted files list.
	///
	/// In the future, this is where the core of the concurrency logic will be.
	///
	/// ## Errors
	///
	/// Transparently returns any `std::io::Error` which may arise. Such an error indicates a
	/// problem opening a file in read mode.
	///
	/// This may indicate a directory path was passed as a file, which isn't currently supported.
	/// Use a subshell (or similar) to list or walk the directory contents.
	pub fn count_all_files(&mut self) -> Result<(), IoError> {
		while let Some(mut stats) = self.file_queue.pop() {
			self.file_processor.populate_file_statistics(&mut stats)?;
			let stats = stats.sort();
			self.sorted_files.push(stats);
		}
		Ok(())
	}
}

impl From<&Args> for Tagalyzer {
	fn from(args: &Args) -> Self {
		let mut file_queue = Vec::new();
		for path in &args.paths {
			file_queue.push(FileStatistics::new(path));
		}

		Self {
			file_queue,
			sorted_files: Vec::new(),
			file_processor: FileProcessor::from(args),
		}
	}
}

impl Display for Tagalyzer {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		for file in &self.sorted_files {
			writeln!(f, "{file}")?;
		}
		Ok(())
	}
}

#[cfg(test)]
mod tests {

	use super::*;
	use std::{
		env, fs,
		io::ErrorKind::{AlreadyExists as FileExists, NotFound},
		path::PathBuf,
	};

	use rand::random;

	#[test]
	fn test_new() {
		let args = Args::parse_from(["tag", "Cargo.toml"].iter());

		let tagalyzer = Tagalyzer::from(&args);

		// Assert Tagalyzer initialized correctly
		assert_eq!(tagalyzer.file_queue.len(), 1);
		assert_eq!(tagalyzer.sorted_files.len(), 0);
	}

	#[test]
	fn test_count_all_files() {
		let file1 = setup_file();
		let file2 = setup_file();

		let args =
			Args::parse_from(["tag", file1.to_str().unwrap(), file2.to_str().unwrap()].iter());
		let mut tagalyzer = Tagalyzer::from(&args);

		assert_eq!(tagalyzer.file_queue.len(), 2);
		assert_eq!(tagalyzer.sorted_files.len(), 0);

		let result = tagalyzer.count_all_files();
		assert!(result.is_ok());

		assert_eq!(tagalyzer.file_queue.len(), 0);
		assert_eq!(tagalyzer.sorted_files.len(), 2);

		let stats1 = tagalyzer.sorted_files.pop().unwrap();
		let stats2 = tagalyzer.sorted_files.pop().unwrap();
		assert_eq!(stats1, stats2);
	}

	#[test]
	fn file_does_not_exist() {
		let args = Args::parse_from(["tag", "foobar"].iter());
		let mut tagalyzer = Tagalyzer::from(&args);

		let result = tagalyzer.count_all_files();

		assert!(result.is_err());

		let e = result.err().unwrap();
		assert_eq!(e.kind(), NotFound)
	}

	/// # Generate Filepath
	///
	/// Utility function to generate a random filepath within a given directory.
	fn gen_filepath(dir: &PathBuf) -> PathBuf {
		let mut filepath = dir.clone();
		filepath.push(format!("text_{}.txt", random::<usize>()));
		return filepath;
	}

	/// # Setup File
	///
	/// Utility function to set up tests with a text file.
	fn setup_file() -> PathBuf {
		let contents: &str = "But Mousie, thou art no thy-lane,\n\
									In proving foresight may be vain:\n\
									The best laid schemes o' Mice an' Men\n\
									Gang aft agley,\n\
									An' lea'e us nought but grief an' pain,\n\
									For promis'd joy!";

		let mut pwd = env::temp_dir();
		pwd.push("tagalyzer_tests");
		if let Err(e) = fs::DirBuilder::new().create(&pwd) {
			if e.kind() != FileExists {
				panic!()
			}
		};

		let filepath = gen_filepath(&pwd);

		fs::write(&filepath, &contents).unwrap();
		filepath
	}
}
