/*
 * 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.
 */

//! # Statistics
//!
//! This module collects the statistical tools used to process files, including a struct to hold
//! data about a specific file and its state. At the moment, that struct is all that's really here.

use std::{collections::HashMap, fmt::Display, marker::PhantomData};

// Allowed for prelude specificlally
#[allow(clippy::missing_docs_in_private_items)]
pub mod prelude {
	pub use super::{FileStatistics, Sorted, Unsorted};
}

/// # Unsorted State Marker
///
/// Empty struct that only serves to store the state of 	`FileStatistics`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unsorted;
/// # Sorted State Marker
///
/// Empty struct that only serves to store the state of 	`FileStatistics`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sorted;

/// # File Statistics
///
/// This struct serves mostly as a data structure to store information about a specific file. It
/// uses a phantom marker to create type safety around its state as sorted or unsorted.
///
/// ## State
///
/// The `State` marker allows for type-safety in state-dependent functions, moving potential issues
/// to compile-time errors instead of relying on runtime checks. There are two states.
///
/// ### Unsorted
///
/// The unsorted state is used to mark a `FileStatistics` as having a mutable word count. It's
/// typically used while actively counting words or processing other statistics.
///
/// ### Sorted
///
/// The sorted state indicates a wordcount has been finalized and is no longer able to be changed,
/// as well as having been sorted by word frequency.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone)]
pub struct FileStatistics<State = Unsorted> {
	/// # Name
	///
	/// The name of the file being processed. This serves no mechanical purpose; it's only used by
	/// `Display`.
	pub name: String,
	/// # Counts
	///
	/// A hashmap keyed on words (`String`s) with a value that counts the occurrences of that word.
	/// This enables quick addition or modification of words and counts when processing, faster than
	/// iterating over a `Vec`.
	///
	/// This field is only accessible in the `Unsorted` state.
	counts: HashMap<String, usize>,
	/// # Sorted Counts
	///
	/// A vector of word and count tuples, sorted by the count in descending order. This field is
	/// used by `Display`, and is only available for the `Sorted` state.
	sorted_counts: Vec<(String, usize)>,
	/// # State
	///
	/// The state of the `FileStatistics`, either `Sorted` or `Unsorted`.
	///
	/// This field uses the `PhantomData` marker, which means the struct is used for type-safety
	/// checks at compile-time, but is removed before the final runtime executable is compiled (at
	/// least to my understanding).
	state: PhantomData<State>,
}

impl FileStatistics {
	#[must_use]
	pub fn new(name: &str) -> Self {
		Self {
			name: name.to_owned(),
			counts: HashMap::new(),
			sorted_counts: Vec::new(),
			state: PhantomData::<Unsorted>,
		}
	}
}

impl FileStatistics<Unsorted> {
	pub fn count_word(&mut self, word: &str) {
		*self.counts.entry(word.to_owned()).or_insert(1) += 1;
	}

	#[must_use]
	pub fn sort(self) -> FileStatistics<Sorted> {
		let mut vec_counts: Vec<(String, usize)> = self
			.counts
			.iter()
			.map(|(word, count)| (word.clone(), *count))
			.collect();
		vec_counts.sort_by(|(_, count1), (_, count2)| count2.cmp(count1));

		FileStatistics {
			name: self.name,
			counts: self.counts,
			sorted_counts: vec_counts,
			state: PhantomData::<Sorted>,
		}
	}

	#[must_use]
	pub const fn get_counts(&self) -> &HashMap<String, usize> {
		&self.counts
	}
}

impl FileStatistics<Sorted> {
	#[must_use]
	pub const fn get_counts(&self) -> &Vec<(String, usize)> {
		&self.sorted_counts
	}
}

impl Display for FileStatistics<Unsorted> {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		writeln!(f, "Unsorted wordcount for {}", self.name)?;

		let mut width = self
			.sorted_counts
			.iter()
			.map(|(w, _)| w.len())
			.max()
			.unwrap_or_default();
		width = std::cmp::min(width, 70);

		for (word, count) in &self.sorted_counts {
			writeln!(f, "{word:<width$}: {count}")?;
		}

		Ok(())
	}
}

impl Display for FileStatistics<Sorted> {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		writeln!(f, "Sorted wordcount for {}", self.name)?;

		let mut width = self
			.sorted_counts
			.iter()
			.map(|(w, _)| w.len())
			.max()
			.unwrap_or_default();
		width = std::cmp::min(width, 70);

		for (word, count) in &self.sorted_counts {
			writeln!(f, "{word:<width$}: {count}")?;
		}

		Ok(())
	}
}

impl PartialEq for FileStatistics<Unsorted> {
	fn eq(&self, other: &Self) -> bool {
		self.counts == other.counts
	}
}

impl PartialEq for FileStatistics<Sorted> {
	fn eq(&self, other: &Self) -> bool {
		// Using sorted_counts could cause issues depending on the order of elements with the same
		// frequency. I.e. [(hello: 1), (world: 1)] != [(world: 1), (hello: 1)].
		self.counts == other.counts
	}
}

impl Eq for FileStatistics {}
