import { findWrapping, liftTarget, canSplit, ReplaceAroundStep } from "prosemirror-transform";
import { Slice, Fragment, NodeRange } from "prosemirror-model";

// :: (NodeType, ?Object) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// returns a command function that wraps the selection in a list with
// the given type an attributes. If `dispatch` is null, only return a
// value to indicate whether this is possible, but don't actually
// perform the change.
export function wrapInList(listType, attrs?) {
    return (state, dispatch) => {
        const { $from, $to } = state.selection;
        let range = $from.blockRange($to), doJoin = false, outerRange = range;

        if (!range) {
            return false;
        }

        // this is at the top of an existing list item
        if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex === 0) {

            // don't do anything if this is the top of the list
            if ($from.index(range.depth - 1) === 0) {
                return false;
            }

            const $insert = state.doc.resolve(range.start - 2);
            outerRange = new NodeRange($insert, $insert, range.depth);

            if (range.endIndex < range.parent.childCount) {
                range = new NodeRange($from, state.doc.resolve($to.end(range.depth)), range.depth);
            }
            doJoin = true;
        }

        const wrap = findWrapping(outerRange, listType, attrs, range);

        if (!wrap) {
            return false;
        }

        if (dispatch) {
            dispatch(doWrapInList(state.tr, range, wrap, doJoin, listType).scrollIntoView());
        }

        return true;
    };
}

function doWrapInList(tr, range, wrappers, joinBefore, listType) {
    let content = Fragment.empty;
    for (let i = wrappers.length - 1; i >= 0; i--) {
        content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));
    }

    tr.step(new ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end,
        new Slice(content, 0, 0), wrappers.length, true));

    let found = 0;
    for (let i = 0; i < wrappers.length; i++) { if (wrappers[i].type === listType) { found = i + 1; } }
    const splitDepth = wrappers.length - found;

    let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0), parent = range.parent;

    for (let i = range.startIndex, e = range.endIndex, first = true; i < e; i++ , first = false) {
        if (!first && canSplit(tr.doc, splitPos, splitDepth)) {
            tr.split(splitPos, splitDepth);
            splitPos += 2 * splitDepth;
        }
        splitPos += parent.child(i).nodeSize;
    }
    return tr;
}

// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// build a command that splits a non-empty textblock at the top level
// of a list item by also splitting that list item.
export function splitListItem(itemType) {
    return (state, dispatch) => {
        const { $from, $to, node } = state.selection;
        if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) { return false; }
        const grandParent = $from.node(-1);
        if (grandParent.type !== itemType) { return false; }
        if ($from.parent.content.size === 0) {
            // in an empty block. If this is a nested list, the wrapping
            // list item should be split. Otherwise, bail out and let next
            // command handle lifting.
            if ($from.depth === 2 || $from.node(-3).type !== itemType ||
                $from.index(-2) !== $from.node(-2).childCount - 1) { return false; }
            if (dispatch) {
                let wrap = Fragment.empty, keepItem = $from.index(-1) > 0;
                // build a fragment containing empty versions of the structure
                // from the outer list item to the parent node of the cursor
                for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d--) {
                    wrap = Fragment.from($from.node(d).copy(wrap));
                }
                // add a second list item with an empty default start node
                wrap = wrap.append(Fragment.from(itemType.createAndFill()));
                const tr = state.tr.replace($from.before(keepItem ? null : -1), $from.after(-3), new Slice(wrap, keepItem ? 3 : 2, 2));
                tr.setSelection(state.selection.constructor.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))));
                dispatch(tr.scrollIntoView());
            }
            return true;
        }
        const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null;
        const tr = state.tr.delete($from.pos, $to.pos);
        const types = nextType && [null, { type: nextType }];
        if (!canSplit(tr.doc, $from.pos, 2, types)) { return false; }
        if (dispatch) { dispatch(tr.split($from.pos, 2, types).scrollIntoView()); }
        return true;
    };
}

// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// create a command to lift the list item around the selection up into
// a wrapping list.
export function liftListItem(itemType) {
    return (state, dispatch) => {
        const { $from, $to } = state.selection;
        const range = $from.blockRange($to, node => node.childCount && node.firstChild.type === itemType);
        if (!range) { return false; }
        if (!dispatch) { return true; }
        if ($from.node(range.depth - 1).type === itemType) { // inside a parent list
            return liftToOuterList(state, dispatch, itemType, range);
        } else { // outer list node
            return liftOutOfList(state, dispatch, range);
        }
    };
}

function liftToOuterList(state, dispatch, itemType, range) {
    const tr = state.tr, end = range.end, endOfList = range.$to.end(range.depth);
    if (end < endOfList) {
        // there are siblings after the lifted items, which must become
        // children of the last item
        tr.step(new ReplaceAroundStep(end - 1, endOfList, end, endOfList,
            new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
        range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
    }
    dispatch(tr.lift(range, liftTarget(range)).scrollIntoView());
    return true;
}

function liftOutOfList(state, dispatch, range) {
    const tr = state.tr, list = range.parent;
    // merge the list items into a single big item
    for (let pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
        pos -= list.child(i).nodeSize;
        tr.delete(pos - 1, pos + 1);
    }
    const $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
    const atStart = range.startIndex === 0, atEnd = range.endIndex === list.childCount;
    const parent = $start.node(-1), indexBefore = $start.index(-1);
    if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
        item.content.append(atEnd ? Fragment.empty : Fragment.from(list)))) {
        return false;
    }
    const start = $start.pos, end = start + item.nodeSize;
    // strip off the surrounding list. At the sides where we're not at
    // the end of the list, the existing list is closed. At sides where
    // this is the end, it is overwritten to its end.
    tr.step(new ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
        new Slice((atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
            .append(atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))),
            atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
    dispatch(tr.scrollIntoView());
    return true;
}

// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
// create a command to sink the list item around the selection down
// into an inner list.
export function sinkListItem(itemType) {
    return function (state, dispatch) {
        const { $from, $to } = state.selection;
        const range = $from.blockRange($to, node => node.childCount && node.firstChild.type === itemType);
        if (!range) { return false; }
        const startIndex = range.startIndex;
        if (startIndex === 0) { return false; }
        const parent = range.parent, nodeBefore = parent.child(startIndex - 1);
        if (nodeBefore.type !== itemType) { return false; }

        if (dispatch) {
            const nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type === parent.type;
            const inner = Fragment.from(nestedBefore ? itemType.create() : null);
            const slice = new Slice(Fragment.from(itemType.create(null, Fragment.from(parent.copy(inner)))),
                nestedBefore ? 3 : 1, 0);
            const before = range.start, after = range.end;
            dispatch(state.tr.step(new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
                before, after, slice, 1, true))
                .scrollIntoView());
        }
        return true;
    };
}