Module:Cast and crew

require('strict')

local p = {}

local getArgs = require('Module:Arguments').getArgs
local error_message = require('Module:Error')['error']
local TableTools = require('Module:TableTools')

local function getLink(args)
	-- Get the Wikidata item ID from the frame arguments
	local wikidataItem = args[1]
	if not wikidataItem then
		return error_message({'[[Module:Cast and crew]] error: no Wikidata item provided'})
	end
	
	-- Query Wikidata for sitelinks
	local entity = mw.wikibase.getEntity(wikidataItem)
	if not entity then
		return error_message({'[[Module:Cast and crew]] error: invalid or nonexistent Wikidata item'})
	end
	
	local label = entity:getLabel('en') or wikidataItem
	
	-- Check for an English Wikisource link
	local enwikisourceLink = entity:getSitelink('enwikisource')
	if enwikisourceLink then
		return '[[' .. enwikisourceLink .. '|' .. label .. ']]'
	end
	
	-- Check for an English Wikipedia link
	local enwikiLink = entity:getSitelink('enwiki')
	if enwikiLink then
		return '[[w:' .. enwikiLink .. '|' .. label .. ']]'
	end
	
	-- Fallback to a direct Wikidata link with label
	return '[[d:' .. wikidataItem .. '|' .. label .. ']]'
end
function p.getLink(args)
	return getLink(args)
end

function p._getCastList(args)
	local wikidataItem = args[1]
	if not wikidataItem then
		return error_message({'[[Module:Cast and crew]] error: no Wikidata item provided'})
	end
	
	-- Query Wikidata for cast member claims (P161)
	local entity = mw.wikibase.getEntity(wikidataItem)
	if not entity or not entity.claims then
		return nil
	end
	
	local castMembers = entity.claims['P161']
	if not castMembers then
		castMembers = entity.claims['P725'] -- voice actors
	end
	if not castMembers then
		return nil
	end
	
	local leadingActors = {}
	local otherCastMembers = {}
	local uncreditedActors = {}
	
	for _, castMember in ipairs(castMembers) do
		local castMemberQID = (castMember.mainsnak.datavalue and castMember.mainsnak.datavalue.value.id) or nil
		local roleClaims = (castMember.qualifiers and (castMember.qualifiers['P453'] or castMember.qualifiers['P4633'])) or nil
		local characteristicClaims = (castMember.qualifiers and castMember.qualifiers['P1552']) or nil
		
		local role = ''
		local uncredited = false
		local isLeadingActor = false
		
		-- Get the role if available
		if roleClaims and roleClaims[1].datavalue and roleClaims[1].datavalue.value then
			if roleClaims[1].datavalue.type == "string" then
				role = roleClaims[1].datavalue.value
			else
				local roleQID = roleClaims[1].datavalue.value.id
				if roleQID == 'Q18086706' then
					role = 'Self'
				elseif roleQID then
					role = getLink({roleQID})
				end
			end
		end
		
		-- Check for characteristics (leading actor, uncredited)
		if characteristicClaims then
			for _, characteristic in ipairs(characteristicClaims) do
				local characteristicQID
				if characteristic and characteristic.datavalue and characteristic.datavalue.value then
					characteristicQID = characteristic.datavalue.value.id
				end
				if characteristicQID == 'Q1765879' then  -- Leading actor
					isLeadingActor = true
				elseif characteristicQID == 'Q16582801' or characteristicQID == 'Q122392315' then  -- Uncredited
					uncredited = true
				end
			end
		end
		
		-- Generate link for the cast member
		local castMemberLink = getLink({castMemberQID})
		if uncredited then
			castMemberLink = castMemberLink .. ' (uncredited)'
		end
		
		-- generate table cells
		local castMemberCells = mw.html.create('tr')
			:tag('td')
				:wikitext(role)
				:done()
			:tag('td')
				:wikitext(castMemberLink)
				:allDone()
		
		-- Sort leading actors, other cast members, and uncredited actors
		if isLeadingActor then
			table.insert(leadingActors, castMemberCells)
		elseif uncredited then
			table.insert(uncreditedActors, castMemberCells)
		else
			table.insert(otherCastMembers, castMemberCells)
		end
	end
	
	-- Combine leading actors, other cast members, and uncredited actors
	local castList = TableTools.merge(leadingActors, otherCastMembers, uncreditedActors)
	
	if #castList == 0 then
		return nil
	end
	
	-- Return the formatted table if there are cast members
	table.insert(castList, 1, mw.html.create('tr')
		:tag('td')
			:attr('colspan', '2')
			:wikitext('Cast')
			:allDone()
	)
	table.insert(castList, 2, mw.html.create('tr')
		:tag('td')
			:wikitext('Role')
			:done()
		:tag('td')
			:wikitext('Actor')
			:allDone()
	)
	
	return castList
end
function p.getCastList(frame)
	return p._getCastList(getArgs(frame))
end

