--
-- Speciaziation class for winch
-- V2.0
-- @Originalauthor 50keda, @Edited by Exley
-- @date 22/2/2022



Winch = {}
Winch.JointMarginLimit = 0.8
Winch.MinJointTransLimit = 0.35
Winch.ModDirectory = g_currentModDirectory
Winch.Actions = {
	WINCH_ATTACH_DETACH = { "ATTACH_DETACH", Player.INPUT_CONTEXT_NAME, false, true, false, GS_PRIO_VERY_HIGH },
	WINCH_DETACH_ALL = { "DETACH_ALL", Vehicle.INPUT_CONTEXT_NAME, false, true, false, GS_PRIO_VERY_HIGH },
	WINCH_WINCH_PLAYER = { "WINCH", Player.INPUT_CONTEXT_NAME, false, false, true, GS_PRIO_VERY_HIGH },
	WINCH_WINCH_VEHICLE = { "WINCH", Vehicle.INPUT_CONTEXT_NAME, false, false, true, GS_PRIO_VERY_HIGH },
	WINCH_WINCH_SPEEDUP_PLAYER = { "WINCH_SPEEDUP", Player.INPUT_CONTEXT_NAME, false, false, true, GS_PRIO_VERY_HIGH },
	WINCH_WINCH_SPEEDUP_VEHICLE = { "WINCH_SPEEDUP", Vehicle.INPUT_CONTEXT_NAME, false, false, true, GS_PRIO_VERY_HIGH },
	WINCH_RELEASE_PLAYER = { "RELEASE", Player.INPUT_CONTEXT_NAME, false, true, false, GS_PRIO_VERY_HIGH },
	WINCH_RELEASE_VEHICLE = { "RELEASE", Vehicle.INPUT_CONTEXT_NAME, false, true, false, GS_PRIO_VERY_HIGH },
	WINCH_SWITCH_TO_NEXT = { "SWITCH_TO_NEXT", Player.INPUT_CONTEXT_NAME, false, true, false, GS_PRIO_VERY_LOW },
}

-- "static" variables for deteminating user active winch
Winch.AvailableWinchesCount = 0
Winch.AvailableWinches = {}
Winch.ActiveWinchIdx = nil
Winch.WinchJustSwitched = false
Winch.WinchSwitchTimer = 0
Winch.WinchingTimer = 0
Winch.ClientWinchingTimer = 0
Winch.ControlPlayer = false

function Winch.updateAvailableWinchesCount()
	
	Winch.AvailableWinchesCount = 0
	
	for k,v in pairs(Winch.AvailableWinches) do
		if v ~= nil then
			Winch.AvailableWinchesCount = Winch.AvailableWinchesCount + 1
		end
	end

end

function Winch.prerequisitesPresent(specializations)
	return true
end

function Winch.registerEventListeners(vehicleType)
	local functionNames = {
		"onRegisterActionEvents",
		"onLoad",
		"onLoadFinished",
		"onPreDelete",
		"onPostAttach",
		"onPreDetach",
		"onTurnedOn",
		"onTurnedOff",
		"onUpdate"
	}

	for i, v in ipairs(functionNames) do
		SpecializationUtil.registerEventListener(vehicleType, v, Winch)
	end

	Winch.ModifierType = g_soundManager:registerModifierType("WINCHING_LOAD", Winch.winchingLoadModifierValue, Winch.winchingLoadModifierMin, Winch.winchingLoadModifierMax)

end

function Winch:winchingLoadModifierValue()
	local sample = g_soundManager.activeSamples[g_soundManager.currentSampleIndex]
	if sample ~= nil and sample.modifiers.pitch[Winch.ModifierType] ~= nil then
		return sample.winchingLoad
	end

	return 0.0
end

function Winch:winchingLoadModifierMin()
	return 0.0
end

function Winch:winchingLoadModifierMax()
	return 2.0
end

function Winch:onRegisterActionEvents(isActiveForInput)

	-- print(string.format(" Register action events: %s -> active for input: %s", Winch.AvailableWinches[self.wWinchIdx][2], isActiveForInput))

	if self.isClient then

		if self.wActionEventIds ~= nil then
			for _, eventId in pairs(self.wActionEventIds) do
				g_inputBinding:removeActionEvent(eventId)
			end
		end

		self.wActionEventIds = {}
		self.wTriggeredActions = {}

		-- do not register events if winch is not active
		if Winch.ActiveWinchIdx ~= self.wWinchIdx then
			return
		end

		for actionName, actionDetails in pairs(Winch.Actions) do
			local internalActionName = actionDetails[1]
			local contextName = actionDetails[2]
			local triggerUp = actionDetails[3]
			local triggerDown = actionDetails[4]
			local triggerAlways = actionDetails[5]
			local priority = actionDetails[6]

			local isPlayerActionRegisterable = (isActiveForInput == false and contextName == Player.INPUT_CONTEXT_NAME)
			local isVehicleActionRegisterable = (isActiveForInput == true and contextName == Vehicle.INPUT_CONTEXT_NAME)
			if isVehicleActionRegisterable or isPlayerActionRegisterable then
				-- print(string.format(" Registering action: %s ...", actionName))
				
				local oldRegistrationContext = g_inputBinding.registrationContext

				-- stop modification if one is already in progress
				if oldRegistrationContext ~= InputBinding.NO_REGISTRATION_CONTEXT then
					g_inputBinding:endActionEventsModification()
				end
			
				g_inputBinding:beginActionEventsModification(contextName)
				
				local actionValid = false
				local actionEventId = ""
				actionValid, actionEventId, _ = g_inputBinding:registerActionEvent(actionName, self, Winch.actionEventCallback, triggerUp, triggerDown, triggerAlways, false)

				if actionValid then
					g_inputBinding:setActionEventText(actionEventId, g_i18n:getText(string.format("action_%s", actionName)))
					g_inputBinding:setActionEventTextPriority(actionEventId, priority)
					g_inputBinding:setActionEventTextVisibility(actionEventId, true)

					self.wActionEventIds[actionName] = actionEventId
					self.wTriggeredActions[internalActionName] = false
				end

				g_inputBinding:endActionEventsModification()

				-- restart modification if it was in progress before we made changes
				if oldRegistrationContext ~= InputBinding.NO_REGISTRATION_CONTEXT then
					g_inputBinding:beginActionEventsModification(oldRegistrationContext)
				end
			end
		end

	end
end

function Winch.actionEventCallback(self, actionName, inputValue, callbackState)

	-- print(" Winch action callback: ", actionName, inputValue, callbackState)

	local isTriggered = (inputValue > 0)
	self.wTriggeredActions[Winch.Actions[actionName][1]] = isTriggered
end

