# -*- coding: utf-8 -*-

from __future__ import with_statement
from binascii import hexlify
import os
import re
import subprocess as sp
import sys
import time
import warnings
from time import sleep

from options import use_colord_gi

try:
	# XXX D-Bus API is more complete currently
	if not use_colord_gi:
		raise ImportError("")
	from gi.repository import Colord
	from gi.repository import Gio
except ImportError:
	Colord = None
	Gio = None
else:
	cancellable = Gio.Cancellable.new();

from util_dbus import DBusObject, DBusException, BUSTYPE_SYSTEM

from util_os import which
from util_str import safe_str, safe_unicode
import localization as lang

if sys.platform not in ("darwin", "win32"):
	from defaultpaths import xdg_data_home

	if not Colord:
		try:
			Colord = DBusObject(BUSTYPE_SYSTEM, "org.freedesktop.ColorManager", 
												"/org/freedesktop/ColorManager")
		except DBusException, exception:
			warnings.warn(safe_str(exception), Warning)


# See colord/cd-client.c
CD_CLIENT_IMPORT_DAEMON_TIMEOUT = 5000  # ms


if (not Colord or isinstance(Colord, DBusObject) or
	not hasattr(Colord, 'quirk_vendor_name')):
	from config import get_data_path

	# From colord/lib/colord/cd_quirk.c, cd_quirk_vendor_name
	quirk_cache = {
		"suffixes": [
			"Co.", "Co", "Inc.", "Inc", "Ltd.", "Ltd",
			"Corporation", "Incorporated", "Limited",
			"GmbH", "corp."
		],
		"vendor_names": {
			"Acer, inc.": "Acer",
			"Acer Technologies": "Acer",
			"AOC Intl": "AOC",
			"Apple Computer Inc": "Apple",
			"Arnos Insturments & Computer Systems": "Arnos",
			"ASUSTeK Computer Inc.": "ASUSTeK",
			"ASUSTeK Computer INC": "ASUSTeK",
			"ASUSTeK COMPUTER INC.": "ASUSTeK",
			"BTC Korea Co., Ltd": "BTC",
			"CASIO COMPUTER CO.,LTD": "Casio",
			"CLEVO": "Clevo",
			"Delta Electronics": "Delta",
			"Eizo Nanao Corporation": "Eizo",
			"Envision Peripherals,": "Envision",
			"FUJITSU": "Fujitsu",
			"Fujitsu Siemens Computers GmbH": "Fujitsu Siemens",
			"Funai Electric Co., Ltd.": "Funai",
			"Gigabyte Technology Co., Ltd.": "Gigabyte",
			"Goldstar Company Ltd": "LG",
			"LG Electronics": "LG",
			"GOOGLE": "Google",
			"Hewlett-Packard": "Hewlett Packard",
			"Hitachi America Ltd": "Hitachi",
			"HP": "Hewlett Packard",
			"HWP": "Hewlett Packard",
			"IBM France": "IBM",
			"Lenovo Group Limited": "Lenovo",
			"LENOVO": "Lenovo",
			"Iiyama North America": "Iiyama",
			"MARANTZ JAPAN, INC.": "Marantz",
			"Mitsubishi Electric Corporation": "Mitsubishi",
			"Nexgen Mediatech Inc.,": "Nexgen Mediatech",
			"NIKON": "Nikon",
			"Panasonic Industry Company": "Panasonic",
			"Philips Consumer Electronics Company": "Philips",
			"RGB Systems, Inc. dba Extron Electronics": "Extron",
			"SAM": "Samsung",
			"Samsung Electric Company": "Samsung",
			"Samsung Electronics America": "Samsung",
			"samsung": "Samsung",
			"SAMSUNG": "Samsung",
			"Sanyo Electric Co.,Ltd.": "Sanyo",
			"Sonix Technology Co.": "Sonix",
			"System manufacturer": "Unknown",
			"To Be Filled By O.E.M.": "Unknown",
			"Toshiba America Info Systems Inc": "Toshiba",
			"Toshiba Matsushita Display Technology Co.,": "Toshiba",
			"TOSHIBA": "Toshiba",
			"Unknown vendor": "Unknown",
			"Westinghouse Digital Electronics": "Westinghouse Digital",
			"Zalman Tech Co., Ltd.": "Zalman"
		}
	}


prefix = "/org/freedesktop/ColorManager/"
device_ids = {}


def client_connect():
	""" Connect to colord """
	client = Colord.Client.new()

	# Connect to colord
	if not client.connect_sync(cancellable):
		raise CDError("Couldn't connect to colord")
	return client


def device_connect(client, device_id):
	""" Connect to device """
	if isinstance(device_id, unicode):
		device_id = device_id.encode('UTF-8')
	try:
		device = client.find_device_sync(device_id, cancellable)
	except Exception, exception:
		raise CDError(exception.args[0])

	# Connect to device
	if not device.connect_sync(cancellable):
		raise CDError("Couldn't connect to device with ID %r" % device_id)
	return device


def device_id_from_edid(edid, quirk=False, use_serial_32=True,
						truncate_edid_strings=False,
						omit_manufacturer=False, query=False):
	""" Assemble device key from EDID """
	# https://github.com/hughsie/colord/blob/master/doc/device-and-profile-naming-spec.txt
	# Should match device ID returned by gcm_session_get_output_id in
	# gnome-settings-daemon/plugins/color/gsd-color-state.c
	# and Edid::deviceId in colord-kde/colord-kded/Edid.cpp respectively
	if "hash" in edid:
		device_id = device_ids.get(edid["hash"])
		if device_id:
			return device_id
		elif (sys.platform not in ("darwin", "win32") and query and
			  isinstance(Colord, DBusObject)):
			try:
				device = Device(find("device-by-property", ["OutputEdidMd5",
															edid["hash"]]))
				device_id = device.properties.get("DeviceId")
			except CDError, exception:
				warnings.warn(safe_str(exception), Warning)
			else:
				if device_id:
					device_ids[edid["hash"]] = device_id
					return device_id
	parts = ["xrandr"]
	edid_keys = ["monitor_name", "serial_ascii"]
	if not omit_manufacturer:
		edid_keys.insert(0, "manufacturer")
	if use_serial_32:
		edid_keys.append("serial_32")
	for name in edid_keys:
		value = edid.get(name)
		if value:
			if name == "serial_32" and "serial_ascii" in edid:
				# Only add numeric serial if no ascii serial
				continue
			elif name == "manufacturer":
				if quirk:
					value = quirk_manufacturer(value)
			elif isinstance(value, basestring) and truncate_edid_strings:
				# Older versions of colord used only the first 12 bytes
				value = value[:12]
			parts.append(str(value))
	if len(parts) > 1:
		device_id = "-".join(parts)
		return device_id


def find(what, search):
	""" Find device or profile and return object path """
	if not isinstance(Colord, DBusObject):
		raise CDError("colord API not available")
	if not isinstance(search, list):
		search = [search]
	method_name = "_".join(part for part in what.split("-"))
	try:
		return getattr(Colord, "find_" + method_name)(*search)
	except Exception, exception:
		if hasattr(exception, "get_dbus_name"):
			if exception.get_dbus_name() == "org.freedesktop.ColorManager.NotFound":
				raise CDObjectNotFoundError(safe_str(exception))
			else:
				raise CDObjectQueryError(safe_str(exception))
		raise CDError(safe_str(exception))


def get_default_profile(device_id):
	"""
	Get default profile for device
	
	"""
	# Find device object path
	device = Device(get_object_path(device_id, "device"))
	
	# Get default profile
	try:
		properties = device.properties
	except Exception, exception:
		raise CDError(safe_str(exception))
	else:
		if properties.get("ProfilingInhibitors"):
			return None
		profiles = properties.get("Profiles")
		if profiles:
			return profiles[0]
		else:
			raise CDError("Couldn't get default profile for device ID %r" %
						  device_id)


def get_devices_by_kind(kind):
	if not isinstance(Colord, DBusObject):
		return []
	return [Device(unicode(object_path, "UTF-8"))
			for object_path in Colord.get_devices_by_kind(kind)]


def get_display_devices():
	return get_devices_by_kind("display")


def get_display_device_ids():
	return filter(None, (display.properties.get("DeviceId")
						 for display in get_display_devices()))


def get_object_path(search, object_type):
	""" Get object path for profile or device ID """
	result = find(object_type + "-by-id", search)
	if not result:
		raise CDObjectNotFoundError("Could not find object path for %s" % search)
	return result


def install_profile(device_id, profile,
					timeout=CD_CLIENT_IMPORT_DAEMON_TIMEOUT / 1000.0,
					logfn=None):
	"""
	Install profile for device
	
	timeout				  Time to allow for colord to pick up new profiles
						  (recommended not below 2 secs)
	
	"""

	profile_installname = os.path.join(xdg_data_home, 'icc',
									   os.path.basename(profile.fileName))

	profile_exists = os.path.isfile(profile_installname)
	if (profile.fileName != profile_installname and
		profile_exists):
		if logfn:
			logfn(u"About to overwrite existing", profile_installname)
		profile.fileName = None

	if profile.ID == "\0" * 16:
		profile.calculateID()
		profile.fileName = None
	profile_id = "icc-" + hexlify(profile.ID)

	# Write profile to destination
	profile_installdir = os.path.dirname(profile_installname)
	if not os.path.isdir(profile_installdir):
		os.makedirs(profile_installdir)
	# colormgr seems to have a bug where the first attempt at importing a
	# specific profile can time out. This seems to be work-aroundable by
	# writing the profile ourself first, and then importing.
	if not profile.fileName or not profile_exists:
		if logfn:
			logfn(u"Writing", profile_installname)
		profile.fileName = profile_installname
		profile.write()

	cdprofile = None

	if Colord and not isinstance(Colord, DBusObject):
		client = client_connect()
	else:
		# Query colord for profile
		try:
			cdprofile = get_object_path(profile_id, "profile")
		except CDObjectQueryError, exception:
			# Profile not found
			pass

		colormgr = which("colormgr")
		if not colormgr:
			raise CDError("colormgr helper program not found")

		from worker import printcmdline

		cmd = safe_str(colormgr)

		if not cdprofile:
			# Import profile

			if logfn:
				logfn("-" * 80)
				logfn(lang.getstr("commandline"))

			args = [cmd, "import-profile", safe_str(profile.fileName)]
			printcmdline(args[0], args[1:], fn=logfn)
			if logfn:
				logfn("")

			ts = time.time()

			maxtries = 3

			for n in xrange(1, maxtries + 1):
				if logfn:
					logfn("Trying to import profile, attempt %i..." % n)
				try:
					p = sp.Popen(args, stdout=sp.PIPE, stderr=sp.STDOUT)
					stdout, stderr = p.communicate()
				except Exception, exception:
					raise CDError(safe_str(exception))
				if logfn and stdout.strip():
					logfn(stdout.strip())
				if p.returncode == 0 or os.path.isfile(profile_installname):
					if logfn:
						logfn("...ok")
					break
				elif logfn:
					logfn("...failed!")

			if p.returncode != 0 and not os.path.isfile(profile_installname):
				raise CDTimeout("Trying to import profile '%s' failed after "
								"%i tries." % (profile.fileName, n))

	if not cdprofile:
		# Query colord for newly added profile
		for i in xrange(int(timeout / 1.0)):
			try:
				if Colord and not isinstance(Colord, DBusObject):
					cdprofile = client.find_profile_sync(profile_id,
														 cancellable)
				else:
					cdprofile = get_object_path(profile_id, "profile")
			except CDObjectQueryError, exception:
				# Profile not found
				pass
			if cdprofile:
				break
			# Give colord time to pick up the profile
			sleep(1)

		if not cdprofile:
			raise CDTimeout("Querying for profile %r returned no result for %s "
							"secs" % (profile_id, timeout))

	errmsg = "Could not make profile %s default for device %s" % (profile_id,
																  device_id)

	if Colord and not isinstance(Colord, DBusObject):
		# Connect to profile
		if not cdprofile.connect_sync(cancellable):
			raise CDError("Could not connect to profile")

		# Connect to existing device
		device = device_connect(client, device_id)

		# Add profile to device
		try:
			device.add_profile_sync(Colord.DeviceRelation.HARD, cdprofile,
									cancellable)
		except Exception, exception:
			# Profile may already have been added
			warnings.warn(safe_str(exception), Warning)

		# Make profile default for device
		if not device.make_profile_default_sync(cdprofile, cancellable):
			raise CDError(errmsg)
	else:
		# Find device object path
		device = get_object_path(device_id, "device")
		
		if logfn:
			logfn("-" * 80)
			logfn(lang.getstr("commandline"))

		# Add profile to device
		# (Ignore returncode as profile may already have been added)
		args = [cmd, "device-add-profile", device, cdprofile]
		printcmdline(args[0], args[1:], fn=logfn)
		if logfn:
			logfn("")
		try:
			p = sp.Popen(args, stdout=sp.PIPE, stderr=sp.STDOUT)
			stdout, stderr = p.communicate()
		except Exception, exception:
			raise CDError(safe_str(exception))
		if logfn and stdout.strip():
			logfn(stdout.strip())

		if logfn:
			logfn("")
			logfn(lang.getstr("commandline"))

		# Make profile default for device
		args = [cmd, "device-make-profile-default", device, cdprofile]
		printcmdline(args[0], args[1:], fn=logfn)
		if logfn:
			logfn("")
		try:
			p = sp.Popen(args, stdout=sp.PIPE, stderr=sp.STDOUT)
			stdout, stderr = p.communicate()
		except Exception, exception:
			raise CDError(safe_str(exception))
		else:
			if p.returncode != 0:
				raise CDError(stdout.strip() or errmsg)
		if logfn and stdout.strip():
			logfn(stdout.strip())


