--[[----------------------------------------------------------------------------

NmbCatalogDashboard.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2020 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

----------------------------------------------------------------------------]]--

local WFFunctionUtils = require "WFFunctionUtils"

	-- RxLua
local Rx = require "Rx"

	-- WF.core.utils
local KSStringUtils = require 'KSStringUtils'
local KSTableUtils = require 'KSTableUtils'
local WFProgressReporter = require 'WFProgressReporter'

	-- WF.oz_client.managed_catalog
local WFCatIndexUtils = require 'WFCatIndexUtils'
local WFRxIndexStoreSchema = require 'WFRxIndexStoreSchema'

	-- WF.oz_client.managed_catalog.metadata_formatters
local WFCatFormatterUtils = require 'WFCatFormatterUtils'

--------------------------------------------------------------------------------

local derefOpt = KSTableUtils.derefOpt

local isValidAssetDocument = assert( WFCatIndexUtils.isValidAssetDocument )

--------------------------------------------------------------------------------

local maxSimultaneous = 5

--==============================================================================

-- SPAM [jandrew 2020-02-20]: The following formatting code was adapted from
-- WFCatFormatter_exif.lua

local function numericValue( val )
	return WFCatFormatterUtils.convertToNumericValue( val ) or 0
end

------------------------------------------------------------------------------

local function formatNumeratorDenominator( num, den )

	if den == 0 then
		return ""
	end

	den = math.floor( den + 0.5 )
	num = math.floor( num + 0.5 )

	return num, den

end

------------------------------------------------------------------------------

local function localizedFractionalSeconds( val )

	local numerator, denominator = WFCatFormatterUtils.extractNumeratorDenominator( val )
	
	if numerator then

		local fractionString = 0
		
		if numerator > denominator then
			fractionString = KSStringUtils.numberToString( numerator/denominator, 1 )
			return string.format( "%s sec", fractionString )
		else
			local num, den = formatNumeratorDenominator( numerator, denominator )
			return string.format( "%d/%d sec", num, den )
		end

	end

end

------------------------------------------------------------------------------

local pointThreeBot = 0.275
local pointThreeTop = 0.31667

local function getDecimalBasedDisplayValue( dec )

	if 0.95 < dec and dec < 1.0 then
		return 1
	elseif 0.85 <= dec and dec < 0.95 then
		return 0.9
	elseif 0.75 <= dec and dec < 0.85 then
		return 0.8
	elseif 0.65 <= dec and dec < 0.75 then
		return 0.7
	elseif 0.55 <= dec and dec < 0.65 then
		return 0.6
	elseif 0.45 <= dec and dec < 0.55 then
		return 0.5
	elseif 0.35 <= dec and dec < 0.45 then
		return 0.4
	elseif pointThreeBot <= dec and dec < pointThreeTop then
		return 0.3
	end

end

local function apexToNumDenom( n )
	return 1, math.pow( 2, n )
end

local function convertAPEXToShutterSpeed( shutterSpeedVal, preferASCII )

	local numResult = 0

	if type( shutterSpeedVal ) == "number" then

		numResult = shutterSpeedVal

	else

		local denominator = shutterSpeedVal[ 2 ]

		if denominator ~= 0 then
			numResult = numericValue( shutterSpeedVal )
		else
			return ""
		end

	end

	if numResult < 1 and numResult > 0 then

		numResult = math.pow( 2 , -numResult )
		local decimalValue = getDecimalBasedDisplayValue( numResult )
		return string.format( "%d sec", decimalValue )
		
	elseif numResult < 0 then
	
		numResult = math.pow( 2 , -numResult )
		return string.format( "%d sec", numResult )
		
	else

		local n, d = apexToNumDenom( numResult )
		local decimalValue = getDecimalBasedDisplayValue( n / d )

		if decimalValue then
			return string.format( "%d sec", decimalValue )
		else
			numResult = math.pow( 2, numResult )
			return localizedFractionalSeconds( { 1, numResult }, preferASCII )
		end

	end

end

------------------------------------------------------------------------------

local function formatExposure( assetDoc )

	local shutterSpeed = derefOpt( assetDoc, 'xmp', 'exif', 'ShutterSpeedValue' )
	local FStopVal = derefOpt( assetDoc, 'xmp', 'exif', 'FNumber' )
	local preferASCII = false

	if FStopVal and shutterSpeed then

		shutterSpeed = convertAPEXToShutterSpeed( shutterSpeed, preferASCII )
		local fstop = numericValue( FStopVal )
		
		return string.format( "%s at %d", shutterSpeed, fstop )

	end
	
	if shutterSpeed then
		return convertAPEXToShutterSpeed( shutterSpeed, preferASCII )
	end
	
	if FStopVal then
		return numericValue( FStopVal )
	end