function Winch:onLoad(savegame)

	self.getPtoRpm 						= Utils.overwrittenFunction(self.getPtoRpm, Winch.getPtoRpm)
	self.onRegisterActionEvents			= Winch.onRegisterActionEvents

	self.wSwitchToNextWinch			= Winch.wSwitchToNextWinch
	self.wDrawActiveWinchInfo			= Winch.wDrawActiveWinchInfo
	self.wDrawHelpButtons				= Winch.wDrawHelpButtons
	self.wGetAttachedShapeIdx  		= Winch.wGetAttachedShapeIdx
	
	self.wAttachDetachObject			= Winch.wAttachDetachObject
	self.wAttachRaycastCallback		= Winch.wAttachRaycastCallback
	self.wDetachRaycastCallback		= Winch.wDetachRaycastCallback

	self.wAttachObject 				= Winch.wAttachObject
	self.wCreateChainAroundObject		= Winch.wCreateChainAroundObject
	self.wChainInterstRaycastCallback	= Winch.wChainInterstRaycastCallback
	self.wDetachObject 				= Winch.wDetachObject
	
	self.wWinch 						= Winch.wWinch
	self.wRelease 						= Winch.wRelease
	
	self.wUpdateJointFrame 			= Winch.wUpdateJointFrame
	self.wUpdateRope					= Winch.wUpdateRope
	self.wUpdateChain					= Winch.wUpdateChain

	
	-- active winch indexing
	self.wWinchIdx = #Winch.AvailableWinches + 1
	
	local _, name = Winch:wGetVehicleBrandAndName(self)
	if string.len(name) > 20 then
		name = name:sub(0, 20) .. ".."
	end
	local frmtStr = g_i18n:getText("info_activeWinchDetached")
	Winch.AvailableWinches[self.wWinchIdx] = { self, string.format(frmtStr, self.wWinchIdx, name)}
	Winch.updateAvailableWinchesCount()

	if Winch.ActiveWinchIdx == nil then
		Winch.ActiveWinchIdx = self.wWinchIdx
	end


	local key = "vehicle.winch"

	self.wInitialized = false
	self.wIsWinching = false
	self.wComponentIndex = Utils.getNoNil(self.xmlFile:getFloat(key .. "#componentIndex"), 1)
	self.wJointRefNode = I3DUtil.indexToObject(self.components, self.xmlFile:getString(key .. "#jointRefNode"))
	self.wRopeRefNode = I3DUtil.indexToObject(self.components, self.xmlFile:getString(key .. "#ropeRefNode"))
	self.wRopeShape = I3DUtil.indexToObject(self.components, self.xmlFile:getString(key .. "#ropeMesh"))
	self.wRopeEndShape = I3DUtil.indexToObject(self.components, self.xmlFile:getString(key .. "#ropeEndMesh"))
	self.wRopeEndShapeLen = Utils.getNoNil(self.xmlFile:getFloat(key .. "#ropeEndMeshLength"), 0.0)
	self.wChainNode = I3DUtil.indexToObject(self.components, self.xmlFile:getString(key .. "#chainMesh"))
	self.wPulleyNode = I3DUtil.indexToObject(self.components, self.xmlFile:getString(key .. "#pulleyMesh"))
	self.wSpeed = Utils.getNoNil(self.xmlFile:getFloat(key .. "#winchingSpeed"), 0.01)
	self.wSpeedup = Utils.getNoNil(self.xmlFile:getFloat(key .. "#winchingSpeedup"), 2)
	self.wWinchPtoRpm = Utils.getNoNil(self.xmlFile:getFloat(key .. "#winchingPtoRpm"), 340)
	self.wSpeedupWinchPtoRpm = Utils.getNoNil(self.xmlFile:getFloat(key .. "#speedupWinchingPtoRpm"), 540)
	self.wRopeLength = MathUtil.clamp(Utils.getNoNil(self.xmlFile:getFloat(key .. "#ropeLength"), 50), 5, 100)
	self.wMaxChainLength = MathUtil.clamp(Utils.getNoNil(self.xmlFile:getFloat(key .. "#maxChainLength"), 5), 0.5, 20)
	self.wMaxWinchingMass = Utils.getNoNil(self.xmlFile:getFloat(key .. "#maxWinchingMass"), 4)
	self.wChainMargin = Utils.getNoNil(self.xmlFile:getFloat(key .. "#chainMargin"), 0.001)

	-- load ropes for UV scrolling
	local ropesUVScrollKey = key .. ".ropesUVScroll"

	self.wUVScrollRopes = {}
	local i=1
	while true do
		local ropeKey = string.format(ropesUVScrollKey .. ".rope%d", i)
		
		if not self.xmlFile:hasProperty(ropeKey) then
			break
		end

		local ropeNode = I3DUtil.indexToObject(self.components, self.xmlFile:getString(ropeKey .. "#index"))
		if ropeNode ~= nil then
			table.insert(self.wUVScrollRopes, ropeNode)
		end

		i = i + 1
	end

	-- load skinned rope
	local skinnedRopeKey = key .. ".skinnedRope"

	self.wSkinnedRopeBias = Utils.getNoNil(self.xmlFile:getFloat(skinnedRopeKey .. "#ropeBias"), 0.02)
	self.wSkinnedRopeJointsParent = I3DUtil.indexToObject(self.components, self.xmlFile:getString(skinnedRopeKey .. "#jointsParentNode"))
	self.wSkinnedRopeJoints = {}
	if self.wSkinnedRopeJointsParent ~= nil and self.xmlFile:hasProperty(skinnedRopeKey .. "#jointsCount") then
		local jointsCount = self.xmlFile:getInt(skinnedRopeKey .. "#jointsCount") + 1

		for i=1,jointsCount,1 do
			self.wSkinnedRopeJoints[i] = getChildAt(self.wSkinnedRopeJointsParent, i-1)
		end
	end

	-- load dynamic chain I3D
	local dynamicChainKey = key .. ".dynamicChain"
	
	self.wDynamicChainFilename = Utils.getFilename(self.xmlFile:getString(dynamicChainKey .. "#i3dFilename"), Winch.ModDirectory)
	self.wDynamicChainI3DRoot = nil
	self.wDynamicChainMaterialNode = nil
	self.wDynamicChainHook = nil
	self.wDynamicChainHookSize = 0
	self.wDynamicChainI3DLoadRequestId = nil
	if self.wDynamicChainFilename ~= nil then
		local nodeId, sharedLoadRequestId = g_i3DManager:loadSharedI3DFile(self.wDynamicChainFilename, false, false)
		if nodeId > 0 then
			self.wDynamicChainI3DRoot = nodeId
			self.wDynamicChainMaterialNode = I3DUtil.indexToObject(self.wDynamicChainI3DRoot, self.xmlFile:getString(dynamicChainKey .. "#materialHolderNode"))
			self.wDynamicChainHook = I3DUtil.indexToObject(self.wDynamicChainI3DRoot, self.xmlFile:getString(dynamicChainKey .. "#hookNode"))
			self.wDynamicChainWidth = Utils.getNoNil(self.xmlFile:getFloat(dynamicChainKey .. "#chainWidth"), 0.05)
			if self.wDynamicChainHook ~= nil then
				local _, _, size = getTranslation(getChildAt(self.wDynamicChainHook, 0))
				self.wDynamicChainHookSize = size
			end
		end
		self.wDynamicChainI3DLoadRequestId = sharedLoadRequestId
	end
	
	-- load main sound
	local soundKey = key .. ".sounds"
	self.wSoundSample = g_soundManager:loadSampleFromXML(self.xmlFile, soundKey, "winching", self.baseDirectory, self.components[self.wComponentIndex].node, 1, AudioGroup.VEHICLE, nil, self)
	self.wSoundSample.winchingLoad = 0

	-- load chain attach sound
	self.wChainAttachSndSample = g_soundManager:loadSampleFromXML(self.xmlFile, soundKey, "chainAttach", self.baseDirectory, self.components[self.wComponentIndex].node, 1, AudioGroup.VEHICLE, nil, self)
	
	-- load chain detach sound
	self.wChainDetachSndSample = g_soundManager:loadSampleFromXML(self.xmlFile, soundKey, "chainDetach", self.baseDirectory, self.components[self.wComponentIndex].node, 1, AudioGroup.VEHICLE, nil, self)
	
	-- load overriden input attacher info
	local attacherKey = key .. ".overridenInputAttacher"
	self.wAttachJointLowerRotOffset = self.xmlFile:getFloat(attacherKey .. "#lowerRotationOffset")
	self.wAttachJointUpperRotOffset = self.xmlFile:getFloat(attacherKey .. "#upperRotationOffset")
	self.wAttachJointRotSaturation = Utils.getNoNil(self.xmlFile:getFloat(attacherKey .. "#rotSaturation"), 1)

	if self.wAttachJointLowerRotOffset ~= nil then
		self.wAttachJointLowerRotOffset = math.rad(self.wAttachJointLowerRotOffset)
	end
	if self.wAttachJointUpperRotOffset ~= nil then
		self.wAttachJointUpperRotOffset = math.rad(self.wAttachJointUpperRotOffset)
	end


	-- initialize all needed variables
	-- server only
	self.wJoints = {}						-- joints references
	self.wCurrJointZLimit = nil			-- current main joint Z translation limit (we are simulating winch with this)
	self.wCurrAttachedShapesMass = 0		-- mass of all currently attached shape, for fakce calculation of how much winch can winch
	self.wCurrPtoRpm = 0
	-- shared
	self.wAttachedShapes = {}				-- attached objects shapes
	self.wAttachedShapesRefNodes = {}		-- reference nodes for attached shapes
	-- client only
	self.wAttachedShapesChainNodes = {}	-- static chain shapes from joint to joint
	self.wAttachedShapesDynamicChains = {}	-- shapes of dynamic chains wrapped around attached object
	
	self.wActionEventIds = {}
	self.wTriggeredActions = {}

	-- mark as initialized if mandatory things are found
	if self.wJointRefNode ~= nil then
		self.wInitialized = true
	else
		print("Error: Winch initialization failed!")
		return
	end
	
end

function Winch:onLoadFinished(savegame)

	-- reset rope mesh
	self:wUpdateRope(0, true)

	-- reset chain mesh
	if self.wChainNode ~= nil then
		setVisibility(self.wChainNode, false)
	end


end

