/*
 * Copyright (c) 1998-2006 Apple Computer, Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */
 
#include "AppleUSBAudioLevelControl.h"

#define super IOAudioLevelControl

OSDefineMetaClassAndStructors(AppleUSBAudioLevelControl, IOAudioLevelControl)

AppleUSBAudioLevelControl *AppleUSBAudioLevelControl::create (UInt8 theUnitID, UInt8 theInterfaceNumber, UInt8 theControlSelector, UInt8 theChannelNumber, Boolean shouldUpdatePRAM, USBDeviceRequest theUSBDeviceRequest, void *theCallerRefCon, UInt32 subType, UInt32 usage) {
    AppleUSBAudioLevelControl *			control;

    debugIOLog ("+ AppleUSBAudioLevelControl::create (%d, %d, %d, %d, %p, %lX, %lX)", theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, subType, usage);
    control = new AppleUSBAudioLevelControl;
    FailIf (NULL == control, Exit);

    if (FALSE == control->init (theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, shouldUpdatePRAM, theUSBDeviceRequest, theCallerRefCon, subType, usage)) {
        control->release ();
        control = NULL;
    }

Exit:
    debugIOLog ("- AppleUSBAudioLevelControl[%p]::create (%d, %d, %d, %d, %p, %lX, %lX)", control, theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, subType, usage);
    return control;
}

