__author__ = 'rohe0002'

import json
import logging
import six

from urlparse import urlparse
from bs4 import BeautifulSoup

from mechanize import ParseResponseEx
from mechanize._form import ControlNotFoundError, AmbiguityError
from mechanize._form import ListControl

logger = logging.getLogger(__name__)

def pick_interaction(interactions, _base="", content="", req=None):
    unic = content
    if content:
        _bs = BeautifulSoup(content)
    else:
        _bs = None

    for interaction in interactions:
        _match = 0
        for attr, val in interaction["matches"].items():
            if attr == "url":
                if val == _base:
                    _match += 1
            elif attr == "title":
                if _bs is None:
                    break
                if _bs.title is None:
                    break
                if val in _bs.title.contents:
                    _match += 1
                else:
                    _c = _bs.title.contents
                    if isinstance(_c, list) and not isinstance(
                      _c, six.string_types):
                        for _line in _c:
                            if val in _line:
                                _match += 1
                                continue
            elif attr == "content":
                if unic and val in unic:
                    _match += 1
            elif attr == "class":
                if req and val == req:
                    _match += 1

        if _match == len(interaction["matches"]):
            return interaction

    raise KeyError("No interaction matched")


class FlowException(Exception):
    def __init__(self, function="", content="", url=""):
        Exception.__init__(self)
        self.function = function
        self.content = content
        self.url = url

    def __str__(self):
        return json.dumps(self.__dict__)


class RResponse():
    """
    A Response class that behaves in the way that mechanize expects it.
    Links to a requests.Response
    """
    def __init__(self, resp):
        self._resp = resp
        self.index = 0
        self.text = resp.text
        if isinstance(self.text, unicode):
            if resp.encoding == "UTF-8":
                self.text = self.text.encode("utf-8")
            else:
                self.text = self.text.encode("latin-1")
        self._len = len(self.text)
        self.url = str(resp.url)
        self.statuscode = resp.status_code

    def geturl(self):
        return self._resp.url

    def __getitem__(self, item):
        try:
            return getattr(self._resp, item)
        except AttributeError:
            return getattr(self._resp.headers, item)

    def __getattribute__(self, item):
        try:
            return getattr(self._resp, item)
        except AttributeError:
            return getattr(self._resp.headers, item)

    def read(self, size=0):
        """
        Read from the content of the response. The class remembers what has
        been read so it's possible to read small consecutive parts of the
        content.

        :param size: The number of bytes to read
        :return: Somewhere between zero and 'size' number of bytes depending
            on how much it left in the content buffer to read.
        """
        if size:
            if self._len < size:
                return self.text
            else:
                if self._len == self.index:
                    part = None
                elif self._len - self.index < size:
                    part = self.text[self.index:]
                    self.index = self._len
                else:
                    part = self.text[self.index: self.index + size]
                    self.index += size
                return part
        else:
            return self.text


def pick_form(response, url=None, **kwargs):
    """
    Picks which form in a web-page that should be used

    :param response: A HTTP request response. A DResponse instance
    :param content: The HTTP response content
    :param url: The url the request was sent to
    :return: The picked form or None of no form matched the criteria.
    """
    #_txt = response.text

    forms = ParseResponseEx(response)
    if not forms:
        raise FlowException(content=response.text, url=url)

    #if len(forms) == 1:
    #    return forms[0]
    #else:

    _form = None
    # ignore the first form, because I use ParseResponseEx which adds
    # one form at the top of the list
    forms = forms[1:]
    if len(forms) == 1:
        _form = forms[0]
    else:
        if "pick" in kwargs:
            _dict = kwargs["pick"]
            for form in forms:
                if _form:
                    break
                for key, _ava in _dict.items():
                    if key == "form":
                        _keys = form.attrs.keys()
                        for attr, val in _ava.items():
                            if attr in _keys and val == form.attrs[attr]:
                                _form = form
                    elif key == "control":
                        prop = _ava["id"]
                        _default = _ava["value"]
                        try:
                            orig_val = form[prop]
                            if isinstance(orig_val, six.string_types):
                                if orig_val == _default:
                                    _form = form
                            elif _default in orig_val:
                                _form = form
                        except KeyError:
                            pass
                        except ControlNotFoundError:
                            pass
                    elif key == "method":
                        if form.method == _ava:
                            _form = form
                    else:
                        _form = None

                    if not _form:
                        break
        elif "index" in kwargs:
            _form = forms[int(kwargs["index"])]

    return _form