function Winch:onPreDelete()

	-- first make sure everything is detached
	wDetachAllEvent.sendEvent(self)

	-- now stop all the sounds & delete samples
	g_soundManager:stopSample(self.wSoundSample) --, true)
	g_soundManager:stopSample(self.wChainAttachSndSample)
	g_soundManager:stopSample(self.wChainDetachSndSample)

	g_soundManager:deleteSample(self.wChainAttachSndSample)
	g_soundManager:deleteSample(self.wChainDetachSndSample)
	g_soundManager:deleteSample(self.wSoundSample)

	-- remove current winch from available winches, so it won't appear in active list anymore
	Winch.AvailableWinches[self.wWinchIdx] = nil
	Winch.updateAvailableWinchesCount()

	-- if this winch was current active for player, then set first available as current
	if Winch.ActiveWinchIdx == self.wWinchIdx then
		for k,v in pairs(Winch.AvailableWinches) do
			if v ~= nil then
				winchSpec, _ = unpack(v)
				Winch.ActiveWinchIdx = k
				winchSpec:requestActionEventUpdate()
				break
			end
		end
	end

	-- release dynamic chain i3d file
	if self.wDynamicChainI3DLoadRequestId ~= nil then 
		g_i3DManager:releaseSharedI3DFile(self.wDynamicChainI3DLoadRequestId, true)

		self.wDynamicChainI3DLoadRequestId = nil
	end

end

function Winch:onPostAttach(attacherVehicle, jointDescIndex)
	
	local _, name = Winch:wGetVehicleBrandAndName(self)
	local brand1, name1 = Winch:wGetVehicleBrandAndName(attacherVehicle)

	if string.len(name) > 10 then
		name = name:sub(0, 10) .. ".."
	end

	local attacherStr = string.format("%s %s", brand1, name1)
	if string.len(attacherStr) > 25 then
		attacherStr = attacherStr:sub(0, 25) .. ".."
	end

	local frmtStr = g_i18n:getText("info_activeWinchAttached")
	Winch.AvailableWinches[self.wWinchIdx] = {self, string.format(frmtStr, self.wWinchIdx, name, attacherStr)}

end

function Winch:onPreDetach(attacherVehicle, jointDescIndex)
	
	local _, name = Winch:wGetVehicleBrandAndName(self)
	
	if string.len(name) > 20 then
		name = name:sub(0, 20) .. ".."
	end

	local frmtStr = g_i18n:getText("info_activeWinchDetached")
	Winch.AvailableWinches[self.wWinchIdx] = {self, string.format(frmtStr, self.wWinchIdx, name)}

end

function Winch:onTurnedOn(noEventSend)
end

function Winch:onTurnedOff(noEventSend)
	-- make sure to stop sound when switched off
	g_soundManager:stopSample(self.wSoundSample) --, true)
end

function Winch:getPtoRpm(superFunc)
	if self:getDoConsumePtoPower() then
		return self.wCurrPtoRpm
	end

	return 0
end

function Winch:onUpdate(dt)

	if not self.wInitialized then
		return
	end

	local availableRopeOrChain = 0
	if #self.wAttachedShapesRefNodes <= 0 then
		availableRopeOrChain = math.max(0, self.wRopeLength - Winch:wGetPlayerDistanceTo(self.rootNode))
	else
		if self.wAttachedShapesRefNodes[1] ~= nil and entityExists(self.wAttachedShapesRefNodes[1]) then
			availableRopeOrChain = math.max(0, self.wMaxChainLength - Winch:wGetPlayerDistanceTo(self.wAttachedShapesRefNodes[1]))
		end
	end

	local isActiveForInput = self:getIsActiveForInput(true)
	local canOperateWinch = Winch:wGetPlayerDistanceTo(self.rootNode) < self.wRopeLength or isActiveForInput

	-- update winch switching timer
	Winch.WinchSwitchTimer = math.max(Winch.WinchSwitchTimer - dt, 0)
	Winch.WinchingTimer = math.max(Winch.WinchingTimer - dt, 0)
	Winch.ClientWinchingTimer = math.max(Winch.ClientWinchingTimer - dt, 0)
	
	-- update winching sound samples
	if self:getIsTurnedOn() then

		-- if turned on not active on input motor rpm won't be updated!
		-- That's why force it by calling update on motor rpm
		--[[
		if self.attacherVehicle ~= nil and self.attacherVehicle.motor ~= nil then
			if not isActiveForInput or self.attacherVehicle:getLastSpeed() < 2 then
				self.attacherVehicle.motor:update(dt)
			end
		end
		]]
		
		-- if not winching anymore put pto back to normal
		if Winch.ClientWinchingTimer <= 0 then
			self.wCurrPtoRpm = self.spec_powerConsumer.ptoRpm
		end

		-- update sound modifiers depending on current pto rpm
		self.wSoundSample.winchingLoad = (self:getPtoRpm() - self.spec_powerConsumer.ptoRpm) / (self.wSpeedupWinchPtoRpm - self.spec_powerConsumer.ptoRpm)

		if not g_soundManager:getIsSamplePlaying(self.wSoundSample) then
			g_soundManager:playSample(self.wSoundSample, 0)
		end
	end

	-- update joint frames, ropes, chains and possible object detaching if entity vanishes for some reason
	if #self.wAttachedShapesRefNodes > 0 then

		for i=1, #self.wAttachedShapesRefNodes, 1 do

			if self.wAttachedShapesRefNodes[i] ~= nil then

				if entityExists(self.wAttachedShapesRefNodes[i]) then

					local jointRefNode = self.wJointRefNode
					if i ~= 1 then
						jointRefNode = self.wAttachedShapesRefNodes[1]
					end

					-- update rotations of joint
					local asX, asY, asZ = getWorldTranslation(self.wAttachedShapesRefNodes[i])
					local jrX, jrY, jrZ = getWorldTranslation(jointRefNode)
					
					self:wUpdateJointFrame(self.wJoints[i], jointRefNode, {jrX, jrY, jrZ}, {asX, asY, asZ}, i == 1)

					if not (g_currentMission.connectedToDedicatedServer and self.isServer) then

						-- update rope only on main joint, otherwise update chain
						if i == 1 then
							jrX, jrY, jrZ = getWorldTranslation(self.wSkinnedRopeJointsParent)
							self:wUpdateRope(MathUtil.vector3Length(asX - jrX, asY - jrY, asZ - jrZ))
						else
							self:wUpdateChain(i)
						end

					end

				else

					self:wDetachObject(self.wAttachedShapes[i])
				
				end

			end
		end
	end

	-- make sure to switch to current winch if player enters vehicle
	if isActiveForInput and Winch.ActiveWinchIdx ~= self.wWinchIdx then
		Winch.ActiveWinchIdx = self.wWinchIdx
		-- register actions in case user was on foot before where vehicle actions were not registered
		self:onRegisterActionEvents(isActiveForInput)
	end

	local isThisActiveWinch = (self.wWinchIdx == Winch.ActiveWinchIdx)

	-- keep currently active whinch alive all the time, 
	-- otherwise inputs for it will be unregistered and player won't be able to operate it anymore
	if isThisActiveWinch and not self:getRootVehicle():getIsActive() then
		self:raiseActive()

		-- everytime player controlling state changes make sure to register action events,
		-- otherwise player will loose input binding when entering vehicle without a winch.
		if g_currentMission.controlPlayer ~= Winch.ControlPlayer then
			self:onRegisterActionEvents(isActiveForInput)
			Winch.ControlPlayer = g_currentMission.controlPlayer
		end
	end

	-- ignore ALL input if this is not active winch or player is frozen or GUI is visible
	if (not isThisActiveWinch) or g_currentMission.isPlayerFrozen or g_gui:getIsGuiVisible() then

		-- check out end of the onUpdate for more informations on triggered actions
		self.wTriggeredActions = {}
		return
	end

	-- active winch actions only
	if isThisActiveWinch then

		-- handle winch switching
		if self.wTriggeredActions["SWITCH_TO_NEXT"] then
			self:wSwitchToNextWinch(isActiveForInput)
		end

		-- draw button for switching between winches
		self:wDrawActiveWinchInfo()
	end


	-- handle all operational inputs
	if canOperateWinch then

		-- handle winching
		if self:getIsTurnedOn() then
			
			-- handle winching
			if self.wTriggeredActions["WINCH_SPEEDUP"] then
				wWinchEvent.sendEvent(self, true, g_currentMission.playerUserId)
			elseif self.wTriggeredActions["WINCH"] then
				wWinchEvent.sendEvent(self, false, g_currentMission.playerUserId)
			end

		else

			if self.wTriggeredActions["WINCH_SPEEDUP"] or self.wTriggeredActions["WINCH"] then
				g_currentMission:showBlinkingWarning(g_i18n:getText("warning_turnOnWinch"), 2000)
			end

		end

		-- handle release of the winch
		if self.wTriggeredActions["RELEASE"] then
			wReleaseEvent.sendEvent(self)
		end

		-- handle detaching/attaching
		if g_currentMission.player.isObjectInRange then
		
			if self.wTriggeredActions["ATTACH_DETACH"] and availableRopeOrChain > 0 then
				wAttachDetachEvent.sendEvent(self, g_currentMission.player)
			end

		end

		-- handle detaching all if inside tractor and operating winch
		if isActiveForInput and self.wTriggeredActions["DETACH_ALL"] then
			wDetachAllEvent.sendEvent(self)
		end

		-- as last draw helping buttons, if player can operate winch
		self:wDrawHelpButtons(isActiveForInput, availableRopeOrChain)
	end

	-- each update reset triggered actions, 
	-- if triggered again they will be set via input action event callback
	self.wTriggeredActions = {}