bool AppleUSBAudioLevelControl::init (UInt8 theUnitID, UInt8 theInterfaceNumber, UInt8 theControlSelector, UInt8 theChannelNumber, Boolean shouldUpdatePRAM, USBDeviceRequest theUSBDeviceRequest, void *theCallerRefCon, UInt32 subType, UInt32 usage, OSDictionary *properties) {
	const char *				channelName = NULL;
	SInt16						currentValue;
	SInt16						deviceMin;
	SInt16						deviceMax;
	IOFixed						deviceMinDB;
	IOFixed						deviceMaxDB;
	IOFixed						resolutionDB;
	IOReturn					ret;
	Boolean						result;
	Boolean						arePRAMControl;

	debugIOLog ("+ AppleUSBAudioLevelControl[%p]::init (%d, %d, %d, %d, %p, %p)", this, theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, properties);
	result = FALSE;
	arePRAMControl = FALSE;

	FailIf (NULL == theUSBDeviceRequest, Exit);
	mSetValueThreadCall = thread_call_allocate ((thread_call_func_t)updateValueCallback, this);
	FailIf (NULL == mSetValueThreadCall, Exit);

	// If this control is supposed to be a pram volume control, pretend we're just a regular volume control
	// so that we can get the min and max dB from the device as we init
	if (theControlSelector == 0xff) 
	{
		theControlSelector = VOLUME_CONTROL;
		arePRAMControl = TRUE;
	}

	if (kIOAudioLevelControlSubTypeLFEVolume == subType) 
	{
		// The iSub controls are on channels 1 and 2, but we want to make them look like they're on channel 0 for the HAL
		// set it to 1 here so we can query the iSub
		theChannelNumber = 1;
	}

	mUnitID = theUnitID;
	mInterfaceNumber = theInterfaceNumber;
	mControlSelector = theControlSelector;
	mChannelNumber = theChannelNumber;
	mCallerRefCon = theCallerRefCon;
	mUSBDeviceRequest = theUSBDeviceRequest;
	fShouldUpdatePRAM = shouldUpdatePRAM;

	switch (mChannelNumber) 
	{
		case kIOAudioControlChannelIDAll:
			channelName = kIOAudioControlChannelNameAll;
			break;
		case kIOAudioControlChannelIDDefaultLeft:
			channelName = kIOAudioControlChannelNameLeft;
			break;
		case kIOAudioControlChannelIDDefaultRight:
			channelName = kIOAudioControlChannelNameRight;
			break;
		case 0xff:
			debugIOLog ("! AppleUSBAudioLevelControl[%p]::init () - Does not support channel number 0xFF.", this);
			return FALSE;
		default:
			channelName = "Unknown";
			break;
	}

	ret = GetCurVolume (mInterfaceNumber, mChannelNumber, &currentValue);
	FailIf (kIOReturnSuccess != ret, Exit);
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - mChannelNumber %d, currentValue = 0x%X", this, mChannelNumber, currentValue);
	ret = GetVolumeResolution (mInterfaceNumber, mChannelNumber, &mVolRes);
	FailIf (kIOReturnSuccess != ret, Exit);
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - mVolRes = %d, ", this, mVolRes);
	ret = GetMinVolume (mInterfaceNumber, mChannelNumber, &deviceMin);
	FailIf (kIOReturnSuccess != ret, Exit);
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - deviceMin = 0x%X, ", this, deviceMin);
	ret = GetMaxVolume (mInterfaceNumber, mChannelNumber, &deviceMax);
	FailIf (kIOReturnSuccess != ret, Exit);
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - deviceMax = 0x%X", this, deviceMax);

	// Having the device say that it does -infinity dB messes up our math, so set the min at -127.9961dB instead.
	if ((SInt16)0x8000 == deviceMin) 
	{
		deviceMin = (SInt16)0x8001;
		debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - deviceMin adjusted to = %d", this, deviceMin);
	}

	deviceMinDB = ConvertUSBVolumeTodB (deviceMin);
	deviceMaxDB = ConvertUSBVolumeTodB (deviceMax);
	resolutionDB = ConvertUSBVolumeTodB (mVolRes);		// The volume is incremented in units of this many dB, represented as 1/256 dB (eg 256 == 1dB of control)

	mOffset = -deviceMin;
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - mOffset = %d", this, mOffset);

	currentValue = (currentValue + mOffset) / mVolRes;
	if (deviceMin < 0 && deviceMax > 0) 
	{
		deviceMax += mVolRes;
		debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - deviceMax adjusted to = 0x%X", this, deviceMax);
	}
	deviceMax = ((deviceMin + mOffset) + (deviceMax + mOffset)) / mVolRes;

	if (kIOAudioLevelControlSubTypeLFEVolume == subType) 
	{
		currentValue = currentValue / 2;
		//updateUSBValue (currentValue);
	}

	// Set values needed to compute proper PRAM boot beep volume setting
	fMaxVolume = deviceMax;
	fMinVolume = deviceMin + mOffset;
	deviceMin = -1;

	if (arePRAMControl) 
	{
		// If this is a 'pram' control then there is no need to call the hardware.
		UInt8 						curPRAMVol;
		IODTPlatformExpert * 		platform = NULL;

		curPRAMVol = 0;
		platform = OSDynamicCast (IODTPlatformExpert, getPlatform ());
		if (NULL != platform) 
		{
			platform->readXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);
			curPRAMVol = (curPRAMVol & 0xF8);
		}
		currentValue = curPRAMVol;
		deviceMin = 0;
		deviceMax = 7;
		subType = kIOAudioLevelControlSubTypePRAMVolume;
		channelName = kIOAudioControlChannelNameAll;
		theChannelNumber = 0;	// force it to the master channel even though we're piggy backing off of the channel 1 control
	}

	if (kIOAudioLevelControlSubTypeLFEVolume == subType) 
	{
		// The iSub controls are on channels 1 and 2, but we want to make them look like they're on channel 0 for the HAL
		theChannelNumber = 0;
		channelName = kIOAudioControlChannelNameAll;
	}

	debugIOLog ("? AppleUSBAudioLevelControl[%p]::init () - min = %d, max = %d, current = %d", this, deviceMin, deviceMax, currentValue);

	FailIf (FALSE == super::init (currentValue, deviceMin, deviceMax, deviceMinDB, deviceMaxDB, theChannelNumber, channelName, 0, subType, usage), Exit);

	if (kIOAudioLevelControlSubTypeLFEVolume == subType) 
	{
		updateUSBValue (currentValue);
	}

	result = TRUE;

Exit:
	debugIOLog ("- AppleUSBAudioLevelControl[%p]::init (%d, %d, %d, %d, %p, %p)", this, theUnitID, theInterfaceNumber, theControlSelector, theChannelNumber, theUSBDeviceRequest, properties);
	return result;
}