def do_click(httpc, form, **kwargs):
    """
    Emulates the user clicking submit on a form.

    :param httpc: The Client instance
    :param form: The form that should be submitted
    :return: What do_request() returns
    """

    if "click" in kwargs:
        request = None
        _name = kwargs["click"]
        try:
            _ = form.find_control(name=_name)
            request = form.click(name=_name)
        except AmbiguityError:
            # more than one control with that name
            _val = kwargs["set"][_name]
            _nr = 0
            while True:
                try:
                    cntrl = form.find_control(name=_name, nr=_nr)
                    if cntrl.value == _val:
                        request = form.click(name=_name, nr=_nr)
                        break
                    else:
                        _nr += 1
                except ControlNotFoundError:
                    raise Exception("No submit control with the name='%s' and "
                                    "value='%s' could be found" % (_name,
                                                                   _val))
    else:
        request = form.click()

    headers = {}
    for key, val in request.unredirected_hdrs.items():
        headers[key] = val

    url = request._Request__original

    if form.method == "POST":
        return httpc.send(url, "POST", data=request.data, headers=headers)
    else:
        return httpc.send(url, "GET", headers=headers)


def select_form(httpc, orig_response, **kwargs):
    """
    Pick a form on a web page, possibly enter some information and submit
    the form.

    :param httpc: A HTTP client instance
    :param orig_response: The original response (as returned by requests)
    :return: The response do_click() returns
    """
    response = RResponse(orig_response)
    try:
        _url = response.url
    except KeyError:
        _url = kwargs["location"]

    form = pick_form(response, _url, **kwargs)
    #form.backwards_compatible = False
    if not form:
        raise Exception("Can't pick a form !!")

    if "set" in kwargs:
        for key, val in kwargs["set"].items():
            if key.startswith("_"):
                continue
            if "click" in kwargs and kwargs["click"] == key:
                continue

            try:
                form[key] = val
            except ControlNotFoundError:
                pass
            except TypeError:
                cntrl = form.find_control(key)
                if isinstance(cntrl, ListControl):
                    form[key] = [val]
                else:
                    raise

    return do_click(httpc, form, **kwargs)


#noinspection PyUnusedLocal
def chose(httpc, orig_response, path, **kwargs):
    """
    Sends a HTTP GET to a url given by the present url and the given
    relative path.

    :param orig_response: The original response
    :param content: The content of the response
    :param path: The relative path to add to the base URL
    :return: The response do_click() returns
    """


    if not path.startswith("http"):
        try:
            _url = orig_response.url
        except KeyError:
            _url = kwargs["location"]

        part = urlparse(_url)
        url = "%s://%s%s" % (part[0], part[1], path)
    else:
        url = path

    return httpc.send(url, "GET")
    #return resp, ""


def post_form(httpc, orig_response, **kwargs):
    """
    The same as select_form but with no possibility of change the content
    of the form.

    :param httpc: A HTTP Client instance
    :param orig_response: The original response (as returned by requests)
    :param content: The content of the response
    :return: The response do_click() returns
    """
    response = RResponse(orig_response)

    form = pick_form(response, **kwargs)

    return do_click(httpc, form, **kwargs)


def NoneFunc():
    return None


#noinspection PyUnusedLocal
def parse(httpc, orig_response, **kwargs):
    # content is a form from which I get the SAMLResponse
    response = RResponse(orig_response)

    form = pick_form(response, **kwargs)
    #form.backwards_compatible = False
    if not form:
        raise Exception("Can't pick a form !!")

    return {"SAMLResponse": form["SAMLResponse"],
            "RelayState": form["RelayState"]}


#noinspection PyUnusedLocal
def interaction(args):
    _type = args["type"]
    if _type == "form":
        return select_form
    elif _type == "link":
        return chose
    elif _type == "response":
        return parse
    else:
        return NoneFunc

# ========================================================================


class Operation(object):
    def __init__(self, args=None):
        if args:
            self.function = interaction(args)

        self.args = args or {}
        self.request = None

    def update(self, dic):
        self.args.update(dic)

    #noinspection PyUnusedLocal
    def post_op(self, result, conv, args):
        pass

    def __call__(self, httpc, conv, location, response, content,
                 features):
        try:
            _args = self.args.copy()
        except (KeyError, AttributeError):
            _args = {}

        _args["location"] = location
        _args["features"] = features

        logger.info("--> FUNCTION: %s" % self.function.__name__)
        logger.info("--> ARGS: %s" % _args)

        result = self.function(httpc, response, **_args)
        self.post_op(result, conv, _args)
        return result