end

function Winch:wGetVehicleBrandAndName(vehicle)

	if vehicle.isDeleted or not vehicle.isVehicleSaved or vehicle.nonTabbable then
        return "-", "-"
    end
    
    local name = "-"
    local brand = "-"
    local storeItem = g_storeManager.xmlFilenameToItem[vehicle.configFileName:lower()]
    
    if storeItem ~= nil then
        if storeItem.name ~= nil then
            name = tostring(storeItem.name)
        end
        if storeItem.brandIndex ~= nil then
        	local brandItem = g_brandManager:getBrandByIndex(storeItem.brandIndex)
        	if brandItem ~= nil then
            	brand = tostring(brandItem.name)
        	end
        end
	end

	return brand, name
end

function Winch:wSwitchToNextWinch(isActiveForInput)

	--print(string.format("Switched from: %s", Winch.AvailableWinches[Winch.ActiveWinchIdx][2]))

	-- abort multiple switcing at once
	if Winch.WinchSwitchTimer > 0 then
		return
	end

	Winch.WinchSwitchTimer = 10

	if isActiveForInput then
		return
	end

	local currentFound = false
	local firstKey = nil
	local newKey = nil
	for k,_ in pairs(Winch.AvailableWinches) do
		if firstKey == nil then
			firstKey = k
		end

		if currentFound then
			newKey = k
			break
		end

		if k == Winch.ActiveWinchIdx then
			currentFound = true
		end
	end

	if newKey == nil then
		newKey = firstKey
	end

	Winch.ActiveWinchIdx = newKey
	Winch.WinchJustSwitched = true

	local winchSpec, winchInfo = unpack(Winch.AvailableWinches[Winch.ActiveWinchIdx])
	local message = string.format(g_i18n:getText("info_activeWinchNotification"), winchInfo)
	g_currentMission:addIngameNotification(FSBaseMission.INGAME_NOTIFICATION_INFO, message)

	self:onRegisterActionEvents()
	
	winchSpec:raiseActive()
	winchSpec:onRegisterActionEvents()

	--print(string.format("Switched to: %s", winchInfo))
end

function Winch:wGetPlayerDistanceTo(node)
	
	local playerNode = g_currentMission.player.model.pickUpKinematicHelperNode

	-- if player is not available report just a bit bigger distance then rope is set, not to be able to attach it in that case
	if playerNode == nil then
		return Utils.getNoNil(self.wRopeLength, 0.0) + 1
	end

	local x, y, z = getWorldTranslation(playerNode)
	local x1, y1, z1 = getWorldTranslation(node)

	return MathUtil.vector3Length(x - x1, y - y1, z - z1)

end