end

------------------------------------------------------------------------------

local function formatFocalLengthValue( val )

	if val then

		local focalLength = numericValue( val )
		local precision = 4 - math.max( 0, math.floor( math.log10( focalLength ) ) )

		return focalLength
		
	end

end

------------------------------------------------------------------------------

local function getLensFromDocValue( docValue )

	return derefOpt( docValue, 'importSource', 'lens', 'name' )
			or derefOpt( docValue, 'xmp', 'aux', 'Lens' ) 

end

------------------------------------------------------------------------------

local function formatISO( assetDoc )

	local iso = derefOpt( assetDoc, 'xmp', 'exif', 'ISOSpeedRatings' )
	
	if type( iso ) == 'table' then
		iso = iso[ 1 ]
	end

	return iso

end

------------------------------------------------------------------------------

local function generateIndexSchema( indexStoreSchema )

	indexStoreSchema:addTable {

		sql = [[CREATE TABLE exifData( lens, fstop, focalLength, exposure, shutterSpeed, iso )]],

		updateLazily = true,

		useLocalId = true,

		relevantTypeSubtypeTuples = {
			{ 'asset', 'image' },
			{ 'asset', 'video' },
		},
		
		includeDocumentsFromSource = WFRxIndexStoreSchema.sourceCatalogDocuments,
			-- Don't index space-feed documents.
			
		map = function( docValue, emit )

			if isValidAssetDocument( docValue ) then
				
				-- Lens
				
				local lens = getLensFromDocValue( docValue )
				
				-- F/Stop
				
				local rawFstop = derefOpt( docValue, 'xmp', 'exif', 'FNumber' )
				local fstop = rawFstop and numericValue( rawFstop )

				-- Focal Length
				
				local rawFocalLength = derefOpt( docValue, 'xmp', 'exif', 'FocalLength' )
				local focalLength = rawFocalLength and formatFocalLengthValue( rawFocalLength )
				
				-- Exposure
				
				local exposure = formatExposure( docValue )
				
				-- Shutterspeed
				
				local rawShutterSpeed = derefOpt( docValue, 'xmp', 'exif', 'ShutterSpeedValue' )
				local shutterSpeed = rawShutterSpeed and convertAPEXToShutterSpeed( rawShutterSpeed )
		
				-- ISO
				
				local iso = formatISO( docValue )
				
				emit( lens, fstop, focalLength, exposure, shutterSpeed, iso )
				
			end

		end,
		
		hasOneRowPerDocument = true,

	}

end

--------------------------------------------------------------------------------

local function createExifQueryStr( column )

	-- We purposefully limit the responses to 50 in order to prevent there being
	-- too many datapoints on the generated graphs.

	return string.format( [[SELECT * FROM (
			SELECT %s, count( %s ) as countOf 
			  FROM exifData 
			  WHERE %s IS NOT NULL
			  GROUP BY %s
			  ORDER BY countOf DESC
			  LIMIT 50)
			ORDER BY %s ASC]], column, column, column, column, column )

end

--------------------------------------------------------------------------------

-- Step 1: Define your gatherer function:

-- The gatherer function must return a single table of results, along with
-- the name of the metric.

--------------------------------------------------------------------------------

