# Part of flinks
# (C) Martin Bays 2008
# Released under the terms of the GPLv3

import sys, os
from string import *

import re

from .lynxDump import lynxDump

from .constants import USER_AGENT

def placeInList(ref, list, hint=0):
    """Returns last index for which list[index] < ref, returning -1 if none.
    hint can be an index which the return value is expected to be close to.
    """
    if list == []: return -1

    lowerBound = max(0, hint)
    while lowerBound > 0 and ref < list[lowerBound]:
        lowerBound -= 1

    for i,e in enumerate( list[lowerBound:] ):
        if ref < e:
            return lowerBound + i - 1
    return lowerBound + i

def getItemCapped(list, i):
    if i < 0: i = 0
    if i >= len(list): i = len(list)-1
    return list[i]

STATE_WHITESPACE = 0
STATE_NEWLINE = 1
STATE_WORD = 2
STATE_LINKNUM = 3

class IndexedHypertext:
    def __init__(self, url, lynxArgs=[]):
        self.url = url
        self.dumped, linkUrls, self.lynxErr = lynxDump(url, lynxArgs=lynxArgs)

        self.links = [ {"url": url, "word": ''} for url in linkUrls ]

        self.knownAnchors = {}

        self.words = []

        self.marks = {}

        # index word position of start of each sentence, paragraph, link
        self.sentenceIndex = [0]
        self.paraIndex = [0]
        self.linkIndex = []

        self.parserGenerator = self.getParserGenerator()
        self.parsed = False
        self.atWord = -1
        self.nextWord = 0
        self.atSentence = -1
        self.atPara = -1
        if len(self.linkIndex) and self.linkIndex[0] == 0:
            self.lastLink = 0
        else: self.lastLink = -1

    def getParserGenerator(self):
        class Parser:
            def __init__(parseSelf):
                parseSelf.wordNum = -1
                parseSelf.sentenceEnded = False
                parseSelf.paraEnded = False
                parseSelf.maybeLink = False
            def wordCompleted(parseSelf, word):
                try:
                    if (word[-1] in ".!?" or
                            (word[-1] == '"' and
                                word[-2] in ".!?")):
                                    parseSelf.sentenceEnded = True
                except IndexError:
                    pass

                skipBrackets = 0
                if parseSelf.maybeLink:
                    parseSelf.maybeLink = False
                    def regexp():
                        return ('(.*?)' +
                                '\[\d+\].*?' * skipBrackets +
                                '\[(\d+)\](.*)')
                    while True:
                        m = re.match(regexp(), word)
                        if m:
                            wordstart, linknum, wordend = m.groups()
                            ln = int(linknum)
                            if (ln == len(self.linkIndex)+1 and
                                    ln <= len(self.links) and
                                    wordstart+wordend != ''):
                                self.linkIndex.append(parseSelf.wordNum)
                                word = wordstart+wordend
                                self.links[ln-1]["word"] = word
                            else:
                                skipBrackets += 1
                        else: break

                if parseSelf.sentenceEnded:
                    self.sentenceIndex.append(parseSelf.wordNum+1)
                    parseSelf.sentenceEnded = False
                if parseSelf.paraEnded:
                    self.paraIndex.append(parseSelf.wordNum+1)
                    parseSelf.paraEnded = False

                self.words.append(word)
                return word

            def gen(parseSelf):
                state = STATE_WHITESPACE
                wordacc = ''
                for c in self.dumped:
                    if state in [STATE_WHITESPACE,STATE_NEWLINE]:
                        if c in "\n\r":
                            if state == STATE_NEWLINE:
                                parseSelf.paraEnded = True
                                parseSelf.sentenceEnded = True
                            state = STATE_NEWLINE
                        elif c in whitespace:
                            state = STATE_WHITESPACE
                        else:
                            state = STATE_WORD

                            if wordacc:
                                yield parseSelf.wordCompleted(wordacc)

                            parseSelf.wordNum += 1
                            wordacc = c
                            if c == '[':
                                parseSelf.maybeLink = True

                    elif state == STATE_WORD:
                        if c in whitespace or c == wordacc[-1] == '-':
                            if c in "\n\r": state = STATE_NEWLINE
                            else: state = STATE_WHITESPACE
                        else:
                            wordacc += c
                            if c == '[':
                                parseSelf.maybeLink = True

                yield parseSelf.wordCompleted(wordacc)
                self.parsed = True

        parser = Parser()
        return parser.gen()

    def getWord(self, n):
        if n < 0: return ''
        try:
            return self.words[n]
        except IndexError:
            try:
                for i in range(n - len(self.words) + 1):
                    next(self.parserGenerator)
            except StopIteration:
                return ''
            return self.words[n]
    def getFooIndex(self, index, n):
        if n < 0: return None
        try:
            while len(index) <= n:
                next(self.parserGenerator)
            return index[n]
        except StopIteration:
            return None
    def getSentenceIndex(self, n):
        return self.getFooIndex(self.sentenceIndex, n)
    def getParaIndex(self, n):
        return self.getFooIndex(self.paraIndex, n)
    def getLinkIndex(self, n):
        return self.getFooIndex(self.linkIndex, n)

    def currentLink(self):
        try:
            if self.atWord == self.linkIndex[self.lastLink]:
                return self.lastLink
        except IndexError: pass
        return None

    def readWord(self):
        word = self.getWord(self.nextWord)
        if word == '':
            return None

        self.atWord = self.nextWord
        self.nextWord += 1

        try:
            if self.atWord == self.sentenceIndex[self.atSentence+1]:
                self.atSentence += 1
        except IndexError: pass
        try:
            if self.atWord == self.paraIndex[self.atPara+1]:
                self.atPara += 1
        except IndexError: pass
        try:
            while self.atWord == self.linkIndex[self.lastLink+1]:
                self.lastLink += 1
        except IndexError: pass

        return word

    def currentWord(self):
        if self.atWord == -1:
            return ''
        else:
            return self.getWord(self.atWord)

    def isEmpty(self):
        return self.getWord(0) == ''
    def atEnd(self):
        return self.parsed and self.atWord == len(self.words) - 1

    def seekWord(self, toWord, setMark=False):
        if setMark:
            self.mark('\'')
        self.atWord = toWord
        self.nextWord = toWord
        self.getWord(toWord)
        self.atSentence = placeInList(toWord, self.sentenceIndex)
        self.atPara = placeInList(toWord, self.paraIndex)
        self.lastLink = placeInList(toWord, self.linkIndex)

    def parseAll(self):
        try:
            while True:
                next(self.parserGenerator)
        except StopIteration:
            pass

    def linkOfWord(self, n):
        i = placeInList(n, self.linkIndex)
        if i != -1 and n == self.linkIndex[i]:
            return i
        else:
            return None

    def seekEnd(self):
        self.parseAll()
        self.seekWord(len(self.words)-1, setMark=True)

    def seekWordRelative(self, rel):
        self.seekWord(max(0, self.atWord+rel))

        if self.getWord(self.atWord) == '':
            # gone too far - go back to the end
            # (parsing must have finished, so we can use len(self.words))
            self.seekWord(len(self.words)-1)

    def seekFoo(self, index, n):
        if n < 0: n = 0
        i = self.getFooIndex(index, n)
        if i is not None:
            self.seekWord(i)
        elif index != []:
            self.seekWord(index[-1])
    def seekSentenceRelative(self, rel):
        self.seekFoo(self.sentenceIndex, self.atSentence+rel)
    def seekParaRelative(self, rel):
        self.seekFoo(self.paraIndex, self.atPara+rel)
    def seekLinkRelative(self, rel):
        self.seekFoo(self.linkIndex, self.lastLink+rel)

    def atSentenceEnd(self, n=None):
        if n == None:
            return self.atWord + 1 == self.getSentenceIndex(self.atSentence + 1)
        else:
            i = placeInList(n+1, self.sentenceIndex, self.atSentence)
            return i != -1 and self.sentenceIndex[i] == n+1
    def atParaEnd(self, n=None):
        if n == None:
            return self.atWord + 1 == self.getParaIndex(self.atPara + 1)
        else:
            i = placeInList(n+1, self.paraIndex, self.atPara)
            return i != -1 and self.paraIndex[i] == n+1

    def search(self, pattern, dir=1, matchCase=False, wrap=False):

        patternWords = pattern.split()

        if len(patternWords) == 0:
            return

        if dir == 1:
            initial = self.atWord+1
        else:
            initial = self.atWord-1

        i = initial

        while True:
            if i < 0:
                if wrap:
                    self.parseAll()
                    i = len(self.words) - 1
                else:
                    return False
            matched = 0
            for pi in range(len(patternWords)):
                word = self.getWord(i+pi)
                if word == '' and dir == 1:
                    # reached end
                    if wrap:
                        i = -1
                        break
                    else:
                        return False
                pword = patternWords[pi]
                if matchCase:
                    f = word.find(pword)
                else:
                    f = word.lower().find(pword.lower())
                if (f == -1 or
                        (pi > 0 and f > 0) or
                        (pi == 0 and len(patternWords) > 1 and
                            len(word) - f != len(pword) # doesn't match to end
                            )):
                    break
                else:
                    matched += 1
                if matched == len(patternWords):
                    self.seekWord(i, setMark=True)
                    return True
            if dir == 1:
                i += 1
            else:
                i -= 1
            if wrap and i == initial:
                return False

    def mark(self, char):
        self.marks[char] = self.atWord
    def goMark(self, char):
        if char in self.marks:
            n = self.marks[char]
            self.seekWord(n, setMark=True)
    def findAnchor(self, anchor):
        """findAnchor: return index of a named anchor, or None on failure.

        XXX: the implementation here is rather hacky. It is unlikely to be
        very robust.

        We retrieve a copy of the html, search and replace to insert a marker
        at an appropriate point, then process the result via lynx and search
        for the marker."""

        anchor = anchor.lower()

        try:
            return self.knownAnchors[anchor]
        except KeyError:
            if not hasattr(self, "anchorTempIndexed"):
                from tempfile import mkstemp
                from urllib import URLopener
                class myURLopener(URLopener):
                    version = USER_AGENT

                (_, tempname) = mkstemp()
                try:
                    try:
                        # fetch a copy - note that urllib keeps a cache, so this only
                        # actually retrieves the url the first time.
                        opener = myURLopener()
                        opener.retrieve(self.url, tempname)

                        temphtml = open(tempname).read()

                        def getRep(m):
                            tag = m.group(0)
                            element, attribute, value = m.groups()

                            # don't want to pick up meta elements, since they
                            # are in the header. Of course this is rather
                            # hacky.
                            if element == "meta":
                                return tag

                            return '%s__FLINKS_ANCHOR_%s__' % (tag, value)

                        temphtml = re.sub(
                                '<\s*(\w+)\s[^>]*?(name|id)\s*=\s*"?([\w-]+)"?[^>]*>',
                                getRep, temphtml)
                        open(tempname, 'w').write(temphtml)

                        self.anchorTempIndexed = IndexedHypertext(tempname,
                                lynxArgs=["-force-html"])
                        self.anchorFakeWordCount = 0
                    except IOError:
                        return None
                finally:
                    os.remove(tempname)

            while True:
                if not self.anchorTempIndexed.search('__FLINKS_ANCHOR_'):
                    return None

                m = re.search('__FLINKS_ANCHOR_([\w-]+)__',
                        self.anchorTempIndexed.currentWord())
                if m:
                    name = m.group(1).lower()
                    n = self.anchorTempIndexed.atWord - self.anchorFakeWordCount
                    whole = re.match('^__FLINKS_ANCHOR_([\w-]+)__$',
                        self.anchorTempIndexed.currentWord())
                    if whole:
                        self.anchorFakeWordCount += 1
                    self.knownAnchors[name] = n
                    if name == anchor:
                        return n
                else:
                    continue
