"""
Extract client information from http user agent
The module does not try to detect all capabilities of browser in current form (it can easily be extended though).
Tries to
    * be fast
    * very easy to extend
    * reliable enough for practical purposes
    * assist python web apps to detect clients.
"""

__version__ = '1.7.8'


class DetectorsHub(dict):
    _known_types = ['os', 'dist', 'flavor', 'browser']

    def __init__(self, *args, **kw):
        dict.__init__(self, *args, **kw)
        for typ in self._known_types:
            self.setdefault(typ, [])
        self.registerDetectors()

    def register(self, detector):
        if detector.info_type not in self._known_types:
            self[detector.info_type] = [detector]
            self._known_types.insert(detector.order, detector.info_type)
        else:
            self[detector.info_type].append(detector)

    def __iter__(self):
        return iter(self._known_types)

    def registerDetectors(self):
        detectors = [v() for v in globals().values() if DetectorBase in getattr(v, '__mro__', [])]
        for d in detectors:
            if d.can_register:
                self.register(d)


class DetectorBase(object):
    name = ""  # "to perform match in DetectorsHub object"
    info_type = "override me"
    result_key = "override me"
    order = 10  # 0 is highest
    look_for = "string to look for"
    skip_if_found = []  # strings if present stop processin
    can_register = False
    version_markers = [("/", " ")]
    allow_space_in_version = False
    _suggested_detectors = None
    platform = None
    bot = False

    def __init__(self):
        if not self.name:
            self.name = self.__class__.__name__
        self.can_register = (self.__class__.__dict__.get('can_register', True))

    def detect(self, agent, result):
        # -> True/None
        word = self.checkWords(agent)
        if word:
            result[self.info_type] = dict(name=self.name)
            result['bot'] = self.bot
            version = self.getVersion(agent, word)
            if version:
                result[self.info_type]['version'] = version
            if self.platform:
                result['platform'] = {'name': self.platform, 'version': version}
            return True

    def checkWords(self, agent):
        # -> True/None
        for w in self.skip_if_found:
            if w in agent:
                return False
        if isinstance(self.look_for, (tuple, list)):
            for word in self.look_for:
                if word in agent:
                    return word
        elif self.look_for in agent:
            return self.look_for

    def getVersion(self, agent, word):
        """
        => version string /None
        """
        version_markers = self.version_markers if \
            isinstance(self.version_markers[0], (list, tuple)) else [self.version_markers]
        version_part = agent.split(word, 1)[-1]
        for start, end in version_markers:
            if version_part.startswith(start) and end in version_part:
                version = version_part[1:]
                if end:  # end could be empty string
                    version = version.split(end)[0]
                if not self.allow_space_in_version:
                    version = version.split()[0]
                return version


class OS(DetectorBase):
    info_type = "os"
    can_register = False
    version_markers = [";", " "]
    allow_space_in_version = True
    platform = None


class Dist(DetectorBase):
    info_type = "dist"
    can_register = False
    platform = None


class Flavor(DetectorBase):
    info_type = "flavor"
    can_register = False
    platform = None


class Browser(DetectorBase):
    info_type = "browser"
    can_register = False


class Firefox(Browser):
    look_for = "Firefox"
    version_markers = [('/', '')]
    skip_if_found = ["SeaMonkey", "web/snippet"]


class SeaMonkey(Browser):
    look_for = "SeaMonkey"
    version_markers = [('/', '')]


class Konqueror(Browser):
    look_for = "Konqueror"
    version_markers = ["/", ";"]


class OperaMobile(Browser):
    look_for = "Opera Mobi"
    name = "Opera Mobile"

    def getVersion(self, agent, word):
        try:
            look_for = "Version"
            return agent.split(look_for)[1][1:].split(' ')[0]
        except IndexError:
            look_for = "Opera"
            return agent.split(look_for)[1][1:].split(' ')[0]


class Opera(Browser):
    look_for = "Opera"

    def getVersion(self, agent, word):
        try:
            look_for = "Version"
            return agent.split(look_for)[1][1:].split(' ')[0]
        except IndexError:
            look_for = "Opera"
            version = agent.split(look_for)[1][1:].split(' ')[0]
            return version.split('(')[0]


class OperaNew(Browser):
    """
    Opera after version 15
    """
    name = "Opera"
    look_for = "OPR"
    version_markers = [('/', '')]


