{-
	Copyright (C) 2010-2016 Dr. Alistair Ward

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
-}
{- |
 [@AUTHOR@]	Dr. Alistair Ward

 [@DESCRIPTION@]	A data-type which references a set of files by their paths, and qualifies them with their aggregate size.
-}

module Squeeze.Data.FileCombination(
-- * Types
-- ** Data-types
	FileCombination(
--		MkFileCombination,
		getAggregateFileSize,
		getFilePathList
	),
-- * Constants
	nullFileCombination,
-- * Functions
	comparingAggregateFileSize,
	prepend,
	risingFilter,
--	risingMerge,
	risingMergeByAggregateFileSize,
-- ** Constructors
	mkFileCombination,
	singleton,
-- ** Predicates
	hasSizeBy
) where

import qualified	Data.List
import qualified	Data.Ord
import qualified	Squeeze.Data.File	as Data.File
import qualified	ToolShed.Data.Foldable
import qualified	ToolShed.SelfValidate

-- | Declare a list of files qualified by its aggregate size.
data FileCombination	= MkFileCombination {
	getAggregateFileSize	:: !Data.File.FileSize,		-- ^ The aggregate size of the files referenced by 'getFilePathList'.
	getFilePathList		:: Data.File.FilePathList	-- ^ A list of paths, defining a set of files.
} deriving Eq

instance Show FileCombination where
	showsPrec _ fileCombination	= shows (getAggregateFileSize fileCombination) . showChar '\t' . shows (Data.List.sort $ getFilePathList fileCombination)

instance ToolShed.SelfValidate.SelfValidator FileCombination	where
	getErrors fileCombination	= ToolShed.SelfValidate.extractErrors [
		(
			getAggregateFileSize fileCombination < 0,
			"aggregate size must be positive; " ++ show fileCombination
		), (
			getAggregateFileSize fileCombination /= 0 && null (getFilePathList fileCombination),
			"when zero files are specified, the aggregate size must also be zero; " ++ show fileCombination
		), let
			duplicateFilePaths	= map head . filter ((> 1) . length) . ToolShed.Data.Foldable.gather $ getFilePathList fileCombination
		in (
			not $ null duplicateFilePaths,
			"duplicate file-paths have been specified; " ++ show duplicateFilePaths
		)
	 ]

-- | A constant empty instance.
nullFileCombination :: FileCombination
nullFileCombination	= MkFileCombination 0 []

-- | Smart constructor.
mkFileCombination :: Data.File.FileSize -> Data.File.FilePathList -> FileCombination
mkFileCombination fileSize filePathList
	| ToolShed.SelfValidate.isValid fileCombination	= fileCombination
	| otherwise					= error $ "Squeeze.Data.FileCombination.mkFileCombination:\t" ++ ToolShed.SelfValidate.getFirstError fileCombination
	where
		fileCombination	= MkFileCombination fileSize filePathList

-- | Construct a 'FileCombination' from a single 'Data.File.FileSizeAndPath'.
singleton :: Data.File.FileSizeAndPath -> FileCombination
singleton (fileSize, filePath)	= mkFileCombination fileSize [filePath]

{- |
	* Prepend a 'Data.File.FileSizeAndPath' to an existing 'FileCombination'.

	* CAVEAT: performance hot-spot, so it by-passes the checks made by 'mkFileCombination'.
-}
{-# INLINE prepend #-}
prepend
	:: Data.File.FileSizeAndPath	-- ^ The new path to prepend to the incumbent file-combination.
	-> FileCombination		-- ^ The incumbent combination of files.
	-> FileCombination
prepend (fileSize, filePath) MkFileCombination {
	getAggregateFileSize	= aggregateFileSize,
	getFilePathList		= filePathList
} = MkFileCombination {
	getAggregateFileSize	= fileSize + aggregateFileSize,
	getFilePathList		= filePath : filePathList
}

-- | Predicate used to determine whether a specific file-combination matches a size-related requirement.
{-# INLINE hasSizeBy #-}
hasSizeBy
	:: (Data.File.FileSize -> Bool)	-- ^ The predicate.
	-> FileCombination		-- ^ The input datum to be tested.
	-> Bool
hasSizeBy predicate MkFileCombination { getAggregateFileSize = aggregateFileSize }	= predicate aggregateFileSize

-- | Progressively raises the selection-criterion as each match is found, to produce monotonically increasing file-combinations.
risingFilter
	:: Data.File.FileSize	-- ^ The initial minimum byte-size of file to accept.
	-> [FileCombination]	-- ^ The input list of files to filter.
	-> [FileCombination]	-- ^ The resulting list of files, which have met rising criterion.
risingFilter _ []	= []
risingFilter minimumSize (fileCombination@MkFileCombination { getAggregateFileSize = aggregateFileSize } : fileCombinations)
	| aggregateFileSize >= minimumSize	= fileCombination : risingFilter aggregateFileSize fileCombinations
	| otherwise				= risingFilter minimumSize fileCombinations

{- |
	* Merges two lists of monotonically increasing values, into a single monotonically increasing list, by dropping values which compare less than results already found.

	* CAVEAT: both lists must produce an element, in order to determine which is selected.
	As a result, though one list produces a value, it can't be returned until the other does (which make take a long time), even if ultimately the first is then selected.
-}
risingMerge
	:: (FileCombination -> FileCombination -> Ordering)	-- ^ Comparator used to select the best file-combination from the heads of the two list supplied.
	-> [FileCombination]					-- ^ A list of monotonically increasing file-combinations.
	-> [FileCombination]					-- ^ A list of monotonically increasing file-combinations.
	-> [FileCombination]
risingMerge cmp	= slave nullFileCombination	where
	lessThan bar	= (== LT) . (`cmp` bar)

	slave bar [] r			= dropWhile (lessThan bar) r
	slave bar l []			= dropWhile (lessThan bar) l
	slave _ (x : xs) (y : ys)	= case x `cmp` y of
		GT	-> x : slave x xs ys
		LT	-> y : slave y xs ys
		_	-> x : y : slave x xs ys

-- | Compares two /file-combination/s by their aggregate file-size.
comparingAggregateFileSize :: FileCombination -> FileCombination -> Ordering
comparingAggregateFileSize	= Data.Ord.comparing getAggregateFileSize

{- |
	Merges two lists of monotonically increasing lists of file-combinations,
	into a single monotonically increasing list,
	by dropping values which have a smaller aggregate size than results already found.
-}
risingMergeByAggregateFileSize
	:: [FileCombination]	-- ^ A list of monotonically increasing file-combinations.
	-> [FileCombination]	-- ^ A list of monotonically increasing file-combinations.
	-> [FileCombination]
risingMergeByAggregateFileSize	= risingMerge comparingAggregateFileSize