def quirk_manufacturer(manufacturer):
	if (Colord and not isinstance(Colord, DBusObject) and
		hasattr(Colord, 'quirk_vendor_name')):
		return Colord.quirk_vendor_name(manufacturer)

	# Correct some company names
	for old, new in quirk_cache['vendor_names'].iteritems():
		if manufacturer.startswith(old):
			manufacturer = new
			break

	# Get rid of suffixes
	for suffix in quirk_cache['suffixes']:
		if manufacturer.endswith(suffix):
			manufacturer = manufacturer[0:len(manufacturer) - len(suffix)]

	manufacturer = manufacturer.rstrip()

	return manufacturer


class Object(DBusObject):

	def __init__(self, object_path, object_type):
		try:
			DBusObject.__init__(self, BUSTYPE_SYSTEM,
								"org.freedesktop.ColorManager", object_path,
								object_type)
		except DBusException, exception:
			raise CDError(safe_str(exception))
		self._object_type = object_type

	_properties = DBusObject.properties

	@property
	def properties(self):
		try:
			properties = {}
			for key, value in self._properties.iteritems():
				if key == "Profiles":
					value = [Profile(object_path) for object_path in value]
				properties[key] = value
			return properties
		except DBusException, exception:
			raise CDError(safe_str(exception))


class Device(Object):

	def __init__(self, object_path):
		Object.__init__(self, object_path, "Device")


class Profile(Object):

	def __init__(self, object_path):
		Object.__init__(self, object_path, "Profile")


class CDError(Exception):
	pass


class CDObjectQueryError(CDError):
	pass


class CDObjectNotFoundError(CDObjectQueryError):
	pass


class CDTimeout(CDError):
	pass


if __name__ == "__main__":
	import sys
	for arg in sys.argv[1:]:
		print get_default_profile(arg)