class Netscape(Browser):
    look_for = "Netscape"
    version_markers = [("/", '')]


class Trident(Browser):
    look_for = "Trident"
    skip_if_found = ["MSIE", "Opera"]
    name = "Microsoft Internet Explorer"
    version_markers = ["/", ";"]
    trident_to_ie_versions = {
        '4.0': '8.0',
        '5.0': '9.0',
        '6.0': '10.0',
        '7.0': '11.0',
    }

    def getVersion(self, agent, word):
        return self.trident_to_ie_versions.get(super(Trident, self).getVersion(agent, word))


class MSIE(Browser):
    look_for = "MSIE"
    skip_if_found = ["Opera"]
    name = "Microsoft Internet Explorer"
    version_markers = [" ", ";"]

class MSEdge(Browser):
    look_for = "Edge"
    skip_if_found = ["MSIE"]
    version_markers = ["/", ""]

class Galeon(Browser):
    look_for = "Galeon"


class WOSBrowser(Browser):
    look_for = "wOSBrowser"

    def getVersion(self, agent, word):
        pass


class Safari(Browser):
    look_for = "Safari"
    skip_if_found = ["Edge"]

    def checkWords(self, agent):
        unless_list = ["Chrome", "OmniWeb", "wOSBrowser", "Android"]
        if self.look_for in agent:
            for word in unless_list:
                if word in agent:
                    return False
            return self.look_for

    def getVersion(self, agent, word):
        if "Version/" in agent:
            return agent.split('Version/')[-1].split(' ')[0].strip()
        if "Safari/" in agent:
            return agent.split('Safari/')[-1].split(' ')[0].strip()
        else:
            return agent.split('Safari ')[-1].split(' ')[0].strip()  # Mobile Safari

class GoogleBot(Browser):
    # https://support.google.com/webmasters/answer/1061943
    look_for = ["Googlebot", "Googlebot-News", "Googlebot-Image",
                "Googlebot-Video", "Googlebot-Mobile", "Mediapartners-Google",
                "Mediapartners", "AdsBot-Google", "web/snippet"]
    bot = True
    version_markers = [('/', ';'), ('/', ' ')]

class GoogleFeedFetcher(Browser):
    look_for = "Feedfetcher-Google"
    bot = True

    def get_version(self, agent):
        pass

class RunscopeRadar(Browser):
    look_for = "runscope-radar"
    bot = True

class GoogleAppEngine(Browser):
    look_for = "AppEngine-Google"
    bot = True

    def get_version(self, agent):
        pass

class GoogleApps(Browser):
    look_for = "GoogleApps script"
    bot = True

    def get_version(self, agent):
        pass

class TwitterBot(Browser):
    look_for = "Twitterbot"
    bot = True

class MJ12Bot(Browser):
    look_for = "MJ12bot"
    bot = True

class YandexBot(Browser):
    # http://help.yandex.com/search/robots/agent.xml
    look_for = "Yandex"
    bot = True

    def getVersion(self, agent, word):
        return agent[agent.index('Yandex'):].split('/')[-1].split(')')[0].strip()

class BingBot(Browser):
    look_for = "bingbot"
    version_markers = ["/", ";"]
    bot = True


class BaiduBot(Browser):
    # http://help.baidu.com/question?prod_en=master&class=1&id=1000973
    look_for = ["Baiduspider", "Baiduspider-image", "Baiduspider-video",
                "Baiduspider-news", "Baiduspider-favo", "Baiduspider-cpro",
                "Baiduspider-ads"]
    bot = True
    version_markers = ('/', ';')


class LinkedInBot(Browser):
    look_for = "LinkedInBot"
    bot = True

class ArchiveDotOrgBot(Browser):
    look_for = "archive.org_bot"
    bot = True

class YoudaoBot(Browser):
    look_for = "YoudaoBot"
    bot = True

class YoudaoBotImage(Browser):
    look_for = "YodaoBot-Image"
    bot = True

class RogerBot(Browser):
    look_for = "rogerbot"
    bot = True

class TweetmemeBot(Browser):
    look_for = "TweetmemeBot"
    bot = True

class WebshotBot(Browser):
    look_for = "WebshotBot"
    bot = True

class SensikaBot(Browser):
    look_for = "SensikaBot"
    bot = True

class YesupBot(Browser):
    look_for = "YesupBot"
    bot = True