function Winch:wDrawHelpButtons(isActiveForInput, availableRopeOrChain)

	if self.isClient then
	
		local canAttachDetach = (g_currentMission.player.isObjectInRange and availableRopeOrChain > 0)
		if canAttachDetach then
			g_inputBinding:setActionEventText(self.wActionEventIds["WINCH_ATTACH_DETACH"], g_i18n:getText(string.format("action_%s", "WINCH_ATTACH_DETACH")))
		end
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_ATTACH_DETACH"], canAttachDetach)
		
		if not isActiveForInput then
			if #self.wAttachedShapesRefNodes <= 0 then
				g_currentMission:addExtraPrintText(string.format(g_i18n:getText("info_availableWinchRope"), availableRopeOrChain))
			else
				g_currentMission:addExtraPrintText(string.format(g_i18n:getText("info_availableWinchChain"), availableRopeOrChain))			
			end
		end
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_DETACH_ALL"], isActiveForInput)

		local isAnyShapeAttached = (#self.wAttachedShapes > 0)
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_WINCH_PLAYER"], isAnyShapeAttached)
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_WINCH_VEHICLE"], isAnyShapeAttached)
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_WINCH_SPEEDUP_PLAYER"], isAnyShapeAttached)
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_WINCH_SPEEDUP_VEHICLE"], isAnyShapeAttached)
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_RELEASE_PLAYER"], isAnyShapeAttached)
		g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_RELEASE_VEHICLE"], isAnyShapeAttached)

	end

end

function Winch:wDrawActiveWinchInfo()

	if self.isClient then
		local canSetActive = (Winch.AvailableWinchesCount > 1)
		if canSetActive then			
			g_currentMission:addExtraPrintText(string.format(g_i18n:getText("info_activeWinch"), Winch.AvailableWinches[Winch.ActiveWinchIdx][2]))
		end
		if self.wActionEventIds ~= nil then
			g_inputBinding:setActionEventActive(self.wActionEventIds["WINCH_SWITCH_TO_NEXT"], canSetActive)
		end
	end
end

function Winch:wGetAttachedShapeIdx(shape)

	local idx = -1

	-- search for given shape in allready attached shapes	
	for k, v in pairs(self.wAttachedShapes) do
		if v == shape then
			idx = k
			break
		end
	end

	return idx
end

function Winch:wAttachDetachObject(x, y, z, x1, y1, z1, isDetach)

	if x == nil or y == nil or z == nil or x1 == nil or y1 == nil or z1 == nil then
		print("WINCH: Error: Invalid input paramters one of them is nil")
		return
	end

	local dx = x - x1
	local dy = y - y1
	local dz = z - z1
	local length = MathUtil.vector3Length(dx, dy, dz)

	-- depedning on input argument decide which callback we have to use for raycasting
	local callback = "wAttachRaycastCallback"
	local requiredDiff = 1
	if isDetach then
		callback = "wDetachRaycastCallback"
		requiredDiff = -1
	end

	local allreadyAttachedCount = #self.wAttachedShapes

	raycastClosest(x1, y1, z1, dx / length, dy / length, dz / length, callback, 4, self, 0x1000000)
	
	-- in case raycast from camera node did not succed just send another one from kinematic helpr
	if #self.wAttachedShapes - allreadyAttachedCount ~= requiredDiff then
		raycastClosest(x, y, z, dx / length, dy / length, dz / length, callback, 3, self, 0x1000000)
	end

	--[[
	if #self.wAttachedShapes - allreadyAttachedCount ~= requiredDiff then
		if isDetach then
			print("WINCH: Detach raycasting did not find any objects!")
		else
			print("WINCH: Attach raycasting did not find any objects!")
		end
	end
	]]
		
	if isDetach and #self.wAttachedShapes < allreadyAttachedCount then
		return true
	elseif not isDetach and #self.wAttachedShapes > allreadyAttachedCount then
		return true
	end

	return false

end

function Winch:wAttachRaycastCallback(shape, x, y, z, distance)
	-- print(string.format("WINCH: Attach raycast callback (current mass): %s", getMass(shape)))
	if getMass(shape) > 0 then
		self:wAttachObject(shape, x, y, z, distance)
	end
end

function Winch:wDetachRaycastCallback(shape, x, y, z, distance)
	-- print(string.format("WINCH: Detach raycast callback (current mass): %s", getMass(shape)))
	if getMass(shape) > 0 then
		self:wDetachObject(shape)
	end
end

function Winch:wAttachObject(shape, apX, apY, apZ, distance)

	-- initial check
	if shape == nil or shape == 0 then
		return
	end

	-- ignore attaching if already attached
	if self:wGetAttachedShapeIdx(shape) ~= -1 then
		return
	end

	-- TODO: test it! and properly implement it
	--setAngularDamping(shape, 0.75)

	local idx = #self.wAttachedShapes + 1
	self.wAttachedShapes[idx] = shape
	self.wCurrAttachedShapesMass = self.wCurrAttachedShapesMass + getMass(shape)

	-- create reference node on split shape
	local attachedShapeRefNode = createTransformGroup("wAttachRef")
	self.wAttachedShapesRefNodes[idx] = attachedShapeRefNode
	link(shape, attachedShapeRefNode)
	local aplX, aplY, aplZ = worldToLocal(attachedShapeRefNode, apX, apY, apZ)
	setTranslation(attachedShapeRefNode, aplX, aplY, aplZ)

	if self.isServer then

		-- create joint for winching
		local constr = JointConstructor:new()
		if idx == 1 then

			constr:setActors(self.components[self.wComponentIndex].node, shape)
			constr:setJointTransforms(self.wJointRefNode, attachedShapeRefNode)

			local jrX, jrY, jrZ = getWorldTranslation(self.wJointRefNode)
			self.wCurrJointZLimit = MathUtil.vector3Length(apX - jrX, apY - jrY, apZ - jrZ) + 0.001
			constr:setTranslationLimit(0, true, -Winch.JointMarginLimit, Winch.JointMarginLimit)
			constr:setTranslationLimit(1, true, -Winch.JointMarginLimit, Winch.JointMarginLimit)
			constr:setTranslationLimit(2, true, -self.wCurrJointZLimit, Winch.JointMarginLimit)
			
			constr:setTranslationLimitSpring(0, 5, 0, 5, 0, 5)
			constr:setEnableCollision(true)

			self.wJoints[idx] = constr:finalize()

			self:wUpdateJointFrame(self.wJoints[idx], self.wJointRefNode, {jrX, jrY, jrZ}, {apX, apY, apZ}, true)
			
		else

			constr:setActors(self.wAttachedShapes[1], shape)
			constr:setJointTransforms(self.wAttachedShapesRefNodes[1], attachedShapeRefNode)

			local jrX, jrY, jrZ = getWorldTranslation(self.wAttachedShapesRefNodes[1])
			local zLimit = MathUtil.vector3Length(apX - jrX, apY - jrY, apZ - jrZ) + 0.0015
			constr:setTranslationLimit(0, true, -0.1, 0.1)
			constr:setTranslationLimit(1, true, -0.1, 0.1)
			constr:setTranslationLimit(2, true, -zLimit, 0.0015)
			
			constr:setTranslationLimitSpring(0, 5, 0, 5, 0, 5)
			constr:setEnableCollision(true)

			self.wJoints[idx] = constr:finalize()

			self:wUpdateJointFrame(self.wJoints[idx], self.wAttachedShapesRefNodes[1], {jrX, jrY, jrZ}, {apX, apY, apZ}, false)
		
		end

	end

	if not self.isServer or not g_currentMission.connectedToDedicatedServer then
		
		-- create chain around attached object
		self:wCreateChainAroundObject(idx, distance)

		if idx == 1 then

			-- if first mesh, then just update main rope
			self:wUpdateRope()

		else

			-- otherwise create chain mesh
			if self.wChainNode ~= nil then
				self.wAttachedShapesChainNodes[idx] = clone(self.wChainNode, false, false, false)
				link(self.wAttachedShapesRefNodes[idx], self.wAttachedShapesChainNodes[idx])

				setVisibility(self.wAttachedShapesChainNodes[idx], true)

				-- copy over RDT factors from winch
				local x,y,z,w = getShaderParameter(self.wRopeShape, "RDT")
				setShaderParameter(self.wAttachedShapesChainNodes[idx], "RDT", x, y, z, w)

				self:wUpdateChain(idx)
			end

		end

	end
end

function Winch:wCreateChainAroundObject(idx, distance)

	local shape = self.wAttachedShapes[idx]
	local shapeRefNode = self.wAttachedShapesRefNodes[idx]

	local beltData = g_tensionBeltManager:getBeltData("basic")
	local edgeLength = 0.1
	local geometryBias = self.wChainMargin

	-- get chain material
	local materialId = getMaterial(self.wDynamicChainMaterialNode, 0)
	if materialId == nil or materialId <= 0 then
		materialId = beltData.dummyMaterial.materialId
	end

	local tensionBelt = TensionBeltGeometryConstructor:new()
	tensionBelt:setWidth(self.wDynamicChainWidth)
	tensionBelt:setMaxEdgeLength(edgeLength)
	tensionBelt:setMaterial(materialId)
	tensionBelt:setUVscale(beltData.dummyMaterial.uvScale)

	-- add hook on the begining of the chain
	if self.wDynamicChainHook ~= nil then
		tensionBelt:addAttachment(0, 0, self.wDynamicChainHookSize * self.wDynamicChainWidth)
	end

	-- create origin node and belt start and end nodes
	local originNode = createTransformGroup("wChainLinkNode")
	link(shape, originNode)
	
	local startNode = createTransformGroup("wChainStartNode")
	local endNode = createTransformGroup("wChainEndNode")
	link(originNode, startNode)
	link(originNode, endNode)

	local x,y,z = getWorldTranslation(shapeRefNode)
	local dxCamera, dyCamera, dzCamera = localDirectionToWorld(g_currentMission.player.cameraNode, 0, 1, 0)
	local dxShape, dyShape, dzShape = localDirectionToWorld(shape, 0, 1, 0)

	-- move origin node a bit closer to camera ( this should help for belt wrapping around object)
	local closeupDiff = 0.05
	local lx, ly, lz = worldToLocal(g_currentMission.player.cameraNode, x, y, z)
	local lx1, ly1, lz1 = localToLocal(g_currentMission.player.cameraNode, shape, lx, ly, lz + closeupDiff)
	setTranslation(originNode, lx1, ly1, lz1)

	-- crazy hack to better visualize chains and not appearing under hooks
	setTranslation(shapeRefNode, lx1, ly1, lz1)

	-- calculate Y-plane direction vector to object center
	local dx = lx1
	local dy = 0
	local dz = lz1
	local length = MathUtil.vector3Length(dx, dy, dz)
	dx = dx / length
	dz = dz / length
	
	-- put it into tangent direction
	setDirection(originNode, dx, dy, dz, 0, 1, 0)
	
	-- properly rotate start and end nodes
	rotateAboutLocalAxis(startNode, 1.57, 0, 1, 0)
	rotateAboutLocalAxis(endNode, 1.57, 0, 1, 0)
	rotateAboutLocalAxis(startNode, -1.57, 0, 0, 1)
	rotateAboutLocalAxis(endNode, -1.57, 0, 0, 1)

	-- offset both for 1cm on each side, so belt will be generated at all
 	setTranslation(startNode, 0.01, 0, 0)
 	setTranslation(endNode, -0.01, 0, 0)
	
	-- finally set fixed points!
	tensionBelt:setFixedPoints(endNode, startNode)
	tensionBelt:setGeometryBias(geometryBias)
	tensionBelt:setLinkNode(shape)
	
	-- initialize temporary data to which callback can write
	self.wTempIntersectionPoints = {}
	self.wTempShape = shape

	-- do raycasting around joint to collect all intersection points
	-- (we could use tensionBelt:addShape but it didn't work properly on spruce)
	-- Algorithm simply rotates origin node and raycasts towards itself from different angles
	-- each time it does a rotation on local axis for cca 2.8° up to 90° and then raycasts
	local i = -32
	while i <= 32 do

		i = i + 1

		rotateAboutLocalAxis(originNode, -0.05 * i, 0, 1, 0)

		x, y, z = localToWorld(originNode, 0, 0, -2)
		dirX, dirY, dirZ = localDirectionToWorld(originNode, 0, 0, 1)
		raycastAll(x, y, z, dirX, dirY, dirZ, "wChainInterstRaycastCallback", 2, self, 0x1000)

		rotateAboutLocalAxis(originNode, 0.05 * i , 0, 1, 0)

	end

	-- after all points are collected finally add intersection points
	dirX, dirY, dirZ = localDirectionToWorld(startNode, 1, 0, 0)
	for k,v in pairs(self.wTempIntersectionPoints) do
		x, y, z = unpack(v)
		tensionBelt:addIntersectionPoint(x, y, z, dirX, dirY, dirZ)
	end

	-- clear termporary data from raycasting
	for k,v in pairs(self.wTempIntersectionPoints) do self.wTempIntersectionPoints[k] = nil end
	self.wTempIntersectionPoints = nil
	self.wTempShape = nil

    -- tadaaa create shape!!!
	local beltShape, _, beltLength = tensionBelt:finalize()
	
	-- now finaly add hook mesh
	if self.wDynamicChainHook ~= nil then
		local hook = clone(self.wDynamicChainHook, false, false, false)
		link(getChildAt(beltShape, 0), hook)
		setScale(hook, self.wDynamicChainWidth, self.wDynamicChainWidth, self.wDynamicChainWidth)
		setTranslation(hook, 0, self.wChainMargin, 0)
		
		-- should be on 0 0 0
		setRotation(hook, 0, 0, 0)  

		setShaderParameter(beltShape, "beltClipOffsets", 0, self.wDynamicChainHookSize * self.wDynamicChainWidth, 1, 0, false)

		-- copy over RDT factors from winch
		local x,y,z,w = getShaderParameter(self.wRopeShape, "RDT")
		setShaderParameter(hook, "RDT", x, y, z, w)
	end

	-- all creted helper transform groups can now be relased, as geometry of belt is created
	unlink(startNode)
	unlink(endNode)
	unlink(originNode)

	-- print("WINCH: Belt for chain creted, length: " .. beltLength)
	
	self.wAttachedShapesDynamicChains[idx] = beltShape

end

function Winch:wChainInterstRaycastCallback(shape, x, y, z, distance)
	if shape == self.wTempShape then
		table.insert(self.wTempIntersectionPoints, {x,y,z})
		return false
	end
	return true
end

function Winch:wDetachObject(shape)

	local foundShapeIdx = nil
	if shape == nil then
		foundShapeIdx = #self.wAttachedShapes
	else
		foundShapeIdx = self:wGetAttachedShapeIdx(shape)
	end

	-- if shape is found finally remove it
	if foundShapeIdx > 0 then

		-- reset mass first
		self.wCurrAttachedShapesMass = 0

		if foundShapeIdx == 1 then -- if root shape then we have to collapse everything

			for i=#self.wAttachedShapes,1,-1 do

				if self.isServer then
				
					removeJoint(self.wJoints[i])
					table.remove(self.wJoints, i)

				end
				
				if self.wAttachedShapesRefNodes[i] ~= nil and entityExists(self.wAttachedShapesRefNodes[i]) then
					unlink(self.wAttachedShapesRefNodes[i])
				end

				table.remove(self.wAttachedShapes, i)
				table.remove(self.wAttachedShapesRefNodes, i)
				
				if not self.isServer or not g_currentMission.connectedToDedicatedServer then
					
					if entityExists(self.wAttachedShapesDynamicChains[i]) then
						unlink(self.wAttachedShapesDynamicChains[i])
					end

					table.remove(self.wAttachedShapesChainNodes, i)
					table.remove(self.wAttachedShapesDynamicChains, i)
				end

			end

		else -- otherwise just found shape has to be removed

			if self.isServer then
				
				-- first recalculate mass
				for idx,k in pairs(self.wAttachedShapes) do
					
					-- again check entity, because if detach is invoked directly from update,
					-- this means that entities are disappearing (sold most probably).
					-- So in that case 2 entities can disappear at same time,
					-- but when first entity is detached (this function is called) and
					-- second is also already missing, getMass would spit out errors in log
					if entityExists(k) and idx ~= foundShapeIdx then
						self.wCurrAttachedShapesMass = self.wCurrAttachedShapesMass + getMass(k)
					end

				end

				removeJoint(self.wJoints[foundShapeIdx])
				table.remove(self.wJoints, foundShapeIdx)

			end
			
			if self.wAttachedShapesRefNodes[foundShapeIdx] ~= nil and entityExists(self.wAttachedShapesRefNodes[foundShapeIdx]) then
				unlink(self.wAttachedShapesRefNodes[foundShapeIdx])
			end

			table.remove(self.wAttachedShapes, foundShapeIdx)
			table.remove(self.wAttachedShapesRefNodes, foundShapeIdx)
			
			if not self.isServer or not g_currentMission.connectedToDedicatedServer then
				
				if entityExists(self.wAttachedShapesDynamicChains[foundShapeIdx]) then
					unlink(self.wAttachedShapesDynamicChains[foundShapeIdx])
				end

				table.remove(self.wAttachedShapesChainNodes, foundShapeIdx)
				table.remove(self.wAttachedShapesDynamicChains, foundShapeIdx)
			end
			
		end

		if not self.isServer or not g_currentMission.connectedToDedicatedServer then
			-- if everything was removed reset joint reference node
			if #self.wAttachedShapes <= 0 then

				self:wUpdateRope(0, true)

			end
		end

		return true
	end

	return false
end

function Winch:wWinch(speedup, userId)

	if Winch.WinchingTimer > 0 then
		return
	end

	-- really small timeout, just to make sure multiple sended events 
	-- are not triggering double speedup of winching
	Winch.WinchingTimer = 10  

	if not self:getIsTurnedOn() or #self.wAttachedShapesRefNodes <= 0 then
		return
	end

	-- calulate current joint distance and set is as current joint limit just to make sure
	-- that during winching rope is always in uptight state, actual winching is done below
	local asX, asY, asZ = getWorldTranslation(self.wAttachedShapesRefNodes[1])	
	local jrX, jrY, jrZ = getWorldTranslation(self.wJointRefNode)
	local currentJointDistance = MathUtil.vector3Length(asX - jrX, asY - jrY, asZ - jrZ)
	if self.wCurrJointZLimit == nil or self.wCurrJointZLimit > currentJointDistance + 0.01 then
		self.wCurrJointZLimit = currentJointDistance + 0.01
	end

	-- "start winching" if mass of attached shapes is small enough
	local isToHeavy = false
	if self.wCurrAttachedShapesMass < self.wMaxWinchingMass then
		local diff =  self.wSpeed
		if speedup ~= nil and speedup == true then
			diff = diff * self.wSpeedup
		end
		self.wCurrJointZLimit = MathUtil.clamp(self.wCurrJointZLimit - diff, Winch.MinJointTransLimit, self.wCurrJointZLimit)
	else
		isToHeavy = true
	end

	-- only update translation limit if closest possible limit not reached yet
	local isWinching = false
	if self.wCurrJointZLimit > Winch.MinJointTransLimit then
		setJointTranslationLimit(self.wJoints[1], 2, true, -self.wCurrJointZLimit, Winch.JointMarginLimit)
		isWinching = true
	end

	-- post feedback of winching
	wWinchFeedbackEvent.sendEvent(self, isToHeavy, isWinching, speedup, userId)
end

function Winch:wRelease()

	if #self.wJoints > 0 then
		setJointTranslationLimit(self.wJoints[1], 2, true, -self.wRopeLength, Winch.JointMarginLimit)
		self.wCurrJointZLimit = nil
	end

end

function Winch:wUpdateJointFrame(joint, refNode, jointRefPos, attachPointPos, updatePulleyAndRopeRefNode)

	local jrX, jrY, jrZ = unpack(jointRefPos)
	local apX, apY, apZ = unpack(attachPointPos)

	local dX = jrX - apX
	local dY = jrY - apY
	local dZ = jrZ - apZ
	
	local dlX, dlY, dlZ = worldDirectionToLocal(getParent(refNode), dX, dY, dZ)
	local upX, upY, upZ = worldDirectionToLocal(getParent(refNode), 0, 1, 0)

	setDirection(refNode, dlX, dlY, dlZ, upX, upY, upZ)

	if updatePulleyAndRopeRefNode ~= nil and updatePulleyAndRopeRefNode == true then
		
		if self.wPulleyNode ~= nil then
			setDirection(self.wPulleyNode, dlX, 0, dlZ, 0, 1, 0)
		end
		
		-- if rope reference node is given then update it's rotation too so rope will be properly directed
		-- NOTE: this should be the case when user uses pulley & wants to offset rope mesh from joint ref point
		if self.wRopeRefNode ~= nil then
		
			jrX, jrY, jrZ = getWorldTranslation(self.wRopeRefNode)

			dX = jrX - apX
			dY = jrY - apY
			dZ = jrZ - apZ
			
			dlX, dlY, dlZ = worldDirectionToLocal(getParent(self.wRopeRefNode), dX, dY, dZ)
			setDirection(self.wRopeRefNode, dlX, dlY, dlZ, 0, 1, 0)

		end

	end

	if joint ~= nil then
		setJointFrame(joint, 0, refNode)
	end

end

function Winch:wUpdateRope(scale, resetRotation)
	
	local jointsCount = #self.wSkinnedRopeJoints

	if self.wRopeShape == nil and jointsCount <= 0 then
		return
	end

	-- if no scale is given calculate it
	if scale == nil then

		local x, y, z = getWorldTranslation(self.wAttachedShapesRefNodes[1])
		local x1, y1, z1 = getWorldTranslation(self.wJointRefNode)

		scale = MathUtil.vector3Length(x1 - x, y1 - y, z1 - z)

	end

	-- as we got the scale now subtract the size of rope end mesh
	scale = math.max(0.05, scale - self.wRopeEndShapeLen)

	if jointsCount > 0 then
		
		local transZPerJoint = scale / (jointsCount - 1)
		local wantedYs = {}
		local wantedYsIndices = {}
		
		-- collect world Y positions for all joints
		local anyJointUnderGround = false
		for i=1,jointsCount,1 do

			-- first make sure to reset transfromations so any calculations can be done properly!
			setRotation(self.wSkinnedRopeJoints[i], 0, 0, 0)
			setTranslation(self.wSkinnedRopeJoints[i], 0, 0, (i - 1) * transZPerJoint)

			local x,y,z = localToWorld(self.wSkinnedRopeJoints[1], 0, 0, (i - 1) * transZPerJoint)
			local terrainY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, y, z)

			if terrainY + self.wSkinnedRopeBias >= y then
				-- get terrain once again, for better approximation of terrain height for this joint
				terrainY = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, terrainY, z)
				_,y,_ = worldToLocal(self.wSkinnedRopeJoints[1], x, terrainY + self.wSkinnedRopeBias, z)

				wantedYs[i] = y
				anyJointUnderGround = true
			else
				wantedYs[i] = 0
			end

			wantedYsIndices[i] = i
		end

		-- remove ones that would give effect of "pulling down" of the rope, 
		-- by naive comparison of middle point between selected two points and
		-- removing it if it's lower that would be straight line from next and prev point
		if anyJointUnderGround then
			local anyPointRemoved = true
			while anyPointRemoved do
				
				anyPointRemoved = false

				-- until any points are removed we have run algorithm again
				local i = 3
				while i <= #wantedYs do
					local prevY = wantedYs[i-2]
					local prevIdx = wantedYsIndices[i-2]
					local nextY = wantedYs[i]
					local nextIdx = wantedYsIndices[i]
					local currY = wantedYs[i-1]
					local currIdx = wantedYsIndices[i-1]

					-- if predicted height for current point is lower then it would be,
					-- on straight line from previous to next point, then remove current point
					-- otherwise move to next point
					if prevY + (nextY - prevY) * ((currIdx - prevIdx) / (nextIdx - prevIdx)) > currY then
						table.remove(wantedYs, i-1)
						table.remove(wantedYsIndices, i-1)
						anyPointRemoved = true
					else
						i = i + 1
					end
				end
			end
		end

		-- set new translations to all joints
		local prevHighestIdx = wantedYsIndices[1]
		table.remove(wantedYsIndices, 1)
		local nextHighestIdx = wantedYsIndices[1]
		table.remove(wantedYsIndices, 1)
		for i=1,jointsCount,1 do

			local currY = 0
			if i == prevHighestIdx then
				currY = wantedYs[1]
			elseif i < nextHighestIdx then
				currY = (wantedYs[2] - wantedYs[1]) * ((i - prevHighestIdx) / (nextHighestIdx - prevHighestIdx)) + wantedYs[1]
			elseif i == jointsCount then
				currY = 0  -- last joint has to be on zero always, to be sticked to shape reference node even if underground
			else
				currY = wantedYs[2]
				
				prevHighestIdx = nextHighestIdx
				nextHighestIdx = wantedYsIndices[1]
				table.remove(wantedYsIndices, 1)
				table.remove(wantedYs, 1)
			end

			local currZ = (i - 1) * transZPerJoint
			setTranslation(self.wSkinnedRopeJoints[i], 0, currY, currZ)
		end

		-- interpolate angles to new curvature only if one of joints was under ground
		if anyJointUnderGround then
			for i=2,jointsCount-1,1 do

				local prevX, prevY, prevZ = getTranslation(self.wSkinnedRopeJoints[i - 1])
				local currX, currY, currZ = getTranslation(self.wSkinnedRopeJoints[i])
				local nextX, nextY, nextZ = getTranslation(self.wSkinnedRopeJoints[i + 1])

				local angle = -math.atan2((currY - prevY), (currZ - prevZ))
				local angle2 = -math.atan2((nextY - currY), (nextZ - currZ))
				
				setRotation(self.wSkinnedRopeJoints[i], (angle + angle2) / 2, 0, 0)

				if i == 2 then
					setRotation(self.wSkinnedRopeJoints[i-1], angle, 0, 0)
				elseif i == jointsCount - 1 then
					
					-- now compensate height for end rope mesh, 
					-- which will be after applying the rotation displaced
					local diffY = math.tan(angle2) * self.wRopeEndShapeLen
					setTranslation(self.wSkinnedRopeJoints[i + 1], nextX, nextY + diffY, nextZ)
					setRotation(self.wSkinnedRopeJoints[i + 1], angle2, 0, 0)
					
					-- propagate change down the joints until there is any joint that's higher than last one
					local ii = i
					local x, y, z = getTranslation(self.wSkinnedRopeJoints[ii])
					local currDiffY = diffY * (ii / jointsCount)  -- slowly fade out the difference
					while y > nextY + currDiffY and ii > 1 do
						setTranslation(self.wSkinnedRopeJoints[ii], x, y + currDiffY, z)
						ii = ii - 1
						currDiffY = diffY * (ii / jointsCount)
						x, y, z = getTranslation(self.wSkinnedRopeJoints[ii])
					end
				end
			end
		end
	end

	if self.wRopeShape ~= nil then

		if jointsCount <= 0 then

			local sX, sY, _ = getScale(self.wRopeShape)
			setScale(self.wRopeShape, sX, sY, scale)

			if resetRotation ~= nil and resetRotation == true then
				setRotation(self.wRopeShape, 0, 0, 0)
			end

		end

		local x,_,z,w = getShaderParameter(self.wRopeShape, "uvScale")
		setShaderParameter(self.wRopeShape, "uvScale", x, scale, z, w, false)

		for i=1,#self.wUVScrollRopes,1 do

			x,_,z,w = getShaderParameter(self.wUVScrollRopes[i], "offsetUV")
			setShaderParameter(self.wUVScrollRopes[i], "offsetUV", x, (scale * 4) % 1, z, w, false)

		end

	end

	if resetRotation ~= nil and resetRotation == true and self.wRopeRefNode then

		local jrX, jrY, jrZ = getTranslation(self.wRopeRefNode)

		local dlX = jrX + 0
		local dlY = jrY + 0.6
		local dlZ = jrZ + 0.4
		
		setDirection(self.wRopeRefNode, dlX, dlY, dlZ, 0, 1, 0)

	end
