# -*- coding: utf-8 -*-
#
# Copyright 2008-2011 Zuza Software Foundation
# 2013,2015,2016 Friedel Wolff
#
# This file is part of Virtaal.
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, see <http://www.gnu.org/licenses/>.

import logging

import pycurl
from gi.repository import GObject
from six import StringIO
from six.moves.urllib import request, parse

try:
    import libproxy
    proxy_factory = libproxy.ProxyFactory()
except ImportError:
    libproxy = None

from virtaal.common.gobjectwrapper import GObjectWrapper

__all__ = ['HTTPClient', 'HTTPRequest', 'RESTRequest']


class HTTPRequest(GObjectWrapper):
    """Single HTTP request, blocking if used standalone."""

    __gtype_name__ = 'HttpClientRequest'
    __gsignals__ = {
        "http-success": (GObject.SignalFlags.RUN_LAST, None, (object,)),
        "http-redirect": (GObject.SignalFlags.RUN_LAST, None, (object,)),
        "http-client-error": (GObject.SignalFlags.RUN_LAST, None, (object,)),
        "http-server-error": (GObject.SignalFlags.RUN_LAST, None, (object,)),
    }

    def __init__(self, url, method='GET', data=None, headers=None,
            headers_only=False, user_agent=None, follow_location=False,
            force_quiet=True):
        GObjectWrapper.__init__(self)
        self.result = StringIO()
        self.result_headers = StringIO()

        self.url = url
        self.method = method
        self.data = data
        self.headers = headers
        self.status = None

        # the actual curl request object
        self.curl = pycurl.Curl()
        if (logging.root.level == logging.DEBUG and not force_quiet):
            self.curl.setopt(pycurl.VERBOSE, 1)

        self.curl.setopt(pycurl.WRITEFUNCTION, self.result.write)
        self.curl.setopt(pycurl.HEADERFUNCTION, self.result_headers.write)
        # We want to use gzip and deflate if possible:
        self.curl.setopt(pycurl.ENCODING, "") # use all available encodings
        self.curl.setopt(pycurl.URL, self.url)

        # let's set the HTTP request method
        if method == 'GET':
            self.curl.setopt(pycurl.HTTPGET, 1)
        elif method == 'POST':
            self.curl.setopt(pycurl.POST, 1)
        elif method == 'PUT':
            self.curl.setopt(pycurl.UPLOAD, 1)
        else:
            self.curl.setopt(pycurl.CUSTOMREQUEST, method)
        if data:
            if method == "PUT":
                self.data = StringIO.StringIO(data)
                self.curl.setopt(pycurl.READFUNCTION, self.data.read)
                self.curl.setopt(pycurl.INFILESIZE, len(self.data.getvalue()))
            else:
                self.curl.setopt(pycurl.POSTFIELDS, self.data)
                self.curl.setopt(pycurl.POSTFIELDSIZE, len(self.data))
        if headers:
            self.curl.setopt(pycurl.HTTPHEADER, headers)
        if headers_only:
            self.curl.setopt(pycurl.HEADER, 1)
            self.curl.setopt(pycurl.NOBODY, 1)
        if user_agent:
            self.curl.setopt(pycurl.USERAGENT, user_agent)
        if follow_location:
            self.curl.setopt(pycurl.FOLLOWLOCATION, 1)

        if libproxy:
            for proxy in proxy_factory.getProxies(self.url):
                # if we connect to localhost (localtm) with proxy specifically
                # set to direct://, libcurl connects fine, but then asks
                #   GET http://localhost:55555/unit/en/af/whatever
                # instead of
                #   GET /unit/en/af/whatever
                # and it doesn't work. We have to set it specifically to ""
                # though, otherwise it seems to fall back to environment
                # variables.
                if proxy == "direct://":
                    proxy = ""
                self.curl.setopt(pycurl.PROXY, proxy)
                #only use the first one
                break
        else:
            # Proxy: let's be careful to isolate the protocol to ensure that we
            # support the case where http and https might use different proxies
            split_url = self.url.split('://', 1)
            if len(split_url) > 1:
                #We were able to get a protocol
                protocol, address = split_url
                split_result = parse.urlsplit('//' + address)
                host = split_result.hostname
                proxies = request.getproxies()
                if protocol in proxies and not request.proxy_bypass(host):
                    self.curl.setopt(pycurl.PROXY, proxies[protocol])

        # self reference required, because CurlMulti will only return
        # Curl handles
        self.curl.request = self

    def __repr__(self):
        return '<%s:%s>' % (self.method, self.get_effective_url())

    def get_effective_url(self):
        return self.curl.getinfo(pycurl.EFFECTIVE_URL)

    def perform(self):
        """run the request (blocks)"""
        self.curl.perform()

    def handle_result(self):
        """called after http request is done"""
        self.status = self.curl.getinfo(pycurl.HTTP_CODE)

        #TODO: handle 3xx, throw exception on other codes
        if self.status >= 200 and self.status < 300:
            # 2xx indicated success
            self.emit("http-success", self.result.getvalue())
        elif self.status >= 300 and self.status < 400:
            # 3xx redirection
            self.emit("http-redirect", self.result.getvalue())
        elif self.status >= 400 and self.status < 500:
            # 4xx client error
            self.emit("http-client-error", self.status)
        elif self.status >= 500 and self.status < 600:
            # 5xx server error
            self.emit("http-server-error", self.status)