class DotBot(Browser):
    look_for = "DotBot"
    bot = True

class PhantomJS(Browser):
    look_for = "Browser/Phantom"
    bot = True

class FacebookExternalHit(Browser):
    look_for = 'facebookexternalhit'
    bot = True


class NokiaOvi(Browser):
    look_for = "S40OviBrowser"

class UCBrowser(Browser):
    look_for = "UCBrowser"

class BrowserNG(Browser):
    look_for = "BrowserNG"

class Dolfin(Browser):
    look_for = 'Dolfin'

class NetFront(Browser):
    look_for = 'NetFront'

class Jasmine(Browser):
    look_for = 'Jasmine'

class Openwave(Browser):
    look_for = 'Openwave'

class UPBrowser(Browser):
    look_for = 'UP.Browser'

class OneBrowser(Browser):
    look_for = 'OneBrowser'

class ObigoInternetBrowser(Browser):
    look_for = 'ObigoInternetBrowser'

class TelecaBrowser(Browser):
    look_for = 'TelecaBrowser'

class MAUI(Browser):
    look_for = 'Browser/MAUI'

    def getVersion(self, agent, word):
        version = agent.split("Release/")[-1][:10]
        return version


class NintendoBrowser(Browser):
    look_for = 'NintendoBrowser'


class AndroidBrowser(Browser):
    look_for = "Android"
    skip_if_found = ['Chrome', 'Windows Phone']

    # http://decadecity.net/blog/2013/11/21/android-browser-versions
    def getVersion(self, agent, word):
        pass


class Linux(OS):
    look_for = 'Linux'
    platform = 'Linux'

    def getVersion(self, agent, word):
        pass


class Blackberry(OS):
    look_for = 'BlackBerry'
    platform = 'BlackBerry'

    def getVersion(self, agent, word):
        pass


class BlackberryPlaybook(Dist):
    look_for = 'PlayBook'
    platform = 'BlackBerry'

    def getVersion(self, agent, word):
        pass


class WindowsPhone(OS):
    name = "Windows Phone"
    platform = 'Windows'
    look_for = ["Windows Phone OS", "Windows Phone"]
    version_markers = [(" ", ";"), (" ", ")")]


class iOS(OS):
    look_for = ('iPhone', 'iPad')
    skip_if_found = ['like iPhone']


class iPhone(Dist):
    look_for = 'iPhone'
    platform = 'iOS'
    skip_if_found = ['like iPhone']

    def getVersion(self, agent, word):
        version_end_chars = [' ']
        if not "iPhone OS" in agent:
            return None
        part = agent.split('iPhone OS')[-1].strip()
        for c in version_end_chars:
            if c in part:
                version = part.split(c)[0]
                return version.replace('_', '.')
        return None


class IPad(Dist):
    look_for = 'iPad;'
    platform = 'iOS'

    def getVersion(self, agent, word):
        version_end_chars = [' ']
        if not "CPU OS " in agent:
            return None
        part = agent.split('CPU OS ')[-1].strip()
        for c in version_end_chars:
            if c in part:
                version = part.split(c)[0]
                return version.replace('_', '.')
        return None


class Macintosh(OS):
    look_for = 'Macintosh'

    def getVersion(self, agent, word):
        pass


class MacOS(Flavor):
    look_for = 'Mac OS'
    platform = 'Mac OS'
    skip_if_found = ['iPhone', 'iPad']

    def getVersion(self, agent, word):
        version_end_chars = [';', ')']
        part = agent.split('Mac OS')[-1].strip()
        for c in version_end_chars:
            if c in part:
                version = part.split(c)[0]
                return version.replace('_', '.')
        return ''


class Windows(Dist):
    look_for = 'Windows'
    platform = 'Windows'


class Windows(OS):
    look_for = 'Windows'
    platform = 'Windows'
    skip_if_found = ["Windows Phone"]
    win_versions = {
                    "NT 10.0": "10",
                    "NT 6.3": "8.1",
                    "NT 6.2": "8",
                    "NT 6.1": "7",
                    "NT 6.0": "Vista",
                    "NT 5.2": "Server 2003 / XP x64",
                    "NT 5.1": "XP",
                    "NT 5.01": "2000 SP1",
                    "NT 5.0": "2000",
                    "98; Win 9x 4.90": "Me"
    }

    def getVersion(self, agent, word):
        v = agent.split('Windows')[-1].split(';')[0].strip()
        if ')' in v:
            v = v.split(')')[0]
        v = self.win_versions.get(v, v)
        return v


