from typing import ClassVar
from .fragment import Fragment


class ReplaceError(ValueError):
    pass


def remove_range(content, from_, to):
    from_index_info = content.find_index(from_)
    index, offset = from_index_info["index"], from_index_info["offset"]
    child = content.maybe_child(index)
    to_index_info = content.find_index(to)
    index_to, offset_to = to_index_info["index"], to_index_info["offset"]
    if offset == from_ or child.is_text:
        if offset_to != to and not content.child(index_to).is_text:
            raise ValueError("removing non-flat range")
        return content.cut(0, from_).append(content.cut(to))
    if index != index_to:
        raise ValueError("removing non-flat range")
    return content.replace_child(
        index,
        child.copy(remove_range(child.content, from_ - offset - 1, to - offset - 1)),
    )


def insert_into(content, dist, insert, parent):
    a = content.find_index(dist)
    index, offset = a["index"], a["offset"]
    child = content.maybe_child(index)
    if offset == dist or child.is_text:
        if parent and not parent.can_replace(index, index, insert):
            return None
        return content.cut(0, dist).append(insert).append(content.cut(dist))
    inner = insert_into(child.content, dist - offset - 1, insert, None)
    if inner:
        return content.replace_child(index, child.copy(inner))
    return None


class Slice:
    empty: ClassVar["Slice"]

    def __init__(self, content, open_start, open_end):
        self.content = content
        self.open_start = open_start
        self.open_end = open_end

    @property
    def size(self):
        return self.content.size - self.open_start - self.open_end

    def insert_at(self, pos, fragment):
        content = insert_into(self.content, pos + self.open_start, fragment, None)
        if content:
            return Slice(content, self.open_start, self.open_end)

    def remove_between(self, from_, to):
        return Slice(
            remove_range(self.content, from_ + self.open_start, to + self.open_start),
            self.open_start,
            self.open_end,
        )

    def eq(self, other):
        return (
            self.content.eq(other.content)
            and self.open_start == other.open_start
            and self.open_end == other.open_end
        )

    def __str__(self):
        return f"{self.content}({self.open_start},{self.open_end})"

    def to_json(self):
        if not self.content.size:
            return None
        json = {"content": self.content.to_json()}
        if self.open_start > 0:
            json["openStart"] = self.open_start
        if self.open_end > 0:
            json["openEnd"] = self.open_end
        return json

    @classmethod
    def from_json(cls, schema, json_data):
        if isinstance(json_data, str):
            import json

            json_data = json.loads(json)
        if not json_data:
            return cls.empty
        open_start = json_data.get("openStart", 0) or 0
        open_end = json_data.get("openEnd", 0) or 0
        if not isinstance(open_start, int) or not isinstance(open_end, int):
            raise ValueError("invalid input for Slice.from_json")
        return cls(
            Fragment.from_json(schema, json_data.get("content")),
            open_start,
            open_end,
        )

    @classmethod
    def max_open(cls, fragment: Fragment, open_isolating=True):
        open_start = 0
        open_end = 0
        n = fragment.first_child
        while n and not n.is_leaf and (open_isolating or n.type.spec.get("isolating")):
            open_start += 1
            n = n.first_child
        n = fragment.last_child
        while n and not n.is_leaf and (open_isolating or n.type.spec.get("isolating")):
            open_end += 1
            n = n.last_child
        return cls(fragment, open_start, open_end)


Slice.empty = Slice(Fragment.empty, 0, 0)


def replace(from_, to, slice):
    if slice.open_start > from_.depth:
        raise ReplaceError("Inserted content deeper than insertion position")
    if from_.depth - slice.open_start != to.depth - slice.open_end:
        raise ReplaceError("Inconsistent open depths")
    return replace_outer(from_, to, slice, 0)


def replace_outer(from_, to, slice: Slice, depth):
    index = from_.index(depth)
    node = from_.node(depth)
    if index == to.index(depth) and depth < from_.depth - slice.open_start:
        inner = replace_outer(from_, to, slice, depth + 1)
        return node.copy(node.content.replace_child(index, inner))
    elif not slice.content.size:
        return close(node, replace_two_way(from_, to, depth))
    elif (
        not slice.open_start
        and not slice.open_end
        and from_.depth == depth
        and to.depth == depth
    ):
        parent = from_.parent
        content = parent.content
        return close(
            parent,
            content.cut(0, from_.parent_offset)
            .append(slice.content)
            .append(content.cut(to.parent_offset)),
        )
    else:
        prepare = prepare_slice_for_replace(slice, from_)
        start, end = prepare["start"], prepare["end"]
        return close(node, replace_three_way(from_, start, end, to, depth))


def check_join(main, sub):
    if not sub.type.compatible_content(main.type):
        raise ReplaceError(f"Cannot join {sub.type.name} onto {main.type.name}")


def joinable(before, after, depth):
    node = before.node(depth)
    check_join(node, after.node(depth))
    return node


def add_node(child, target):
    last = len(target) - 1
    if last >= 0 and child.is_text and child.same_markup(target[last]):
        target[last] = child.with_text(target[last].text + child.text)
    else:
        target.append(child)


def add_range(start, end, depth, target):
    node = (end or start).node(depth)
    start_index = 0
    end_index = end.index(depth) if end else node.child_count
    if start:
        start_index = start.index(depth)
        if start.depth > depth:
            start_index += 1
        elif start.text_offset:
            add_node(start.node_after, target)
            start_index += 1
    i = start_index
    while i < end_index:
        add_node(node.child(i), target)
        i += 1
    if end and end.depth == depth and end.text_offset:
        add_node(end.node_before, target)


def close(node, content):
    if not node.type.valid_content(content):
        raise ReplaceError(f"Invalid content for node {node.type.name}")
    return node.copy(content)


def replace_three_way(from_, start, end, to, depth):
    open_start = joinable(from_, start, depth + 1) if from_.depth > depth else False
    open_end = joinable(end, to, depth + 1) if to.depth > depth else False
    content = []
    add_range(None, from_, depth, content)
    if open_start and open_end and start.index(depth) == end.index(depth):
        check_join(open_start, open_end)
        add_node(
            close(open_start, replace_three_way(from_, start, end, to, depth + 1)),
            content,
        )
    else:
        if open_start:
            add_node(
                close(open_start, replace_two_way(from_, start, depth + 1)), content
            )
        add_range(start, end, depth, content)
        if open_end:
            add_node(close(open_end, replace_two_way(end, to, depth + 1)), content)
    add_range(to, None, depth, content)
    return Fragment(content)


def replace_two_way(from_, to, depth):
    content = []
    add_range(None, from_, depth, content)
    if from_.depth > depth:
        type = joinable(from_, to, depth + 1)
        add_node(close(type, replace_two_way(from_, to, depth + 1)), content)
    add_range(to, None, depth, content)
    return Fragment(content)


def prepare_slice_for_replace(slice: Slice, along):
    extra = along.depth - slice.open_start
    parent = along.node(extra)
    node = parent.copy(slice.content)
    for i in range(extra - 1, -1, -1):
        node = along.node(i).copy(Fragment.from_(node))
    return {
        "start": node.resolve_no_cache(slice.open_start + extra),
        "end": node.resolve_no_cache(node.content.size - slice.open_end - extra),
    }