void AppleUSBAudioLevelControl::free () {
    debugIOLog ("+ AppleUSBAudioLevelControl[%p]::free ()", this);

    if (mSetValueThreadCall) 
	{
        thread_call_free (mSetValueThreadCall);
        mSetValueThreadCall = NULL;
    }

    super::free ();
    debugIOLog ("- AppleUSBAudioLevelControl[%p]::free ()", this);
}

IOReturn AppleUSBAudioLevelControl::performValueChange (OSObject * newValue) {
	OSNumber *					newValueAsNumber;
	SInt32						newValueAsSInt32;

    debugIOLog ("+ AppleUSBAudioLevelControl[%p]::performValueChange (%p)", this, newValue); 

	newValueAsNumber = OSDynamicCast (OSNumber, newValue);
	FailIf (NULL == newValueAsNumber, Exit);
	newValueAsSInt32 = newValueAsNumber->unsigned32BitValue ();
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::performValueChange (%ld) [converted]", this, newValueAsSInt32);

    if (NULL != mSetValueThreadCall) 
	{
        thread_call_enter1 (mSetValueThreadCall, (thread_call_param_t)newValueAsSInt32);
    }

    debugIOLog ("- AppleUSBAudioLevelControl[%p]::performValueChange (%d)", this, newValueAsSInt32);

Exit:
    return kIOReturnSuccess;
}

void AppleUSBAudioLevelControl::updateUSBValue () {
    updateUSBValue (getIntValue ());
}

void AppleUSBAudioLevelControl::updateUSBValue (SInt32 newValue) {
//	SInt32						newiSubVolume;
    SInt16						theValue;
	SInt16						newVolume;
    IOReturn					ret;

    debugIOLog ("+ AppleUSBAudioLevelControl[%p]::updateUSBValue (%d)", this, newValue);

	if (newValue < 0) 
	{
		newVolume = 0x8000;
	} 
	else 
	{
		if (newValue > 0) 
		{
			newVolume = ((newValue - 1) * mVolRes) - mOffset;
		} 
		else 
		{
			newVolume = (newValue * mVolRes) - mOffset;
		}
	}
    theValue = HostToUSBWord (newVolume);
	debugIOLog ("? AppleUSBAudioLevelControl[%p]::updateUSBValue () - Volume value is 0x%x. Setting volume to 0x%x (little endian)", this, newVolume, theValue);
	ret = SetCurVolume (mInterfaceNumber, mChannelNumber, theValue);

	if (getSubType () == kIOAudioLevelControlSubTypeLFEVolume) 
	{
		// We set the iSub's left volume (on channel 1 above), now set it on channel 2 to mimic having only a master volume control
		ret = SetCurVolume (mInterfaceNumber, 2, theValue);
	}

	if (TRUE == fShouldUpdatePRAM && FALSE == gExpertMode) 
	{	
		// We do that only if we are on a OS 9 like UI guideline
		WritePRAMVol (newValue, newValue);
	}
	
    if (ret != kIOReturnSuccess) 
	{
        debugIOLog ("! AppleUSBAudioLevelContol::updateUSBValue () - Set current value for control selector %d, channel number %d failed with error 0x%X", mControlSelector, mChannelNumber, ret);
    }

    debugIOLog ("- AppleUSBAudioLevelControl[%p]::updateUSBValue (%d)", this, newValue);
}