class Ubuntu(Dist):
    look_for = 'Ubuntu'
    version_markers = ["/", " "]


class Debian(Dist):
    look_for = 'Debian'
    version_markers = ["/", " "]


class Chrome(Browser):
    look_for = "Chrome"
    version_markers = ["/", " "]
    skip_if_found = ["OPR", "Edge"]

    def getVersion(self, agent, word):
        part = agent.split(word + self.version_markers[0])[-1]
        version = part.split(self.version_markers[1])[0]
        if '+' in version:
            version = part.split('+')[0]
        return version.strip()


class ChromeiOS(Browser):
    look_for = "CriOS"
    version_markers = ["/", " "]


class ChromeOS(OS):
    look_for = "CrOS"
    platform = ' ChromeOS'
    version_markers = [" ", " "]

    def getVersion(self, agent, word):
        version_markers = self.version_markers
        if word + '+' in agent:
            version_markers = ['+', '+']
        return agent.split(word + version_markers[0])[-1].split(version_markers[1])[1].strip()[:-1]


class Android(Dist):
    look_for = 'Android'
    platform = 'Android'
    skip_if_found = ['Windows Phone']

    def getVersion(self, agent, word):
        return agent.split(word)[-1].split(';')[0].strip()


class WebOS(Dist):
    look_for = 'hpwOS'

    def getVersion(self, agent, word):
        return agent.split('hpwOS/')[-1].split(';')[0].strip()


class NokiaS40(OS):
    look_for = 'Series40'
    platform = 'Nokia S40'

    def getVersion(self, agent, word):
        pass


class Symbian(OS):
    look_for = ['Symbian', 'SymbianOS']
    platform = 'Symbian'


class PlayStation(OS):
    look_for = ['PlayStation', 'PLAYSTATION']
    platform = 'PlayStation'
    version_markers = [" ", ")"]


class prefs:  # experimental
    os = dict(
        Linux=dict(dict(browser=[Firefox, Chrome], dist=[Ubuntu, Android])),
        BlackBerry=dict(dist=[BlackberryPlaybook]),
        Macintosh=dict(flavor=[MacOS]),
        Windows=dict(browser=[MSIE, Firefox]),
        ChromeOS=dict(browser=[Chrome]),
        Debian=dict(browser=[Firefox])
    )
    dist = dict(
        Ubuntu=dict(browser=[Firefox]),
        Android=dict(browser=[Safari]),
        IPhone=dict(browser=[Safari]),
        IPad=dict(browser=[Safari]),
    )
    flavor = dict(
        MacOS=dict(browser=[Opera, Chrome, Firefox, MSIE])
    )


detectorshub = DetectorsHub()


def detect(agent, fill_none=False):
    """
    fill_none: if name/version is not detected respective key is still added to the result with value None
    """
    result = dict(platform=dict(name=None, version=None))
    _suggested_detectors = []

    for info_type in detectorshub:
        detectors = _suggested_detectors or detectorshub[info_type]
        for detector in detectors:
            try:
                detector.detect(agent, result)
            except Exception as _err:
                pass

    if fill_none:
        attrs_d = {'name': None, 'version': None}
        for key in ('os', 'browser'):
            if key not in result:
                result[key] = attrs_d
            else:
                for k, v in attrs_d.items():
                    result[k] = v

    return result


def simple_detect(agent):
    """
    -> (os, browser) # tuple of strings
    """
    result = detect(agent)
    os_list = []
    if 'flavor' in result:
        os_list.append(result['flavor']['name'])
    if 'dist' in result:
        os_list.append(result['dist']['name'])
    if 'os' in result:
        os_list.append(result['os']['name'])

    os = os_list and " ".join(os_list) or "Unknown OS"
    os_version = os_list and (result.get('flavor') and result['flavor'].get('version')) or \
        (result.get('dist') and result['dist'].get('version')) or (result.get('os') and result['os'].get('version')) or ""
    browser = 'browser' in result and result['browser'].get('name') or 'Unknown Browser'
    browser_version = 'browser' in result and result['browser'].get('version') or ""
    if browser_version:
        browser = " ".join((browser, browser_version))
    if os_version:
        os = " ".join((os, os_version))
    return os, browser