function p._getCrewList(args)
	local properties = {
		{"Production company", "P272"},
		{"Distributor", "P750"},
		{"Director", "P57"},
		{"Producer", "P162"},
		{"Screenwriter", "P58"},
		{"Cinematographer", "P344"},
		{"Editor", "P1040"},
		{"Composer", "P86"},
		{"Animator", "P6942"},
		{"Production designer", "P2554"},
		{"Costume designer", "P2515"},
		{"Storyboard artist", "P3275"},
	}
	local deathYearNeeded = {
		["Director"] = true,
		["Producer"] = true,
		["Screenwriter"] = true,
		["Animator"] = true,
		["Cinematographer"] = true,
		["Composer"] = true
	}
	
	local wikidataItem = args[1]
	if not wikidataItem then
		return error_message({'[[Module:Cast and crew]] error: no Wikidata item provided'})
	end
	
	local entity = mw.wikibase.getEntity(wikidataItem)
	if not entity or not entity.claims then
		return nil
	end
	
	local crewTableRows = {}
	local latestDeathYear = 0
	local deathYears = {}
	
	local releaseDateClaim = entity.claims['P577'] and entity.claims['P577'][1]
	local releaseDate = releaseDateClaim and releaseDateClaim.mainsnak.datavalue.value.time
	local releaseYear = releaseDate and releaseDate:match('+([0-9]+)')
	
	local isSilentFilm = false
	if releaseYear and tonumber(releaseYear) >= 1926 then
		local instanceOfClaims = entity.claims['P31'] or {}
		local genreClaims = entity.claims['P136'] or {}
		for _, claim in ipairs(instanceOfClaims) do
			if not isSilentFilm and claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value and claim.mainsnak.datavalue.value.id == 'Q226730' then
				isSilentFilm = true
				break
			end
		end
		if not isSilentFilm then
			for _, claim in ipairs(genreClaims) do
				if not isSilentFilm and claim and claim.mainsnak and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value and claim.mainsnak.datavalue.value.id == 'Q226730' then
					isSilentFilm = true
					break
				end
			end
		end
	end
	
	for _, prop in ipairs(properties) do
		local role, propertyId = unpack(prop)
		local crewMembers = entity.claims[propertyId]
		
		if crewMembers then
			local crewMemberLinks = {}
			for _, crewMember in ipairs(crewMembers) do
				if crewMember and crewMember.mainsnak and crewMember.mainsnak.datavalue and crewMember.mainsnak.datavalue.value then
					local crewMemberQID = crewMember.mainsnak.datavalue.value.id
					local crewMemberLink = getLink({crewMemberQID})
					
					if deathYearNeeded[role] and not deathYears[crewMemberQID] then
						local deathDateClaims = mw.wikibase.getBestStatements(crewMemberQID, 'P570')
						if deathDateClaims and deathDateClaims[1] and deathDateClaims[1].mainsnak and deathDateClaims[1].mainsnak.datavalue and deathDateClaims[1].mainsnak.datavalue.value then
							local deathDate = deathDateClaims[1].mainsnak.datavalue.value['time']
							if deathDate then
								local deathYear = deathDate:match('+([0-9]+)')
								if deathYear then
									crewMemberLink = crewMemberLink .. ' (d. ' .. deathYear .. ')'
									deathYears[crewMemberQID] = deathYear
									latestDeathYear = math.max(latestDeathYear, tonumber(deathYear))
								end
							end
						end
					end
					
					table.insert(crewMemberLinks, crewMemberLink)
				end
			end
			
			if #crewMemberLinks > 0 then
				table.insert(crewTableRows, mw.html.create('tr')
					:tag('td')
						:wikitext(role)
						:done()
					:tag('td')
						:wikitext(table.concat(crewMemberLinks, ', '))
						:allDone()
				)
			end
		end
	end
	
	if #crewTableRows == 0 then
		return nil
	end
	
	table.insert(crewTableRows, 1, mw.html.create('tr'):tag('th'):attr('colspan', '2'):wikitext('Crew'):allDone())
	
	if latestDeathYear > 0 then
		local currentYear = tonumber(os.date('*t').year)
		local pmaYears = currentYear - latestDeathYear - 1
		local footnoteText = 'Based on available information, the latest crew member that is relevant to international copyright laws died in ' .. latestDeathYear .. ', meaning that this film may be in the public domain in countries and jurisdictions with ' .. pmaYears .. ' years p.m.a. or less, as well as in the United States.'
		table.insert(crewTableRows, mw.html.create('tr'):tag('td'):attr('colspan', '2'):addClass('footnoteText'):wikitext(footnoteText):allDone())
	end
	
	return crewTableRows
end
function p.getCrewList(frame)
	return p._getCrewList(getArgs(frame))
end

function p._generateCastAndCrew(args)
	-- Try to get the current Wikidata item associated with the page
	local currentPageQID = args[1] or mw.wikibase.getEntityIdForCurrentPage()
	if not currentPageQID then
		return nil
	end
	
	local castListContent = p._getCastList({currentPageQID})
	local crewListContent = p._getCrewList({currentPageQID})
	
	if not crewListContent and not castListContent then
		return nil
	end
	
	local castCrewListContent = mw.html.create('table')
		:addClass('wikitable mw-collapsible mw-collapsed wst-cast-and-crew')
		:tag('caption')
			:wikitext('Cast and Crew ')
			:allDone()
	
	for i, tr in ipairs(castListContent or {}) do
		castCrewListContent:node(tr)
	end
	for i, tr in ipairs(crewListContent or {}) do
		castCrewListContent:node(tr)
	end
	
	return castCrewListContent
end

function p.generateCastAndCrew(frame)
	return p._generateCastAndCrew(getArgs(frame))
end

return p