end

function Winch:wUpdateChain(idx)

	if self.wAttachedShapesChainNodes[idx] == nil then
		return
	end

	local jrX, jrY, jrZ = getWorldTranslation(self.wAttachedShapesRefNodes[1])
	local asX, asY, asZ = getWorldTranslation(self.wAttachedShapesChainNodes[idx])

	local dX = jrX - asX
	local dY = jrY - asY
	local dZ = jrZ - asZ
	local scale = MathUtil.vector3Length(dX, dY, dZ)

	local dlX, dlY, dlZ = worldDirectionToLocal(getParent(self.wAttachedShapesChainNodes[idx]), dX, dY, dZ)
	local upX, upY, upZ = worldDirectionToLocal(getParent(self.wAttachedShapesChainNodes[idx]), 0, 1, 0)

	setDirection(self.wAttachedShapesChainNodes[idx], dlX, dlY, dlZ, upX, upY, upZ)

	local sX, sY, _ = getScale(self.wAttachedShapesChainNodes[idx])
	setScale(self.wAttachedShapesChainNodes[idx], sX, sY, scale)

	local x,_,z,w = getShaderParameter(self.wAttachedShapesChainNodes[idx], "uvScale")
	setShaderParameter(self.wAttachedShapesChainNodes[idx], "uvScale", x, scale, z, w, false)

