# -*- coding: utf-8 -*-
# h-client, a client for an h-source server (such as http://www.h-node.org/)
# Copyright (C) 2011  Antonio Gallo
# Copyright (C) 2011  Michał Masłowski  <mtjm@mtjm.eu>
#
#
# h-client is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# h-client is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with h-client.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import absolute_import

import htmlentitydefs
import urllib
from xml.dom import minidom
import tempfile

try:
	from collections import OrderedDict
	assert OrderedDict is not None
except ImportError:
	from hclient.odict import OrderedDict

import pycurl

from hclient import devices
from hclient import machine


def htmlentitiesDecode(string):
	"""Replace HTML entities with UTF-8 characters."""
	for entity,code in htmlentitydefs.name2codepoint.iteritems():
		string = string.replace("&"+entity+";",unichr(code))
		string = string.replace("&#039;","'")
	return string.encode('utf-8')


class HTTPClient(object):

	"""Do HTTP requests."""

	def __init__(self, domain):
		"""Initialize for specific *domain*."""
		self._contents = ""
		self._cookiejar = tempfile.NamedTemporaryFile()
		self._domain = ""
		self.domain = domain

	@property
	def domain(self):
		"""Domain of the server with added trailing slash."""
		return self._domain

	@domain.setter
	def domain(self, domain):
		"""Set domain."""
		self._domain = domain
		# Ensure that the trailing slash is present.
		if domain and domain[-1] != '/':
			self._domain += '/'

	def _body_callback(self, buf):
		"""Process data fetched."""
		self._contents = self._contents + buf

	def perform(self, request_path, post=None):
		"""Fetch the document from ``http://`` + `domain` + *request_path*.

		The optional argument *post* is a dictionary passed as input
		to use the POST method.  If unspecified, GET is used.

		Returns a pair of status (currently ony `False` on error and
		`True` on success) and document content (a string).
		"""
		url = self.domain + request_path
		self._contents = ""
		curl = pycurl.Curl()
		curl.setopt(curl.URL, url)
		curl.setopt(pycurl.COOKIEFILE, self._cookiejar.name)
		curl.setopt(pycurl.COOKIEJAR, self._cookiejar.name)
		if post is not None:
			curl.setopt(curl.POSTFIELDS, urllib.urlencode(post))
		curl.setopt(curl.WRITEFUNCTION, self._body_callback)
		curl.setopt(curl.FOLLOWLOCATION, 1)

		try:
			curl.perform()
			result = True
		except:
			result = False

		curl.close()
		contents = self._contents
		self._contents = ""
		return (result, contents)


def get_devices_from_xml(content):
	"""Return a dictionary of some objects for devices described in
	*content* XML string.

	This function should not be directly used, since the return value
	format might change.  Use `Client.sync` instead.
	"""
	xmldoc = minidom.parseString(content)
	xml_devices = {}
	for device in xmldoc.getElementsByTagName("device"):
		device_type = device.getElementsByTagName("type")[0].childNodes[0].data
		if device_type == "notebook":
			continue
		try:
			vendorid_productid = \
				device.getElementsByTagName("vendorid_productid")[0] \
				.childNodes[0].data
		except IndexError:
			continue
		interface = device.getElementsByTagName("interface")[0].childNodes[0].data
		if interface == "USB":
			prefix = "u_"
		else:
			prefix = "p_"
		xml_devices[prefix + vendorid_productid] = device
	return xml_devices


