from __future__ import absolute_import

import ast
import re
import operator as op

from ..exceptions import CloudflareSolveError
from . import JavaScriptInterpreter

# ------------------------------------------------------------------------------- #

_OP_MAP = {
    ast.Add: op.add,
    ast.Sub: op.sub,
    ast.Mult: op.mul,
    ast.Div: op.truediv,
    ast.Invert: op.neg,
}


def _flatten_nested_expression(expression):
    """Return pyparsing.nestedExpr-compatible flat tokens for JSFuck math."""
    start = expression.find('(')
    if start < 0:
        return [expression]

    def parse_group(index):
        tokens = []
        chunk = []
        quote = None
        escaped = False

        def flush():
            token = ''.join(chunk).strip()
            if token:
                tokens.append(token)
            del chunk[:]

        while index < len(expression):
            char = expression[index]
            if quote:
                chunk.append(char)
                if escaped:
                    escaped = False
                elif char == '\\':
                    escaped = True
                elif char == quote:
                    quote = None
                    flush()
                index += 1
                continue
            if char in ('"', "'"):
                flush()
                quote = char
                chunk.append(char)
            elif char == '(':
                flush()
                nested, index = parse_group(index + 1)
                tokens.extend(nested)
                continue
            elif char == ')':
                flush()
                return tokens, index + 1
            elif char.isspace():
                flush()
            else:
                chunk.append(char)
            index += 1
        flush()
        return tokens, index

    tokens, _ = parse_group(start + 1)
    return tokens

# ------------------------------------------------------------------------------- #


class Calc(ast.NodeVisitor):

    def visit_BinOp(self, node):
        return _OP_MAP[type(node.op)](self.visit(node.left), self.visit(node.right))

    # ------------------------------------------------------------------------------- #

    def visit_Num(self, node):
        return node.n

    # ------------------------------------------------------------------------------- #

    def visit_Constant(self, node):
        # Python 3.12+ no longer emits ast.Num for numeric literals (deprecated
        # since 3.8, removed from the parser output) - ast.Constant covers both.
        return node.value

    # ------------------------------------------------------------------------------- #

    def visit_Expr(self, node):
        return self.visit(node.value)

    # ------------------------------------------------------------------------------- #

    @classmethod
    def doMath(cls, expression):
        tree = ast.parse(expression)
        calc = cls()
        return calc.visit(tree.body[0])

# ------------------------------------------------------------------------------- #


class Parentheses(object):

    def fix(self, s):
        res = []
        self.visited = set([s])
        self.dfs(s, self.invalid(s), res)
        return res

    # ------------------------------------------------------------------------------- #

    def dfs(self, s, n, res):
        if n == 0:
            res.append(s)
            return
        for i in range(len(s)):
            if s[i] in ['(', ')']:
                s_new = s[:i] + s[i + 1:]
                if s_new not in self.visited and self.invalid(s_new) < n:
                    self.visited.add(s_new)
                    self.dfs(s_new, self.invalid(s_new), res)

    # ------------------------------------------------------------------------------- #

    def invalid(self, s):
        plus = minus = 0
        memo = {"(": 1, ")": -1}
        for c in s:
            plus += memo.get(c, 0)
            minus += 1 if plus < 0 else 0
            plus = max(0, plus)
        return plus + minus

# ------------------------------------------------------------------------------- #


class ChallengeInterpreter(JavaScriptInterpreter):

    def __init__(self):
        super(ChallengeInterpreter, self).__init__('native')

    # ------------------------------------------------------------------------------- #

    def eval(self, body, domain):

        operators = {
            '+': op.add,
            '-': op.sub,
            '*': op.mul,
            '/': op.truediv
        }

        # ------------------------------------------------------------------------------- #

        def jsfuckToNumber(jsFuck):
            # "Clean Up" JSFuck
            jsFuck = jsFuck.replace('!+[]', '1').replace('!![]', '1').replace('[]', '0')
            jsFuck = jsFuck.lstrip('+').replace('(+', '(').replace(' ', '')
            jsFuck = Parentheses().fix(jsFuck)[0]

            # Hackery Parser for Math
            stack = []
            bstack = []

            for i in _flatten_nested_expression(jsFuck):
                if i == '+':
                    stack.append(bstack)
                    bstack = []
                    continue
                bstack.append(i)
            stack.append(bstack)

            return int(''.join([str(Calc.doMath(''.join(i))) for i in stack]))

        # ------------------------------------------------------------------------------- #

        def divisorMath(payload, needle, domain):
            jsfuckMath = payload.split('/')
            if needle in jsfuckMath[1]:
                expression = re.findall(r"^(.*?)(.)\(function", jsfuckMath[1])[0]

                expression_value = operators[expression[1]](
                    float(jsfuckToNumber(expression[0])),
                    float(ord(domain[jsfuckToNumber(jsfuckMath[1][
                        jsfuckMath[1].find('"("+p+")")}') + len('"("+p+")")}'):-2
                    ])]))
                )
            else:
                expression_value = jsfuckToNumber(jsfuckMath[1])

            expression_value = jsfuckToNumber(jsfuckMath[0]) / float(expression_value)

            return expression_value

        # ------------------------------------------------------------------------------- #

        def challengeSolve(body, domain):
            jschl_answer = 0

            try:
                jsfuckChallenge = re.search(
                    r"setTimeout\(function\(\){\s+var.*?f,\s*(?P<variable>\w+).*?:(?P<init>\S+)};"
                    r".*?\('challenge-form'\);.*?;(?P<challenge>.*?a\.value)\s*=\s*\S+\.toFixed\(10\);",
                    body,
                    re.DOTALL | re.MULTILINE
                ).groupdict()
            except AttributeError:
                raise CloudflareSolveError('There was an issue extracting "jsfuckChallenge" from the Cloudflare challenge.')

            kJSFUCK = re.search(r'(;|)\s*k.=(?P<kJSFUCK>\S+);', jsfuckChallenge['challenge'], re.S | re.M)
            if kJSFUCK:
                try:
                    kJSFUCK = jsfuckToNumber(kJSFUCK.group('kJSFUCK'))
                except IndexError:
                    raise CloudflareSolveError('There was an issue extracting "kJSFUCK" from the Cloudflare challenge.')

                try:
                    kID = re.search(r"\s*k\s*=\s*'(?P<kID>\S+)';", body).group('kID')
                except IndexError:
                    raise CloudflareSolveError('There was an issue extracting "kID" from the Cloudflare challenge.')

                try:
                    r = re.compile(r'<div id="{}(?P<id>\d+)">\s*(?P<jsfuck>[^<>]*)</div>'.format(kID))

                    kValues = {}
                    for m in r.finditer(body):
                        kValues[int(m.group('id'))] = m.group('jsfuck')

                    jsfuckChallenge['k'] = kValues[kJSFUCK]
                except (AttributeError, IndexError):
                    raise CloudflareSolveError('There was an issue extracting "kValues" from the Cloudflare challenge.')

            jsfuckChallenge['challenge'] = re.finditer(
                r'{}.*?([+\-*/])=(.*?);(?=a\.value|{})'.format(
                    jsfuckChallenge['variable'],
                    jsfuckChallenge['variable']
                ),
                jsfuckChallenge['challenge']
            )

            # ------------------------------------------------------------------------------- #

            if '/' in jsfuckChallenge['init']:
                val = jsfuckChallenge['init'].split('/')
                jschl_answer = jsfuckToNumber(val[0]) / float(jsfuckToNumber(val[1]))
            else:
                jschl_answer = jsfuckToNumber(jsfuckChallenge['init'])

            # ------------------------------------------------------------------------------- #

            for expressionMatch in jsfuckChallenge['challenge']:
                oper, expression = expressionMatch.groups()

                if '/' in expression:
                    expression_value = divisorMath(expression, 'function(p)', domain)
                else:
                    if 'Element' in expression:
                        expression_value = divisorMath(jsfuckChallenge['k'], '"("+p+")")}', domain)
                    else:
                        expression_value = jsfuckToNumber(expression)

                jschl_answer = operators[oper](jschl_answer, expression_value)

            # ------------------------------------------------------------------------------- #

            # if not jsfuckChallenge['k'] and '+ t.length' in body:
            #    jschl_answer += len(domain)

            # ------------------------------------------------------------------------------- #

            return '{0:.10f}'.format(jschl_answer)

        # ------------------------------------------------------------------------------- #

        return challengeSolve(body, domain)

# ------------------------------------------------------------------------------- #


ChallengeInterpreter()