end


--
-- MP ready
--

wAttachDetachEvent = {}
local wAttachDetachEvent_mt = Class(wAttachDetachEvent, Event)

InitEventClass(wAttachDetachEvent, "wAttachDetachEvent")

function wAttachDetachEvent.emptyNew()
    local self = Event.new(wAttachDetachEvent_mt)
    return self
end

function wAttachDetachEvent.new(object, x, y, z, x1, y1, z1, isDetach)
    local self = wAttachDetachEvent.emptyNew()
    self.object = object
	self.x = x
	self.y = y
	self.z = z
	self.x1 = x1
	self.y1 = y1
	self.z1 = z1
	self.isDetach = isDetach
    return self
end

function wAttachDetachEvent:readStream(streamId, connection)
    self.object = NetworkUtil.readNodeObject(streamId)
	self.x = streamReadFloat32(streamId)
	self.y = streamReadFloat32(streamId)
	self.z = streamReadFloat32(streamId)
	self.x1 = streamReadFloat32(streamId)
	self.y1 = streamReadFloat32(streamId)
	self.z1 = streamReadFloat32(streamId)
	self.isDetach = streamReadBool(streamId)
	self:run(connection)
end

function wAttachDetachEvent:writeStream(streamId, connection)
	NetworkUtil.writeNodeObject(streamId, self.object)
	streamWriteFloat32(streamId, self.x)
	streamWriteFloat32(streamId, self.y)
	streamWriteFloat32(streamId, self.z)
	streamWriteFloat32(streamId, self.x1)
	streamWriteFloat32(streamId, self.y1)
	streamWriteFloat32(streamId, self.z1)
	streamWriteBool(streamId, self.isDetach)
end

function wAttachDetachEvent:run(connection)
	
	if not connection:getIsServer() then
        g_server:broadcastEvent(wAttachDetachEvent.new(self.object, self.x, self.y, self.z, self.x1, self.y1, self.z1, self.isDetach), nil, connection, self.object)
	end

	if self.object ~= nil then
		self.object:wAttachDetachObject(self.x, self.y, self.z, self.x1, self.y1, self.z1, self.isDetach)
	end
end