class RESTRequest(HTTPRequest):
    """Single HTTP REST request, blocking if used standalone."""

    def __init__(self, url, id, method='GET', data=None, headers=None, user_agent=None, params=None):
        super(RESTRequest, self).__init__(url, method, data, headers, user_agent=user_agent, follow_location=True)

        url = self.url
        self.id = id
        if id:
            url += '/' + parse.quote(id.encode('utf-8'), safe='')

        if params:
            url += '?' + parse.urlencode(params)

        self.curl.setopt(pycurl.URL, url)


class HTTPClient(object):
    """Non-blocking client that can handle multiple (asynchronous) HTTP requests."""

    def __init__(self):
        # state variable used to add and remove dispatcher to gtk event loop
        self.running = False

        # Since pycurl doesn't keep references to requests, requests
        # get garbage collected before they are done. We need to keep requests in
        # a set and detroy them manually.
        self.requests = set()
        self.curl = pycurl.CurlMulti()
        self.user_agent = None

    def add(self,request):
        """add a request to the queue"""
        # First ensure that we're not piling up on unanswered requests:
        if len(self.requests) > 15:
            return
        self.curl.add_handle(request.curl)
        self.requests.add(request)
        self.run()

    def run(self):
        """client should not be running when request queue is empty"""
        if self.running: return
        GObject.timeout_add(100, self.perform)
        self.running = True

    def close_request(self, handle):
        """finalize a successful request"""
        self.curl.remove_handle(handle)
        handle.request.handle_result()
        self.requests.remove(handle.request)

    def close_failed_request(self, fail_tuple):
        # fail_tuple is (handle, error_code, error_message)
        # see E_* constants in pycurl for error_code
        self.curl.remove_handle(fail_tuple[0])
        self.requests.remove(fail_tuple[0].request)
        logging.debug(fail_tuple[2])

    def perform(self):
        """main event loop function, non blocking execution of all queued requests"""
        ret, num_handles = self.curl.perform()
        if ret != pycurl.E_CALL_MULTI_PERFORM and num_handles == 0:
            self.running = False
        num, completed, failed = self.curl.info_read()
        [self.close_request(com) for com in completed]
        [self.close_failed_request(fail) for fail in failed]
        if not self.running:
            #we are done with this batch what do we do?
            return False
        return True

    def get(self, url, callback, etag=None, error_callback=None):
        headers = None
        if etag:
            # See http://en.wikipedia.org/wiki/HTTP_ETag for more details about ETags
            headers = ['If-None-Match: "%s"' % (etag)]
        request = HTTPRequest(url, headers=headers, user_agent=self.user_agent, follow_location=True)
        self.add(request)

        if callback:
            request.connect('http-success', callback)
            request.connect('http-redirect', callback)
        if error_callback:
            request.connect('http-client-error', error_callback)
            request.connect('http-server-error', error_callback)

    def set_virtaal_useragent(self):
        """Set a nice user agent indicating Virtaal and its version."""
        if self.user_agent and self.user_agent.startswith('Virtaal'):
            return
        import sys
        from virtaal.__version__ import ver as version
        platform = sys.platform
        if platform.startswith('linux'):
            import os
            # All systems supporting systemd:
            if os.path.isfile('/etc/os-release'):
                try:
                    lines = open('/etc/os-release').read().splitlines()
                    distro = None
                    distro_version = None
                    for line in lines:
                        if line.startswith('NAME'):
                            distro = line.split('=')[-1]
                            distro = distro.replace('"', '')
                        if line.startswith('VERSION'):
                            distro_version = line.split('=')[-1]
                            distro_version = distro_version.replace('"', '')
                    platform = '%s; %s %s' % (platform, distro, distro_version)
                except Exception as e:
                    pass

            # Debian, Ubuntu, Mandriva:
            elif os.path.isfile('/etc/lsb-release'):
                try:
                    lines = open('/etc/lsb-release').read().splitlines()
                    for line in lines:
                        if line.startswith('DISTRIB_DESCRIPTION'):
                            distro = line.split('=')[-1]
                            distro = distro.replace('"', '')
                            platform = '%s; %s' % (platform, distro)
                except Exception as e:
                    pass
            # Fedora, RHEL:
            elif os.path.isfile('/etc/system-release'):
                try:
                    lines = open('/etc/system-release').read().splitlines()
                    for line in lines:
                        distro, dummy, distro_version, codename = line.split()
                        platform = '%s; %s %s' % (platform, distro, distro_version)
                except Exception as e:
                    pass
        elif platform.startswith('win'):
            major, minor = sys.getwindowsversion()[:2]
            # from http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
            name_dict = {
                    (5, 0): "Windows 2000",
                    (5, 1): "Windows XP",
                    (6, 0): "Windows Vista", # Also Windows Server 2008
                    (6, 1): "Windows 7",     # Also Windows Server 2008 R2
                    (6, 2): "Windows 8",     # Also Windows Server 2012
                    (6, 3): "Windows 8.1",   # Also Windows Server 2012 R2
                   (10, 0): "Windows 10",
            }
            # (5, 2) includes XP Professional x64 Edition, Server 2003, Home Server, Server 2003 R2
            name = name_dict.get((major, minor), None)
            if name:
                platform = '%s; %s' % (platform, name)
        elif platform.startswith('darwin'):
            import platform as plat
            release, versioninfo, machine = plat.mac_ver()
            platform = "%s; %s %s" % (platform, release, machine)
        self.user_agent = 'Virtaal/%s (%s)' % (version, platform)
