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

WFCatSearchQuery.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2015 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remains the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated 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 Systems Incorporated.

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

local Rx = require 'Rx'

local WFLogger = require 'WFLogger'

local WFArguments = require 'WFArguments'
local KSTableUtils = require 'KSTableUtils'
local WFRxScheduler = require 'WFRxScheduler'

local Observable = Rx.Observable

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

local logger = WFLogger 'WFCatSearchQuery'
--	:setLevel 'debug'

local check = assert( WFArguments.check )
local derefOpt = KSTableUtils.derefOpt

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

local intOrAlphaOperators = {
	['<'] = true,
	['<='] = true,
	['=='] = true,
	['>='] = true,
	['>'] = true,
	['!='] = true,
}

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

local kDefaultQueryCalmRate = 2

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

local function makeQueryParamsStream( inQueryParams )

	local copiedQueryParams =
			KSTableUtils.shallowcopy( inQueryParams )

	-- We ensure that these query params include a calm param.
	--
	-- The query params are ultimately sent to
	--
	-- 	indexStore:query( queryParams )
	--
	-- Without a calm param, indexStore:query( queryParams ) queries will not
	-- deliver initial results or updates until the underlying index store
	-- tables are up to date.
	--
	-- A particularly egregious case is when the index store is updating either
	-- the albums or album assets table. If there is a big backlog, the album
	-- contents seed query will be delayed for a long time unless calm is
	-- present.
	--
	copiedQueryParams.calm = copiedQueryParams.calm or kDefaultQueryCalmRate

	return Observable.just( copiedQueryParams )

end

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

local function intOrAlphaQueryParamStream( searchDescription, predicate )

	local sql_table = searchDescription.sql_table
	local column = searchDescription.column
	local operator = predicate.operator
	assert( intOrAlphaOperators[ operator ], 'Operator "' .. tostring( operator ) .. '" is not a valid operator' )

	local queryParams = {
		sql = string.format( [[SELECT docId FROM %s WHERE %s %s ? ORDER BY docId]],
			sql_table,
			column,
			operator ),
	}

	for i, termKey in ipairs( searchDescription.query_params or { searchDescription.term } ) do
		queryParams[ i ] = predicate[ termKey ]
	end

	return makeQueryParamsStream( queryParams )

end

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

local function fullTextSearchQueryParamsStream( sql, text )
	return makeQueryParamsStream( {
		sql = sql,
		text
	} )
end

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

local function reviewQueryParamsStream( sql, values )
	if type(values) == "table" then
		local n = #values
		
		return makeQueryParamsStream( {
			sql = sql,
			 unpack( values, 1, n )
		} )
	else
		-- we can get the number or string equivlanet
		local numberEq  = tonumber(values)
		if numberEq == nil then
			numberEq = require 'WFCatSearchByReview'.getValueForReviewStatus(values)
		else
			numberEq = values
		end
		return makeQueryParamsStream( {
			sql = sql,
			numberEq
			} )
	end
	
end

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

local function ratingQueryParamsStream( searchDescription, predicate )

	local sql = searchDescription.sql
	--[[
	Rani : other operators such as <= and >= are also supported
	if predicate.operator and predicate.operator ~= '==' then
		assert( intOrAlphaOperators[ predicate.operator ], 'operator invalid for SQL' )
	end
	--]]
	
	sql = string.replace( sql, '%OPERATOR', predicate.operator or '==' )
		-- For data like ratings, favorites and reviews, we need to both
		-- include the user as part of the query, but may want to do
		-- logical operations on the other part (e.g. rating > some value).
		
	local valueKey = searchDescription.term
	local value = predicate[ valueKey ]
	
	return makeQueryParamsStream( {
		sql = sql,
		value
	} )
	
end

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

local function editedQueryParamsStream( searchDescription, predicate )
	
	local sql = searchDescription.sql
	
	sql = string.replace( sql, '%OPERATOR', predicate.operator or '==' )
		
	local valueKey = searchDescription.term
	local value = predicate[ valueKey ]
	
	return makeQueryParamsStream( {
		sql = sql,
		value
	} )

end

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

local function dateInfoDynamicQueryParamsStream( searchDescription, predicate )

	local columnInfo = predicate.columnInfo
	local sql_table = searchDescription.tableName
	
	assert ( columnInfo , 'ColumnInfo expected' )
	
	local n = 0
	local searchConditions = ""
	local values = {}
	for columnName, columnValue in pairs ( columnInfo ) do
	
		assert( searchDescription.allowedColumnNames[ columnName ], 'Column "' .. 
				columnName .. '" is not a valid for dateRangeOperations' )
		
		if n > 0 then
			searchConditions = searchConditions .. ' and '
		end
		
		table.insert( values, columnValue )
		searchConditions = searchConditions .. columnName .. " == ?"
				
		n = n + 1
	end
	
	local sql  = string.format( [[SELECT docId FROM %s WHERE %s  ORDER BY docId]],
			sql_table,
			searchConditions )
	
	return makeQueryParamsStream( {
		sql = sql,
		unpack( values, 1, n ) 
	} )
	
end


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

local function dateRangeQueryParamsStream( searchDescription, predicate )

	local sql = searchDescription.sql	
	local columnName = predicate.column
	assert( searchDescription.allowedColumnNames[ columnName ], 'Column "' .. columnName .. '" is not a valid for dateRangeOperations' )
	
	
	sql = string.replace( sql, '%COLUMN_NAME', columnName )
	local query_terms = searchDescription.query_params
	
	local values = {}
	local n = 0
	for _, term in ipairs ( query_terms ) do
		local termVal = predicate[ term ]
		if termVal then
			n = n + 1
			values [ n ] =  termVal
		end
	end
	
	return makeQueryParamsStream( {
		sql = sql,
		unpack( values, 1, n ) 
	} )
	
end

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

local function albumAssetsQueryParamsStream( searchDescription, predicate )

	local sql = searchDescription.sql

	local localAlbumIdKey = searchDescription.term

	local localAlbumId = predicate[ localAlbumIdKey ]

	if type( localAlbumId ) ~= "number" then

		logger:warnf( "Non localAlbumId ( '%s' ) passed to albumAssets query, search will return empty results",
						localAlbumId )

	end

	return makeQueryParamsStream( {
		sql = sql,
		localAlbumId	
	} )

end

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

local function simpleSQLQueryParamsStream( sql )

	return makeQueryParamsStream( {

		sql = sql,

	} )

end

--------------------------------------------------------------------------------
-- This emits a query of form
-- 
--   SELECT docId FROM someTable WHERE someTableColumn = ? ORDER by docId ASC

local function singlePlaceholderStringLookupQueryParamsStream( 
													searchDescription,
													predicate )

	local sql = searchDescription.sql

	local placeholderColumnValueKey = searchDescription.query_params[ 1 ]

	local value = predicate[ placeholderColumnValueKey ]

	return makeQueryParamsStream( {
		sql = sql,
		value
	} )

end

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

local syncStatusOperators = {
	['IN'] = true,
	['NOT IN'] = true,
}

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

local WFCatSearchBySyncStatus = require 'WFCatSearchBySyncStatus'

local function syncStatusQueryParamsStream( searchDescription, predicate )
	
	if predicate.operator then
		assert( syncStatusOperators[ predicate.operator ], 'operator invalid for SQL' )
	end

	local sql = string.replace( searchDescription.sql, '%OPERATOR', predicate.operator or 'IN' )
	
	local syncFilterValue = searchDescription.query_params and predicate[ searchDescription.query_params[ 1 ] ]
	
	if WFCatSearchBySyncStatus.isMarkedForOfflineFilter( syncFilterValue ) then
	
		sql = WFCatSearchBySyncStatus.getMarkedForOfflineSql() 
	
	elseif WFCatSearchBySyncStatus.isOldMarkedForOfflineFilter( syncFilterValue ) then
	
		sql = WFCatSearchBySyncStatus.getOldMarkedForOfflineSql() 
	
	else
	
		local syncStatusValues = WFCatSearchBySyncStatus.getStatusIdsFromFilterValue( syncFilterValue )
		
		local syncStatus = " ( "
		
		for _, syncStatusId in ipairs( syncStatusValues ) do
			syncStatus = syncStatus .. tostring( syncStatusId )
			syncStatus = syncStatus .. ", "
		end

		if #syncStatusValues > 0 then
			syncStatus = syncStatus:sub( 1, -3 )  -- remove trailing ', '	
		end
		
		syncStatus = syncStatus .. " ) "
		
		sql = string.format( sql, syncStatus )
	
	end
	
	return makeQueryParamsStream( { sql = sql, } )
	
end

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

local function makeCompoundQueryParams( operator, ... )

	return {

		operator = operator,

		n = select( '#', ... ),

		...

	}

end

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

local function makeCompoundQueryParamsStream( operator )

	return function ( ... )

		return Observable.combineLatest( 
				makeCompoundQueryParams,
					Observable.just( operator ), ... )

	end

end

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

-- date test w 10,000 -> 0.022
--==============================================================================

local makeQueryParamsWithOrStream = makeCompoundQueryParamsStream( 'or' )

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

local makeQueryParamsWithAndStream = makeCompoundQueryParamsStream( 'and' )

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

local function createQueryParamsStreamImp( searchSchema, predicate )

	local searchType = predicate and predicate.type

	if searchType == 'all' or predicate == nil then
		return
	end

	if searchType == 'or' or searchType == 'and' then
	
		local compoundQueryParamsStreamFn =
				searchType == 'or' and  
					makeQueryParamsWithOrStream or makeQueryParamsWithAndStream

		local terms = assert( predicate.terms, '"' .. searchType .. '" query type requires "terms" array' )

		local subTerms = {}

		for _, subpredicate in ipairs( terms ) do

			subTerms[ #subTerms + 1 ] = 
				createQueryParamsStreamImp( searchSchema, subpredicate )

		end

		if #subTerms > 1 then

			return compoundQueryParamsStreamFn( unpack( subTerms ) )

		else

			return subTerms[ 1 ]

		end

	end

	local searchDescription = searchSchema:getSearchDescription( searchType )
	if not searchDescription then
		return nil, 'Invalid search type: ' .. tostring( searchType or '< missing >' )
	end

	local dataType = searchDescription.dataType

	if dataType == 'noOp' then

		return

	end

	if dataType == 'uint' or dataType == 'alpha' or dataType == 'int' then

		return intOrAlphaQueryParamStream( searchDescription, predicate )

	end

	if dataType == 'albumAssets' or dataType == 'projectAlbumAssets' then

		return albumAssetsQueryParamsStream( searchDescription, predicate )

	end
	
	if dataType == 'autoTagsTerm' or -- string
			dataType == 'asset_typesTerm' or -- string
			dataType == 'keywordsTerm' or -- string
			dataType == 'extensionsTerm' or -- string
			dataType == 'locationsTerm' or -- string
			dataType == 'onlineSearchTerm' or -- string
			dataType == 'camerasTerm' then -- string

		return singlePlaceholderStringLookupQueryParamsStream( 
				searchDescription, predicate )

	end
	
	if dataType == 'fullTextSearch' then

		return fullTextSearchQueryParamsStream( 
				searchDescription.sql, 
				predicate[ searchDescription.query_params ] )

	end
	
	if dataType == "peopleSearch" then
	
		local text = predicate[ searchDescription.query_params ]
		
		return makeQueryParamsStream( {
					sql = searchDescription.sql,
					text,
					text
				} )
	end

	if dataType == 'reviewTerm' then

		return reviewQueryParamsStream( 
					searchDescription.sql, 
					predicate[ searchDescription.query_params[1] ] )

	end

	if dataType == 'ratingTerm' then

		return ratingQueryParamsStream( searchDescription, predicate )
	
	end
	
	if dataType == 'editedTerm' then

		return editedQueryParamsStream( searchDescription, predicate )

	end

	if dataType == 'dateRangeTerm' then

		return dateRangeQueryParamsStream( searchDescription, predicate )

	end

	if dataType == 'dateInfoDynamicTerm' then

		return dateInfoDynamicQueryParamsStream( searchDescription, predicate )

	end

	if dataType == 'syncStatusTerm' then

		return syncStatusQueryParamsStream( searchDescription, predicate )

	end


	if dataType == 'none' then

		return simpleSQLQueryParamsStream( searchDescription.sql )

	end
	
	
	return nil, 'unsupported dataType: ' .. tostring( dataType or '< unspecified >' )

end

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

--[[ Search Description is a search as defined here: http://belt.sea.adobe.com/asset/search.html

	searchDesc = {
		predicates = {
			type = '< predicate type >',
			... other key/value pairs specific to this type
		}
	}

	@return an Observable with the search results
]]

local function createQueryParamsStream( searchSchema, searchDescription )

	local predicates = searchDescription and searchDescription.predicates

	local maybeQueryParamsStream, maybeError = 
			createQueryParamsStreamImp( searchSchema, predicates )

	if not maybeQueryParamsStream and maybeError then

		error( maybeError )

	end

	return maybeQueryParamsStream

end

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

local function count( indexStore, searchTerm )

	check( 'WFRxIndexStore', '?table' )

	logger:infof( "count searchTerm = %s\n\nvia %s\n\n",
				  searchTerm, ':traceback:' )

	return ( searchTerm or Observable.just() )

		:debounce( 0.1, WFRxScheduler.sharedScheduler() )
		:map( function( predicate )
			if predicate then
				return indexStore:querySetLogic {
					predicate
				}
				:map( function( resultSet )
					return resultSet.length
				end )
			else
				return indexStore:query {
					sql = [[SELECT count( docId ) 
							FROM assets 
							WHERE isMedia = 1 AND spaceId IS NULL]]
				}
				:map( function( rows )
					return derefOpt( rows, 1, 1 )
				end )
			end
		end )
		:switch()

end

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

local function exists( indexStore, searchTerm )

	check( 'WFRxIndexStore', '?table' )

	logger:infof( "count searchTerm = %s\n\nvia %s\n\n",
				  searchTerm, ':traceback:' )

	return ( searchTerm or Observable.just() )

		:debounce( 0.1, WFRxScheduler.sharedScheduler() )
		:map( function( predicate )
			if predicate then
				return indexStore:querySetLogic {
					predicate
				}
				:map( function( resultSet )
					return ( resultSet.length or 0 ) > 0
				end )
			else
				return indexStore:queryOnce {
					sql = [[ SELECT EXISTS (SELECT 1
							FROM assets
							WHERE isMedia = 1 AND spaceId IS NULL) ]]
				}
				:map( function( rows )
					return ( derefOpt( rows, 1, 1 ) or 0 ) > 0
				end )
			end
		end )
		:switch()

end

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

local function resultSetQueryImp( indexStore, sqlTemplateWithAtJOINPlaceholder,
									resultSetQueryParamsStream,
									resultSetJoinColumn, maybeCalmRate )

	check( 'WFRxIndexStore', 'string' )

	if not resultSetQueryParamsStream then

		logger:warnf( "We got a virtual result set query without a virtual result set stream. Defaulting to a normal query: %s",
						sqlTemplateWithAtJOINPlaceholder )

		return indexStore:query( {
				sql = sqlTemplateWithAtJOINPlaceholder,
				calm = maybeCalmRate } )

	end

	assert( type( resultSetJoinColumn ) == "string",
				"resultSetJoinColumn must be a string" )

	local joinSqlStream =
			resultSetQueryParamsStream
				-- TODO: use :calm instead? :debounce introduces a 0.1 second lag
				:debounce( 0.1, WFRxScheduler.sharedScheduler() )
				:flatMapLatest(
					function( resultSetQueryParams )

						if type( resultSetQueryParams ) ~= "table" then

							return Observable.just( false )

						end

						if resultSetQueryParams.sql then

							resultSetQueryParams = {

								operator = 'or',

								resultSetQueryParams,

							}

						end

						-- TODO add calm factor to resultSetQueryParams

						return indexStore
								:newVirtualTableForSetQuery( resultSetQueryParams )

					end )
				:map( 
					function( resultSetVirtualTableName )

						if type( resultSetVirtualTableName ) ~= "string" then

							return ""

						end

						return string.format( 'JOIN %s AS vt ON %s == vt.id', 
												resultSetVirtualTableName, 
												resultSetJoinColumn )

					end )

	return joinSqlStream
			:flatMapLatest(
				function( joinSql )

					local sql = sqlTemplateWithAtJOINPlaceholder
									:replace( "@JOIN", joinSql )

					return indexStore:query( {

								sql = sql,
								
								calm = maybeCalmRate

							} )

				end )
				:tapOnCompleted( function()

					resultSetQueryParamsStream:close()

				end )

end

--------------------------------------------------------------------------------
-- A resultSetQuery is two separate index store queries. The resultSet query 
-- generates a list of asset ids that are saved in a virtual result set and fed
-- to the query using sql that is JOIN against the result set.
--------------------------------------------------------------------------------

local function resultSetQuery( indexStore, sql, resultSetQueryParamsStream,
								resultSetJoinColumn )

	return resultSetQueryImp( 
			indexStore, sql, resultSetQueryParamsStream, resultSetJoinColumn,
			kDefaultQueryCalmRate )

end

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

local function uncalmedResultSetQuery( indexStore, sql,
										resultSetQueryParamsStream,
										resultSetJoinColumn )

	return resultSetQueryImp( 
			indexStore, sql, resultSetQueryParamsStream, resultSetJoinColumn )

end

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

return {

	createQueryParamsStream = createQueryParamsStream,

	count = count,
	
	exists = exists,

	defaultQueryCalmRate = kDefaultQueryCalmRate,

	resultSetQuery = resultSetQuery,

	privateUnitTestExports = {

		makeTerm = makeQueryParamsStream,

		makeCompoundOrTerm = makeQueryParamsWithOrStream,

		makeCompoundAndTerm = makeQueryParamsWithAndStream,

		uncalmedResultSetQuery = uncalmedResultSetQuery,

	}

}