function wAttachDetachEvent.sendEvent(vehicle, player, noEventSend)

	local x,y,z = getWorldTranslation(player.model.pickUpKinematicHelperNode)
	local x1, y1, z1 = getWorldTranslation(player.cameraNode)

	-- first try detach, if not succesful then go for attach
	local isDetach = true
	local success = vehicle:wAttachDetachObject(x, y, z, x1, y1, z1, isDetach)
	if not success then
		isDetach = false
		success = vehicle:wAttachDetachObject(x, y, z, x1, y1, z1, isDetach)
	end

	-- also play sound inside origin game, other players can't really have this sound playing
	-- as we would need to position this sound on the place of attaching, but we don't know how!
	if success then
		if isDetach and vehicle.wChainDetachSndSample ~= nil then
			g_soundManager:playSample(vehicle.wChainDetachSndSample, 1)
		elseif not isDetach and vehicle.wChainAttachSndSample ~= nil then
			g_soundManager:playSample(vehicle.wChainAttachSndSample, 1)
		end
	end

	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(wAttachDetachEvent.new(vehicle, x, y, z, x1, y1, z1, isDetach), nil, nil, vehicle)
		else
			g_client:getServerConnection():sendEvent(wAttachDetachEvent.new(vehicle, x, y, z, x1, y1, z1, isDetach))
		end
	end
end


wDetachAllEvent = {}
local wDetachAllEvent_mt = Class(wDetachAllEvent, Event)

InitEventClass(wDetachAllEvent, "wDetachAllEvent")

function wDetachAllEvent.emptyNew()
    local self = Event.new(wDetachAllEvent_mt)
    return self
end

function wDetachAllEvent.new(object)
    local self = wDetachAllEvent.emptyNew()
    self.object = object
    return self
end

function wDetachAllEvent:readStream(streamId, connection)
    self.object = NetworkUtil.readNodeObject(streamId)
	self:run(connection)
end

function wDetachAllEvent:writeStream(streamId, connection)
	NetworkUtil.writeNodeObject(streamId, self.object)
end

function wDetachAllEvent:run(connection)
	
	if not connection:getIsServer() then
        g_server:broadcastEvent(wDetachAllEvent.new(self.object), nil, connection, self.object)
	end

	if self.object ~= nil then
		self.object:wDetachObject(self.object.wAttachedShapes[1])
	end
end

function wDetachAllEvent.sendEvent(vehicle, noEventSend)

	if vehicle:wDetachObject(vehicle.wAttachedShapes[1]) then
		if vehicle.wChainDetachSndSample ~= nil then
			g_soundManager:playSample(vehicle.wChainDetachSndSample, 1)
		end
	end

	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(wDetachAllEvent.new(vehicle), nil, nil, vehicle)
		else
			g_client:getServerConnection():sendEvent(wDetachAllEvent.new(vehicle))
		end
	end
end


wWinchEvent = {}
local wWinchEvent_mt = Class(wWinchEvent, Event)

InitEventClass(wWinchEvent, "wWinchEvent")

function wWinchEvent.emptyNew()
    local self = Event.new(wWinchEvent_mt)
    return self
end

function wWinchEvent.new(object, isSpeedup, userId)
    local self = wWinchEvent.emptyNew()
    self.object = object
	self.isSpeedup = isSpeedup
	self.userId = userId
    return self
end

function wWinchEvent:readStream(streamId, connection)
    self.object = NetworkUtil.readNodeObject(streamId)
	self.isSpeedup = streamReadBool(streamId)
	self.userId = NetworkUtil.readNodeObjectId(streamId)	
	self:run(connection)
end

function wWinchEvent:writeStream(streamId, connection)
	NetworkUtil.writeNodeObject(streamId, self.object)
	streamWriteBool(streamId, self.isSpeedup)
	NetworkUtil.writeNodeObjectId(streamId, self.userId)
end

function wWinchEvent:run(connection)
	if self.object ~= nil then
    	self.object:wWinch(self.isSpeedup, self.userId)
	end
end

function wWinchEvent.sendEvent(vehicle, isSpeedup, userId, noEventSend)

	if vehicle.isServer then
		vehicle:wWinch(isSpeedup, userId)
	else
		if noEventSend == nil or noEventSend == false then
			g_client:getServerConnection():sendEvent(wWinchEvent.new(vehicle, isSpeedup, userId))
		end
	end
end


wWinchFeedbackEvent = {}
local wWinchFeedbackEvent_mt = Class(wWinchFeedbackEvent, Event)

InitEventClass(wWinchFeedbackEvent, "wWinchFeedbackEvent")

function wWinchFeedbackEvent.emptyNew()
    local self = Event.new(wWinchFeedbackEvent_mt)
    return self
end

function wWinchFeedbackEvent.new(object, isToHeavy, isWinching, isSpeedup, userId)
    local self = wWinchFeedbackEvent.emptyNew()
    self.object = object
    self.isToHeavy = isToHeavy
    self.isWinching = isWinching
    self.isSpeedup = isSpeedup
    self.userId = userId
    return self
end

function wWinchFeedbackEvent:readStream(streamId, connection)
    self.object = NetworkUtil.readNodeObject(streamId)
    self.isToHeavy = streamReadBool(streamId)
    self.isWinching = streamReadBool(streamId)
    self.isSpeedup = streamReadBool(streamId)
    self.userId = NetworkUtil.readNodeObjectId(streamId)
	self:run(connection)
end

function wWinchFeedbackEvent:writeStream(streamId, connection)
	NetworkUtil.writeNodeObject(streamId, self.object)
	streamWriteBool(streamId, self.isToHeavy)
	streamWriteBool(streamId, self.isWinching)
	streamWriteBool(streamId, self.isSpeedup)
	NetworkUtil.writeNodeObjectId(streamId, self.userId)
end

function wWinchFeedbackEvent:run(connection)
	if self.object ~= nil then
		if self.isToHeavy then
			if g_currentMission.playerUserId == self.userId then
				g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachedObjectsTooHeavy"), 2000)
			end
		end

		if self.object.wSoundSample ~= nil then
			if self.isWinching then
				if self.isSpeedup then
					self.object.wCurrPtoRpm = self.object.wSpeedupWinchPtoRpm
				else
					self.object.wCurrPtoRpm = self.object.wWinchPtoRpm
				end

				-- we can use this variable to tell client update function that winch is winching,
				-- so sounds can be properly updated once timer is up
				Winch.ClientWinchingTimer = 200
			end
		end
	end
end

function wWinchFeedbackEvent.sendEvent(vehicle, isToHeavy, isWinching, isSpeedup, userId, noEventSend)

	--if not g_currentMission.connectedToDedicatedServer then

	if not g_currentMission.connectedToDedicatedServer and g_server ~= nil and isToHeavy then
		if g_currentMission.playerUserId == userId then
			g_currentMission:showBlinkingWarning(g_i18n:getText("warning_attachedObjectsTooHeavy"), 2000)
		end
	end

	if vehicle.wSoundSample ~= nil then
		if isWinching then
			if isSpeedup then
				vehicle.wCurrPtoRpm = vehicle.wSpeedupWinchPtoRpm
			else
				vehicle.wCurrPtoRpm = vehicle.wWinchPtoRpm
			end

			-- we can use this variable to tell client update function that winch is winching,
			-- so sounds can be properly updated once timer is up
			Winch.ClientWinchingTimer = 200
		end
	end

	if noEventSend == nil or noEventSend == false then
		if g_server ~= nil then
			g_server:broadcastEvent(wWinchFeedbackEvent.new(vehicle, isToHeavy, isWinching, isSpeedup, userId), nil, nil, vehicle)
		else
			g_client:getServerConnection():sendEvent(wWinchFeedbackEvent.new(vehicle, isToHeavy, isWinching, isSpeedup, userId))
		end
	end
end


wReleaseEvent = {}
local wReleaseEvent_mt = Class(wReleaseEvent, Event)

InitEventClass(wReleaseEvent, "wReleaseEvent")

function wReleaseEvent.emptyNew()
    local self = Event.new(wReleaseEvent_mt)
    return self
end

function wReleaseEvent.new(object)
    local self = wReleaseEvent.emptyNew()
    self.object = object
    return self
end

function wReleaseEvent:readStream(streamId, connection)
    self.object = NetworkUtil.readNodeObject(streamId)
	self:run(connection)
end

function wReleaseEvent:writeStream(streamId, connection)
	NetworkUtil.writeNodeObject(streamId, self.object)
end

function wReleaseEvent:run(connection)
	if self.object ~= nil then
    	self.object:wRelease()
	end
end

function wReleaseEvent.sendEvent(vehicle, noEventSend)

	if vehicle.isServer then
		vehicle:wRelease()
	else
		if noEventSend == nil or noEventSend == false then
			g_client:getServerConnection():sendEvent(wReleaseEvent.new(vehicle))
		end
	end
end
