import skema.generators as gens
from skema.parser import parse
from jsonschema import validate
import json
from typing import List
from hypothesis import given, settings
from random import randint
from funcy import omit
from hypothesis import strategies as hs
import json
from .regex import regex


def gen_int(prop):
    min_value = prop.get("minimum", 0)
    max_value = (
        prop.get("maximum")
        or prop.get("exclusiveMaximum")
        or (20 if not min_value else None)
    )
    return hs.integers(min_value=min_value, max_value=max_value)


def gen_string(prop):
    min_value = prop.get("minLength", 5)
    max_value = prop.get("maxLength", 20)
    # pattern = prop.get('pattern', r'[ A-Za-z\d]+')
    # return regex(pattern, )\
    #         .filter(lambda x: min_value <= len(x) and len(x) <= max_value)\
    #         .map(lambda x: x.rstrip('\x00'))
    return (
        hs.text(
            hs.characters(min_codepoint=97, max_codepoint=122),
            min_size=min_value,
            max_size=max_value
            # blacklist_categories=('Cc', 'Cs')),
        )
        .map(lambda s: s.strip())
        .filter(lambda s: len(s) > min_value)
    )


def should_include(key, required_list):
    if key in required_list:
        return True
    else:
        return bool(randint(0, 1))


def gen_array(prop, customs):
    min_items = prop.get("minItems", 2)
    max_items = prop.get("maxItems", 5)
    if prop.get("items", {}):
        generator = get_generator(prop.get("items"), customs)
        return hs.lists(elements=generator, min_size=min_items, max_size=max_items)
    else:
        return hs.lists(elements=gen_anything(), min_size=min_items, max_size=max_items)


def gen_anything():
    return hs.one_of(gen_int({}), gen_string({}), hs.booleans(), hs.none())


def gen_json_values():
    return (
        gen_string({})
        | hs.booleans()
        | gen_int({})
        | hs.none()
        | hs.floats(allow_nan=False, allow_infinity=False)
    )


def gen_any_obj():
    return hs.recursive(
        hs.dictionaries(gen_string({}), gen_json_values()),
        lambda children: hs.dictionaries(gen_string({}), children),
        max_leaves=10,
    )


def gen_object(prop, customs):
    required = prop.get("required", [])
    output = {}
    prop_key = "properties"
    if prop.get("properties", None) is None:
        if prop["additionalProperties"]:
            return gen_any_obj()

    for k in prop[prop_key].keys():
        json_prop = prop[prop_key][k]

        if should_include(k, required):
            output[k] = get_generator(json_prop, customs)

    return hs.fixed_dictionaries(output)


def gen_enum(prop):
    enum = prop["enum"]
    return hs.sampled_from(enum)


def gen_const(prop):
    const = prop["const"]
    return hs.just(const)


def gen_bool(prop):
    return hs.booleans()


def gen_one_of(prop, customs):
    possible_values = []
    for value in prop["oneOf"]:
        possible_values.append(get_generator(value, customs))

    return hs.one_of(possible_values)


def gen_any_of(prop, customs):
    possible_values = []
    for value in prop["anyOf"]:
        possible_values.append(get_generator(value, customs))

    return hs.one_of(possible_values)


specials = ["allOf", "oneOf", "anyOf"]


def merge_all_of(prop,):
    dicts = prop["allOf"]

    if not all(
        [any([x in d for x in specials]) or d.get("type") == "object" for d in dicts]
    ):
        raise Exception(
            f"allOf is supported as array of object types: {json.dumps(dicts, indent=4, default=repr)}"
        )
    result = {"additionalProperties": True, "required": [], "properties": {}}
    for obj in dicts:
        if "allOf" in obj:
            obj = merge_all_of(obj)

        result["additionalProperties"] = result["additionalProperties"] and bool(
            obj.get("additionalProperties", True)
        )
        result["properties"] = {
            **result["properties"],
            **(obj.get("properties", {}) or {}),
        }
        result["required"] = result["required"] + (obj.get("required", []) or [])
    result["required"] = list(set(result["required"]))
    return result


def gen_all_of(prop, customs):
    result = merge_all_of(prop)
    return gen_object(result, customs)


def get_generator(prop, customs={}):
    disp = {
        "string": gen_string,
        "integer": gen_int,
        "number": gen_int,
        "boolean": gen_bool,
    }
    if prop.get("title") != None:
        title = prop.get("title", "")  #  .strip()
        if title in customs:
            return hs.builds(lambda x: customs[title](), hs.integers())
    if not omit(prop, ["title", "description"]):
        return gen_anything()

    enum = prop.get("enum", None)
    if enum is not None:
        return gen_enum(prop)

    if "const" in prop:
        return gen_const(prop)

    one_of = prop.get("oneOf", None)
    if one_of is not None:
        return gen_one_of(prop, customs)

    any_of = prop.get("anyOf", None)
    if any_of is not None:
        return gen_any_of(prop, customs)

    all_of = prop.get("allOf", None)
    if all_of is not None:
        return gen_all_of(prop, customs)

    if "object" == prop.get("type"):
        return gen_object(prop, customs)

    if "array" == prop.get("type"):
        return gen_array(prop, customs)

    json_type = prop.get("type", None)
    if json_type is None:
        raise Exception("Couldnt find type in prop {0}".format(prop))

    return disp[json_type](prop)


def fake_data(
    schema: str, ref=None, amount=5, resolvers={}, from_json=False
) -> List[dict]:
    if from_json:
        json_schema = schema
    else:
        json_schema = gens.jsonschema(parse(schema), ref=ref, resolve=True)
    examples = []

    @given(get_generator(json_schema, resolvers))
    @settings(max_examples=amount)
    def generate(example_data):
        examples.append(example_data)

    generate()
    return examples