class Client(object):

	_status = True
	errors = []

	def __init__(self, url = "", request_class=HTTPClient):
		"""Initialize a client for specified h-source node at *url*.

		All HTTP requests are done using *request_class* which by
		default is `HTTPClient`.
		"""

		self._request = request_class(url)

		#: An `OrderedDict` of distros which the server allows to be
		#: listed as tested with the device.
		self.allowedDistros = OrderedDict((
			('blag_90001','BLAG 90001'),
			('blag_120000','BLAG 120000'),
			('blag_140000','BLAG 140000'),
			('dragora_1_1','Dragora 1.1'),
			('dragora_2_0','Dragora 2.0 Ardi'),
			('dragora_2_2', 'Dragora 2.2 Rafaela'),
			('dynebolic_2_5_2','Dynebolic 2.5.2 DHORUBA'),
			('dynebolic_3_0_X','Dyne:III 3.0.X MUNIR'),
			('gnewsense_2_3','gNewSense 2.3 Deltah'),
			('gnewsense_3_0','gNewSense 3.0 Metad (beta)'),
			('gnewsense_3_0_parkes','gNewSense 3.0 Parkes'),
			('musix_2_0','Musix GNU+Linux 2.0 R0'),
			('parabola','Parabola GNU/Linux'),
			('trisquel_3_5','Trisquel 3.5 Awen'),
			('trisquel_4_0','Trisquel 4.0 Taranis'),
			('trisquel_4_5','Trisquel 4.5 Slaine'),
			('trisquel_5_0','Trisquel 5.0 Dagda'),
			('trisquel_5_5', 'Trisquel 5.5 Brigantia'),
			('trisquel_6_0', 'Trisquel 6.0 Toutatis'),
			('ututo_xs_2009','UTUTO XS 2009'),
			('ututo_xs_2010','UTUTO XS 2010'),
			('ututo_xs_2012_04','UTUTO XS 2012.04'),
			('venenux_0_8','VENENUX 0.8'),
			('venenux_0_8_2','VENENUX-EC 0.8.2'),
			('venenux_0_9','VENENUX 0.9'))
		)

		# TODO: document how it's organized or use another class for it.

		#: A structure representing devices found on the system.
		self.devices = {}

	def distroIsAllowed(self,distroCode):
		"""Check if a distro code is allowed or not."""
		return distroCode in self.allowedDistros

	# TODO: probably storing whole URL would be more useful (could
	# support e.g. servers using HTTPS or a non-root path).
	@property
	def node(self):
		"""Domain of the h-source server used, followed by a slash."""
		return self._request.domain

	@node.setter
	def node(self, domain):
		"""Change server."""
		self._request.domain = domain

	def login(self, username, password):
		"""Log in."""
		post = {'username': username, 'password': password}
		result, content = self._request.perform('users/login/en', post)
		if result:
			if self.isLogged():
				return True
			else:
				self.errors.append("wrong username or password")
		else:
			self.errors.append("unable to connect to server")

		return False

	def logout(self):
		"""Log out."""
		result, content = self._request.perform('users/logout/en')

		if result:
			return True
		else:
			self.errors.append("unable to connect to server")
			return False

	def getUserInfo(self):
		"""Return info about the user logged."""
		result, content = self._request.perform('client/userinfo/en')

		if result:
			try:
				xmldoc = minidom.parseString(content)
				status = xmldoc.getElementsByTagName("status")[0].childNodes[0].data

				username = ''
				token = ''
				groups = ''

				if status == 'logged':
					username = xmldoc.getElementsByTagName("username")[0].childNodes[0].data
					token = xmldoc.getElementsByTagName("token")[0].childNodes[0].data
					groups = xmldoc.getElementsByTagName("groups")[0].childNodes[0].data

				return {'status':status,'username':username,'token':token,'groups':groups}
			except:
				self.errors.append("the server is not up-to-date: unable to parse the xml database")
				return False
		else:
			self.errors.append("unable to connect to server")
			return False

	def isLogged(self):
		"""Return True if the user is logged, else return False."""
		info = self.getUserInfo()

		if info != False:
			if info['status'] == 'logged':
				return True

		return False

	def getLicenseNotice(self):
		"""Return the license info."""
		result, content = self._request.perform("client/licenseinfo/en");
		if result:
			try:
				xmldoc = minidom.parseString(content)
				notice = xmldoc.getElementsByTagName("license_info")[0].childNodes[0].data.encode('utf-8')
				return notice
			except:
				self.errors.append("unable to connect to server")
				return False
		else:
			self.errors.append("unable to connect to server")
			return False

	def createDevices(self):
		self.devices = machine.createDevices()

	def changeType(self,deviceCode,nType):
		"""Change the type of a device.

		deviceCode: the code of the device
		nType: the new type of the device
		"""
		dev_type = devices.get_device_type_for_type(nType)

		if dev_type is not None:
			Class = devices.get_class_for_type(nType)
			self.devices[deviceCode][0].type = dev_type
			self.devices[deviceCode][1] = Class

	def sync(self):
		"""Synchronize with the XML database."""
		result, content = self._request.perform('download/all/en')
		if not result:
			self.errors.append("unable to connect to server")
			return False
		# Get data of devices on the server into a dictionary.
		xml_devices = get_devices_from_xml(content)
		# Check local devices if they are on the server.
		for key, dev in self.devices.iteritems():
			# Reset the device parameters.
			dev[2] = 'insert'
			dev[3] = '0'

			try:
				device = xml_devices[key]
			except KeyError:
				continue  # device not found on the server

			interface = device.getElementsByTagName("interface")[0].childNodes[0].data
			device_type = device.getElementsByTagName("type")[0].childNodes[0].data

			if device_type != dev[0].type.type_name:
				self.changeType(key, device_type)

			model_name = device.getElementsByTagName("model_name")[0].childNodes[0].data
			distribution = device.getElementsByTagName("distribution")[0].childNodes[0].data
			device_id = device.getElementsByTagName("id")[0].childNodes[0].data

			if device_type in ("printer", "scanner"):
				works = device.getElementsByTagName("compatibility")[0].childNodes[0].data

				if device_type == "printer":
					# Set the subtype.
					subtype = device.getElementsByTagName("subtype")[0].childNodes[0].data
					dev[0].setSubtype(subtype)

					try:
						it_tracks_users = device.getElementsByTagName("it_tracks_users")[0].childNodes[0].data
						dev[0].setItTracksUsers(it_tracks_users)
					except:
						pass
			else:
				works = device.getElementsByTagName("it_works")[0].childNodes[0].data

			year = device.getElementsByTagName("year")[0].childNodes[0].data

			if device.getElementsByTagName("other_names")[0].hasChildNodes():
				other_names = device.getElementsByTagName("other_names")[0].childNodes[0].data

				dev[0].addOtherNames(other_names)
			else:
				dev[0].setOtherNames([])

			if device.getElementsByTagName("description")[0].hasChildNodes():
				description = device.getElementsByTagName("description")[0].childNodes[0].data
				dev[0].setDescription(htmlentitiesDecode(description))
			else:
				dev[0].setDescription("")

			if device.getElementsByTagName("kernel_libre")[0].hasChildNodes():
				kernel_libre = device.getElementsByTagName("kernel_libre")[0].childNodes[0].data
				dev[0].kernel = kernel_libre

			if device.getElementsByTagName("driver")[0].hasChildNodes():
				driver = device.getElementsByTagName("driver")[0].childNodes[0].data
				dev[0].setDriver(driver)
			else:
				dev[0].setDriver("")

			dev[0].setModel(model_name)
			dev[0].interface = dev[0].type.interfaces_post.index(interface)
			dev[0].setDistributions([])
			dev[0].addDistributions(distribution)
			dev[0].how_it_works = dev[0].type.how_it_works_post.index(works)
			dev[0].setYear(year)
			dev[2] = "update"
			dev[3] = device_id
		return True

	def submit(self,deviceCode = None):
		for key,dev in self.devices.iteritems():
			if key == deviceCode or deviceCode == None:
				post = dev[0].data

				#get the node controller
				controller = dev[0].type.controller

				#get the user info
				info = self.getUserInfo()
				token = info['token']

				post['from_client'] = 'yes'
				
				if dev[2] == 'update':

					post['id_hard'] = dev[3]
					post['updateAction'] = 'update'
					url = controller + '/update/en/' + token

				elif dev[2] == 'insert':

					post['insertAction'] = 'insert'
					url = controller + '/insert/en/' + token

				result, content = self._request.perform(url.encode('utf-8'), post)

				try:
					#parse the response
					xmldoc = minidom.parseString(content)
					response = xmldoc.getElementsByTagName("status")[0].childNodes[0].data.encode('utf-8')

					notice = xmldoc.getElementsByTagName("notice")[0].childNodes[1].data.encode('utf-8')
					self.errors.append(notice.lstrip().rstrip())

					if response == 'executed':
						return True
					else:
						return False
				except:
					self.errors.append("wrong request")
					return False

# Local Variables:
# indent-tabs-mode: t
# python-guess-indent: nil
# python-indent: 4
# tab-width: 4
# End:
