import json
import re

from jsonpath_rw import parse as parse_json_path
from jsonpath_rw.lexer import JsonPathLexerError

INLINE_PATH_PATTERN = r"\$\{([^}]*)\}"


class JSONTemplateParseException(Exception):
    """
    Exception raised if a JSON template could not be parsed.
    """


class JSONTemplate(object):
    """
    Represents a parsed template for producing JSON.
    """

    def __init__(self, template_string):
        try:
            self._parsed = json.loads(template_string)
        except ValueError as ve:
            raise JSONTemplateParseException("Could not parse template: %s" % ve)

        # Apply against an empty object to validate.
        self.apply({})

    def apply(self, data):
        return apply_data_to_obj(self._parsed, data)


def apply_data_to_obj(obj, data, missing="(none)"):
    if isinstance(obj, str):
        return _process_string(obj, data, missing)
    elif isinstance(obj, dict):
        return {
            _process_string(key, data, missing): apply_data_to_obj(value, data, missing)
            for key, value in obj.items()
        }
    elif isinstance(obj, list):
        return [apply_data_to_obj(item, data, missing) for item in obj]
    else:
        return obj


def _process_string(str_value, data, missing_filler):
    # Check for a direct match first.
    if re.match("^" + INLINE_PATH_PATTERN + "$", str_value):
        expression = str_value[2:-1]
        return _process_inline(expression, data)

    def process_inline(match):
        result = _process_inline(match.group(1), data)
        if result is None:
            return missing_filler

        if isinstance(result, list) and len(result) > 1:
            return ",".join(result)

        return str(result)

    return re.sub(INLINE_PATH_PATTERN, process_inline, str_value)


def _process_inline(expression, data):
    if not expression:
        raise JSONTemplateParseException("Empty expression found")

    try:
        parsed = parse_json_path(expression)
    except JsonPathLexerError as jple:
        raise JSONTemplateParseException("For expression `%s`: %s" % (expression, jple))
    except Exception as ex:
        raise JSONTemplateParseException("For expression `%s`: %s" % (expression, ex))

    try:
        found = parsed.find(data)
        if not found:
            return None

        if len(found) > 1:
            return [f.data for f in found]

        return found[0].value
    except IndexError:
        return None