IOReturn AppleUSBAudioLevelControl::GetCurVolume (UInt8 interfaceNumber, UInt8 channelNumber, SInt16 * target) {
    IOUSBDevRequest				devReq;
    SInt16						theVolume;
	IOReturn					result;
	
	result = kIOReturnError;
	theVolume = 0;
	FailIf ( NULL == target, Exit );
	
    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_CUR;
    devReq.wValue = (mControlSelector << 8) | channelNumber;
    devReq.wIndex = (mUnitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theVolume;

	result = mUSBDeviceRequest (&devReq, mCallerRefCon, NULL);
    FailIf (kIOReturnSuccess != result, Exit);

Exit:
	if (NULL != target) 
	{
		* target = USBToHostWord (theVolume);
	}
    return result;
}

IOReturn AppleUSBAudioLevelControl::GetMaxVolume (UInt8 interfaceNumber, UInt8 channelNumber, SInt16 * target) {
    IOReturn					result;
	IOUSBDevRequest				devReq;
    SInt16						theVolume;

	result = kIOReturnError;
	theVolume = 0;
	FailIf (NULL == target, Exit);
	
    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_MAX;
    devReq.wValue = (mControlSelector << 8) | channelNumber;
    devReq.wIndex = (mUnitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theVolume;

	result = mUSBDeviceRequest (&devReq, mCallerRefCon, NULL);
    FailIf (kIOReturnSuccess != result, Exit);

Exit:
	if (NULL != target) 
	{
		* target = USBToHostWord (theVolume);
	}
    return result;
}

IOReturn AppleUSBAudioLevelControl::GetMinVolume (UInt8 interfaceNumber, UInt8 channelNumber, SInt16 * target) {
    IOReturn					result;
	IOUSBDevRequest				devReq;
    SInt16						theVolume;

    result = kIOReturnError;
	theVolume = 0;
	FailIf (NULL == target, Exit);
	
	devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_MIN;
    devReq.wValue = (mControlSelector << 8) | channelNumber;
    devReq.wIndex = (mUnitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theVolume;

	result = mUSBDeviceRequest (&devReq, mCallerRefCon, NULL);
    FailIf (kIOReturnSuccess != result, Exit);

Exit:
	if (NULL != target) 
	{
		* target = USBToHostWord (theVolume);
	}
    return result;
}

IOReturn AppleUSBAudioLevelControl::GetVolumeResolution (UInt8 interfaceNumber, UInt8 channelNumber, UInt16 * target) {
    IOReturn					result;
	IOUSBDevRequest				devReq;
    UInt16						theResolution;

	result = kIOReturnError;
	theResolution = 0;
	FailIf (NULL == target, Exit);
	
    devReq.bmRequestType = USBmakebmRequestType (kUSBIn, kUSBClass, kUSBInterface);
    devReq.bRequest = GET_RES;
    devReq.wValue = (mControlSelector << 8) | channelNumber;
    devReq.wIndex = (mUnitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &theResolution;

	result = mUSBDeviceRequest (&devReq, mCallerRefCon, NULL);
    FailIf (kIOReturnSuccess != result, Exit);

Exit:
	if (NULL != target) 
	{
		* target = USBToHostWord (theResolution);
	}
    return result;
}

IOReturn AppleUSBAudioLevelControl::SetCurVolume (UInt8 interfaceNumber, UInt8 channelNumber, SInt16 volume) {
    IOUSBDevRequest				devReq;
	IOReturn					error;

    devReq.bmRequestType = USBmakebmRequestType (kUSBOut, kUSBClass, kUSBInterface);
    devReq.bRequest = SET_CUR;
    devReq.wValue = (mControlSelector << 8) | channelNumber;
    devReq.wIndex = (mUnitID << 8) | interfaceNumber;
    devReq.wLength = 2;
    devReq.pData = &volume;

	FailIf ((TRUE == isInactive()), DeviceInactive);  	// In case we've been unplugged during sleep
	error = mUSBDeviceRequest (&devReq, mCallerRefCon, NULL);
    FailIf (kIOReturnSuccess != error, Exit);

Exit:
    return error;
	
DeviceInactive:
	debugIOLog("! AppleUSBAudioLevelControl::SetCurVolume () - ERROR attempt to send a device request to and inactive device");
	error = kIOReturnError;
	goto Exit;
}

void AppleUSBAudioLevelControl::updateValueCallback (void *arg1, void *arg2) {
    AppleUSBAudioLevelControl 		*levelControl;
    SInt32							value;
	UInt32							subType;

    debugIOLog ("+ AppleUSBAudioLevelControl::updateValueCallback (%p, %p)", (UInt32*)arg1, (UInt32*)arg2);
    levelControl = OSDynamicCast (AppleUSBAudioLevelControl, (OSObject*)arg1);
    value = (SInt32)arg2;

    if (levelControl) 
	{
		subType = levelControl->getSubType ();
		
		if (kIOAudioLevelControlSubTypePRAMVolume == subType) 
		{
			UInt8 						curPRAMVol;
			IODTPlatformExpert * 		platform = NULL;

			platform = OSDynamicCast (IODTPlatformExpert, getPlatform ());
			if (NULL != platform) 
			{
				platform->readXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);
				curPRAMVol = (curPRAMVol & 0xF8) | value;
				platform->writeXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);
			}
		} 
		else 
		{
			levelControl->updateUSBValue (value);
		}
    }

    debugIOLog ("- AppleUSBAudioLevelControl::updateValueCallback (%p, %p)", (UInt32*)arg1, (UInt32*)arg2);
}

// This is how the thing is defined in the USB Audio spec (section 5.2.2.4.3.2 for the curious).
// The volume setting of a device is described in 1/256 dB increments using a number that goes from
// a max of 0x7fff (127.9961 dB) down to 0x8001 (-127.9961 dB) using standard signed math, but 0x8000
// is actually negative infinity (not -128 dB), so I have to special case it.
IOFixed AppleUSBAudioLevelControl::ConvertUSBVolumeTodB (SInt16 volume) {
	IOFixed							dBVolumeFixed;

	if (volume == (SInt16)0x8000) 
	{
		dBVolumeFixed = ((SInt16)0x8000 * 256) << 8;	// really is negative infinity
	} 
	else 
	{
		dBVolumeFixed = volume * 256;
	}

	debugIOLog ("? AppleUSBAudioLevelControl::ConvertUSBVolumeTodB () - volume = %d, dBVolumeFixed = 0x%X", volume, dBVolumeFixed);

	return dBVolumeFixed;
}

#pragma mark +PRAM VOLUME
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//	Calculates the PRAM volume value for stereo volume.
UInt8 AppleUSBAudioLevelControl::VolumeToPRAMValue (SInt32 leftVol, SInt32 rightVol) {
	UInt32			pramVolume;						// Volume level to store in PRAM
	UInt32 			averageVolume;					// summed volume
	const UInt32 	volumeRange = (fMaxVolume - fMinVolume + 1);
	UInt32 			volumeSteps;

	if (leftVol < 0)
		leftVol = 0;
	if (rightVol < 0)
		rightVol = 0;

	averageVolume = (leftVol + rightVol) >>  1;		// sum the channel volumes and get an average
	volumeSteps = volumeRange / kMaximumPRAMVolume;	// divide the range by the range of the pramVolume
	pramVolume = averageVolume / volumeSteps;    

	// Since the volume level in PRAM is only worth three bits,
	// we round small values up to 1. This avoids SysBeep from
	// flashing the menu bar when it thinks sound is off and
	// should really just be very quiet.

	if ((pramVolume == 0) && (leftVol != 0 || rightVol !=0 )) 
	{
		pramVolume = 1;
	}

	return (pramVolume & 0x07);
}

void AppleUSBAudioLevelControl::WritePRAMVol (SInt32 leftVol, SInt32 rightVol) {
	UInt8						pramVolume;
	UInt8 						curPRAMVol;
	IODTPlatformExpert * 		platform = NULL;

	platform = OSDynamicCast (IODTPlatformExpert, getPlatform ());

    debugIOLog ("? AppleUSBAudioLevelControl::WritePRAMVol () - leftVol = %lu, rightVol = %lu",leftVol,  rightVol);

    if (NULL != platform) 
	{
		pramVolume = VolumeToPRAMValue (leftVol, rightVol);

		// get the old value to compare it with
		platform->readXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount)1);

		// Update only if there is a change
		if (pramVolume != (curPRAMVol & 0x07)) 
		{
			// clear bottom 3 bits of volume control byte from PRAM low memory image
			curPRAMVol = (curPRAMVol & 0xF8) | pramVolume;
            debugIOLog ("? AppleUSBAudioLevelControl::WritePRAMVol () - curPRAMVol = 0x%x", curPRAMVol);

			// write out the volume control byte to PRAM
			platform->writeXPRAM ((IOByteCount)kPRamVolumeAddr, &curPRAMVol, (IOByteCount) 1);
		}
	}
}