local function appendRow( t, row )

	t[ #t + 1 ] = row

	return t

end

--------------------------------------------------------------------------------

local function gatherCountsStat( wfCatalog, statName, statColNames, sql )

	return wfCatalog:indexStore():queryOnce( {

			sql = sql,
				
			quiesceUntilUpToDate = true
		
		} )
		
		:map( 
			function( rows )
		
				return statName,
						WFFunctionUtils.reduce( { statColNames },
							appendRow,
								rows )

			end )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByDate( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Date",
			{ "month", "count" },
			[[SELECT completeMonth, count( completeMonth ) as countOf 
			  FROM captureDate
			  INNER JOIN assets ON assets.docId = captureDate.docId
			  WHERE assets.subtype IN ( 'image', 'video' )
			  	AND completeMonth > 0
				AND spaceId is NULL
			  GROUP BY completeMonth
			  ORDER BY completeMonth ASC]] )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByCamera( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Camera",
			{ "camera", "count" },
			[[SELECT camera, count( camera ) as countOf 
			  FROM cameras 
			  INNER JOIN assets ON assets.docId = cameras.docId
			  WHERE camera IS NOT NULL
			  	AND spaceId IS NULL
			  GROUP BY camera
			  ORDER BY countOf DESC]] )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByMediaType( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Media Type",
			{ "media", "count" },
			[[SELECT type, count( type ) as countOf 
			  FROM mediaTypes
			  INNER JOIN assets ON assets.docId = mediaTypes.docId
			  WHERE spaceId IS NULL
			  GROUP BY type
			  ORDER BY countOf DESC]] )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByLens( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Lens",
			{ "lens", "count" },
			[[SELECT lens, count( lens ) as countOf 
			  FROM exifData 
			  WHERE lens IS NOT NULL
			  GROUP BY lens
			  ORDER BY countOf DESC]] )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByFstop( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By F-Stop",
			{ "f-stop", "count" },
			createExifQueryStr( 'fstop' ) )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByFocalLength( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Focal Length",
			{ "focal length", "count" },
			createExifQueryStr( 'focalLength' ) )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByISO( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By ISO",
			{ "ISO", "count" },
			createExifQueryStr( 'iso' ) )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByExposure( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Exposure",
			{ "exposure", "count" },
			createExifQueryStr( 'exposure' ) )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByShutterSpeed( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Shutter Speed",
			{ "shutter speed", "count" },
			createExifQueryStr( 'shutterSpeed' ) )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByLocation( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Location",
			{ "location", "count" },
			[[SELECT location, count( location ) as countOf 
			  FROM locations
			  INNER JOIN assets ON assets.docId = locations.docId
			  WHERE spaceId IS NULL
			  GROUP BY location
			  ORDER BY countOf DESC
			  LIMIT 15]] )
		
end

--------------------------------------------------------------------------------

local function gatherPhotographsByRating( wfCatalog )

	return gatherCountsStat(
			wfCatalog,
			"Photographs By Rating",
			{ "rating", "count" },
			[[SELECT ratings.rating, count( ratings.rating ) as countOf 
			  FROM ratings 
			  INNER JOIN assets ON assets.docId = ratings.docId
			  WHERE ratings.rating > 0
			  	AND spaceId IS NULL
			  GROUP BY ratings.rating
			  ORDER BY countOf DESC]] )
		
end

--------------------------------------------------------------------------------

local function gatherCamerasOverTime( wfCatalog )

	return wfCatalog:indexStore():queryOnce( {

			sql = [[SELECT year, camera, count( camera ) as countOf 
			  FROM cameras
			  INNER JOIN captureDate ON cameras.docId = captureDate.docId
			  INNER JOIN assets ON assets.docId = cameras.docId
			  WHERE camera IS NOT NULL
			  	AND spaceId IS NULL
			  GROUP BY year, camera
			  ORDER BY year ASC]],
				
			quiesceUntilUpToDate = true
		
		} )
		
		:map( 
			function( rows )
		
				-- FIXME [jandrew 2020-02-21]: The following is super ugly,
				-- but hackathon time is running out. This could easily be
				-- simplified with a better query and better code.
		
				local countsByCameras = {}
				local seenYears = {}
				local cameras = {}
				
				local years = {}
				
				for _, row in ipairs( rows ) do
				
					local year = row[ 1 ]
					local camera = row[ 2 ]
					local count = row[ 3 ]
					
					if not seenYears[ year ] then
						years[ #years + 1 ] = year
					end
					
					seenYears[ year ] = true
					cameras[ camera ] = true
					
					local countsForCamera = countsByCameras[ camera ] or {}
					countsForCamera[ year ] = count
					countsByCameras[ camera ] = countsForCamera
					
				end
				
				-- Make sure each camera has a count of 0 for years in which
				-- there were no photographs.
				
				local cameraCounts = {}
				
				for _, year in ipairs( years ) do

					for camera, _ in pairs( cameras ) do
					
						local cameraCount = cameraCounts[ camera ] or {}
					
						local countsForCameras = countsByCameras[ camera ] or {}
						
						cameraCount[ #cameraCount + 1 ] = countsForCameras[ year ] or 0
					
						cameraCounts[ camera ] = cameraCount
					
					end
				
				end
				
				local results = {
					years,
					cameraCounts
				}
						
				return "Camera Usage Over Time", results

			end )
		
end

--------------------------------------------------------------------------------

local function gatherLensesOverTime( wfCatalog )

	return wfCatalog:indexStore():queryOnce( {

			sql = [[SELECT year, lens, count( lens ) as countOf 
			  FROM exifData
			  INNER JOIN captureDate ON exifData.docId = captureDate.docId
			  INNER JOIN assets ON assets.docId = exifData.docId
			  WHERE lens IS NOT NULL
			  	AND spaceId IS NULL
			  GROUP BY year, lens
			  ORDER BY year ASC]],
				
			quiesceUntilUpToDate = true
		
		} )
		
		:map( 
			function( rows )
		
				-- FIXME [jandrew 2020-02-21]: The following is super ugly,
				-- but hackathon time is running out. This could easily be
				-- simplified with a better query and better code.
		
				local countsByLens = {}
				local seenYears = {}
				local lenses = {}
				
				local years = {}
				
				for _, row in ipairs( rows ) do
				
					local year = row[ 1 ]
					local lens = row[ 2 ]
					local count = row[ 3 ]
					
					if not seenYears[ year ] then
						years[ #years + 1 ] = year
					end
					
					seenYears[ year ] = true
					lenses[ lens ] = true
					
					local countsForLens = countsByLens[ lens ] or {}
					countsForLens[ year ] = count
					countsByLens[ lens ] = countsForLens
					
				end
				
				-- Make sure each lens has a count of 0 for years in which
				-- there were no photographs.
				
				local lensCounts = {}
				
				for _, year in ipairs( years ) do

					for lens, _ in pairs( lenses ) do
					
						local lensCount = lensCounts[ lens ] or {}
					
						local countsForLens = countsByLens[ lens ] or {}
						
						lensCount[ #lensCount + 1 ] = countsForLens[ year ] or 0
					
						lensCounts[ lens ] = lensCount
					
					end
				
				end
				
				local results = {
					years,
					lensCounts
				}
						
				return "Lens Usage Over Time", results

			end )
		
end

--------------------------------------------------------------------------------

-- Step 2: Add your gatherer function to the following table:

local gathererFns = {
	gatherPhotographsByDate,
	gatherPhotographsByCamera,
	gatherPhotographsByMediaType,
	gatherPhotographsByLens,
	gatherPhotographsByFstop,
	gatherPhotographsByFocalLength,
	gatherPhotographsByISO,
	gatherPhotographsByExposure,
	gatherPhotographsByShutterSpeed,
	gatherPhotographsByLocation,
	gatherPhotographsByRating,
	gatherCamerasOverTime,
	gatherLensesOverTime
}

--------------------------------------------------------------------------------

local function makeGatherFunctionResultStream( gatherFunc, wfCatalog )

	return gatherFunc( wfCatalog ):take( 1 )

end

--------------------------------------------------------------------------------

local function reportAndAccumulateGatherFuncResult( 
				gatherFuncStatus, gatherFuncName, gatherFuncStats )

	gatherFuncStatus.wfProgressReporter:reportSuccess()

	gatherFuncStatus.statistics[ gatherFuncName ] = gatherFuncStats

	return gatherFuncStatus

end

--------------------------------------------------------------------------------

local function gatherCatalogStatisticsImp( wfCatalog, wfProgressReporter )

	wfProgressReporter:setTotal( #gathererFns )
	
	local gatherFuncStatus = {

			wfProgressReporter = wfProgressReporter,

			statistics = {}

	}

	local gatherFuncResultStreams =
			WFFunctionUtils.map( 
				makeGatherFunctionResultStream, 
					gathererFns, wfCatalog )

	return Rx.Observable.from( gatherFuncResultStreams )
			:flatMerge( maxSimultaneous )
			:scan( reportAndAccumulateGatherFuncResult, gatherFuncStatus )
			:takeLast( 1 )
			:pluck( 'statistics' )

end

--------------------------------------------------------------------------------

local function gatherCatalogStatistics( wfCatalogStream, ... )

		-- Setup a progress reporter to report periodic progress while the
		-- statistics are being generated.
		
	return Rx.Observable.create(
		function( observer )
	
			local wfProgressReporter = 
					WFProgressReporter( "Gathering Catalog Statistics" )

			local progSubscription =
					wfProgressReporter:status():subscribe( observer )

			local reportSubscription =
				wfCatalogStream
					:flatMapLatest(
						function( wfCatalog )

							return gatherCatalogStatisticsImp( 
									wfCatalog, wfProgressReporter )

						end )
						:subscribe(
							function( statistics )
									
								wfProgressReporter:done( statistics )

							end,
							function( errStr )

								-- TODO report error here

							end )

			return Rx.CompositeDisposable( reportSubscription, progSubscription )

		end )

end

--------------------------------------------------------------------------------

local function makeModel( wfCatalogStream, ... )

	return {

		value = gatherCatalogStatistics( wfCatalogStream, ... ),

		-- submodels (probably not needed)
		models = {

		},

	}
	
end

--==============================================================================

return {

	makeModel = makeModel,
	generateIndexSchema = generateIndexSchema

}
