export const org_cycle = (cm) => {
    const pos = cm.getCursor();
    isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos);
};

const state = {
    stab: "CONTENT",
};
export const org_set_fold = (cm) => {
    const cursor = cm.getCursor();
    set_folding_mode(cm, state.stab);
    cm.setCursor(cursor);
    return state.stab;
};
/*
 * DONE: Global visibility cycling
 * TODO: or move to previous table field.
 */
export const org_shifttab = (cm) => {
    if (state.stab === "SHOW_ALL") {
        state.stab = "OVERVIEW";
    } else if (state.stab === "OVERVIEW") {
        state.stab = "CONTENT";
    } else if (state.stab === "CONTENT") {
        state.stab = "SHOW_ALL";
    }
    set_folding_mode(cm, state.stab);
    return state.stab;
};

function set_folding_mode(cm, mode) {
    if (mode === "OVERVIEW") {
        folding_mode_overview(cm);
    } else if (mode === "SHOW_ALL") {
        folding_mode_all(cm);
    } else if (mode === "CONTENT") {
        folding_mode_content(cm);
    }
    cm.refresh();

    function folding_mode_overview(cm) {
        cm.operation(function() {
            for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
                fold(cm, window.CodeMirror.Pos(i, 0));
            }
        });
    }
    function folding_mode_content(cm) {
        cm.operation(function() {
            let previous_header = null;
            for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
                fold(cm, window.CodeMirror.Pos(i, 0));
                if (/header/.test(cm.getTokenTypeAt(window.CodeMirror.Pos(i, 0))) === true) {
                    const level = cm.getLine(i).replace(/^(\*+).*/, "$1").length;
                    if (previous_header && level > previous_header.level) {
                        unfold(cm, window.CodeMirror.Pos(previous_header.line, 0));
                    }
                    previous_header = {
                        line: i,
                        level,
                    };
                }
            }
        });
    }
    function folding_mode_all(cm) {
        cm.operation(function() {
            for (let i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) {
                if (/header/.test(cm.getTokenTypeAt(window.CodeMirror.Pos(i, 0))) === true) {
                    unfold(cm, window.CodeMirror.Pos(i, 0));
                }
            }
        });
    }
}

/*
 * Promote heading or move table column to left.
 */
export const org_metaleft = (cm) => {
    const line = cm.getCursor().line;
    _metaleft(cm, line);
};
function _metaleft(cm, line) {
    let p = null;
    if (p = isTitle(cm, line)) {
        if (p["level"] > 1) {
            cm.replaceRange(
                "",
                { line: p.start, ch: 0 },
                { line: p.start, ch: 1 },
            );
        }
    } else if (p = isItemList(cm, line)) {
        for (let i=p.start; i<=p.end; i++) {
            if (p["level"] > 0) {
                cm.replaceRange(
                    "",
                    { line: i, ch: 0 },
                    { line: i, ch: 2 },
                );
            }
        }
    } else if (p = isNumberedList(cm, line)) {
        for (let i=p.start; i<=p.end; i++) {
            if (p["level"] > 0) {
                cm.replaceRange(
                    "",
                    { line: i, ch: 0 },
                    { line: i, ch: 3 },
                );
            }
        }
        rearrange_list(cm, line);
    }
}

/*
 * Demote a subtree, a list item or move table column to right.
 * In front of a drawer or a block keyword, indent it correctly.
 */
export const org_metaright = (cm) => {
    const line = cm.getCursor().line;
    _metaright(cm, line);
};

function _metaright(cm, line) {
    let p = null;
    let tmp = null;
    if (p = isTitle(cm, line)) {
        cm.replaceRange("*", { line: p.start, ch: 0 });
    } else if (p = isItemList(cm, line)) {
        if (tmp = isItemList(cm, p.start - 1)) {
            if (p.level < tmp.level + 1) {
                for (let i=p.start; i<=p.end; i++) {
                    cm.replaceRange("  ", { line: i, ch: 0 });
                }
            }
        }
    } else if (p = isNumberedList(cm, line)) {
        if (tmp = isNumberedList(cm, p.start - 1)) {
            if (p.level < tmp.level + 1) {
                for (let i=p.start; i<=p.end; i++) {
                    cm.replaceRange("   ", { line: i, ch: 0 });
                }
                rearrange_list(cm, p.start);
            }
        }
    }
}

/*
 * Insert a new heading or wrap a region in a table
 */
export const org_meta_return = (cm) => {
    const line = cm.getCursor().line;
    const content = cm.getLine(line);
    let p = null;

    if (p = isItemList(cm, line)) {
        const level = p.level;
        cm.replaceRange(
            "\n"+" ".repeat(level*2)+"- ",
            { line: p.end, ch: cm.getLine(p.end).length },
        );
        cm.setCursor({ line: p.end+1, ch: level*2+2 });
    } else if (p = isNumberedList(cm, line)) {
        const level = p.level;
        cm.replaceRange(
            "\n"+" ".repeat(level*3)+(p.n+1)+". ",
            { line: p.end, ch: cm.getLine(p.end).length },
        );
        cm.setCursor({ line: p.end+1, ch: level*3+3 });
        rearrange_list(cm, line);
    } else if (p = isTitle(cm, line)) {
        const tmp = previousOfType(cm, "title", line);
        const level = (tmp && tmp.level) || 1;
        cm.replaceRange("\n"+"*".repeat(level)+" ", { line, ch: content.length });
        cm.setCursor({ line: line+1, ch: level+1 });
    } else if (content.trim() === "") {
        cm.replaceRange("* ", { line, ch: 0 });
        cm.setCursor({ line, ch: 2 });
    } else {
        cm.replaceRange("\n\n* ", { line, ch: content.length });
        cm.setCursor({ line: line + 2, ch: 2 });
    }
};

const TODO_CYCLES = ["TODO", "DONE", ""];
/*
 * Cycle the thing at point or in the current line, depending on context.
 * Depending on context, this does one of the following:
 * - TODO: switch a timestamp at point one day into the past
 * - DONE: on a headline, switch to the previous TODO keyword.
 * - TODO: on an item, switch entire list to the previous bullet type
 * - TODO: on a property line, switch to the previous allowed value
 * - TODO: on a clocktable definition line, move time block into the past
 */
export const org_shiftleft = (cm) => {
    const cycles = [].concat(TODO_CYCLES.slice(0).reverse(), TODO_CYCLES.slice(-1));
    const line = cm.getCursor().line;
    const content = cm.getLine(line);
    const params = isTitle(cm, line);

    if (params === null) return;
    params["status"] = cycles[cycles.indexOf(params["status"]) + 1];
    cm.replaceRange(
        makeTitle(params),
        { line, ch: 0 },
        { line, ch: content.length },
    );
};
/*
 * Cycle the thing at point or in the current line, depending on context.
 * Depending on context, this does one of the following:
 * - TODO: switch a timestamp at point one day into the future
 * - DONE: on a headline, switch to the next TODO keyword.
 * - TODO: on an item, switch entire list to the next bullet type
 * - TODO: on a property line, switch to the next allowed value
 * - TODO: on a clocktable definition line, move time block into the future
 */
export const org_shiftright = (cm) => {
    cm.operation(() => {
        const cycles = [].concat(TODO_CYCLES, [TODO_CYCLES[0]]);
        const line = cm.getCursor().line;
        const content = cm.getLine(line);
        const params = isTitle(cm, line);

        if (params === null) return;
        params["status"] = cycles[cycles.indexOf(params["status"]) + 1];
        cm.replaceRange(
            makeTitle(params),
            { line, ch: 0 },
            { line, ch: content.length },
        );
    });
};

export const org_insert_todo_heading = (cm) => {
    cm.operation(() => {
        const line = cm.getCursor().line;
        const content = cm.getLine(line);

        let p = null;
        if (p = isItemList(cm, line)) {
            const level = p.level;
            cm.replaceRange(
                "\n"+" ".repeat(level*2)+"- [ ] ",
                { line: p.end, ch: cm.getLine(p.end).length },
            );
            cm.setCursor({ line: line+1, ch: 6+level*2 });
        } else if (p = isNumberedList(cm, line)) {
            const level = p.level;
            cm.replaceRange(
                "\n"+" ".repeat(level*3)+(p.n+1)+". [ ] ",
                { line: p.end, ch: cm.getLine(p.end).length },
            );
            cm.setCursor({ line: p.end+1, ch: level*3+7 });
            rearrange_list(cm, line);
        } else if (p = isTitle(cm, line)) {
            const level = (p && p.level) || 1;
            cm.replaceRange("\n"+"*".repeat(level)+" TODO ", { line, ch: content.length });
            cm.setCursor({ line: line+1, ch: level+6 });
        } else if (content.trim() === "") {
            cm.replaceRange("* TODO ", { line, ch: 0 });
            cm.setCursor({ line, ch: 7 });
        } else {
            cm.replaceRange("\n\n* TODO ", { line, ch: content.length });
            cm.setCursor({ line: line + 2, ch: 7 });
        }
    });
};

/*
 * Move subtree up or move table row up.
 * Calls ‘org-move-subtree-up’ or ‘org-table-move-row’ or
 * ‘org-move-item-up’, depending on context
 */
export const org_metaup = (cm) => {
    cm.operation(() => {
        const line = cm.getCursor().line;
        let p = null;

        if (p = isItemList(cm, line)) {
            const a = isItemList(cm, p.start - 1);
            if (a) {
                swap(cm, [p.start, p.end], [a.start, a.end]);
                rearrange_list(cm, line);
            }
        } else if (p = isNumberedList(cm, line)) {
            const a = isNumberedList(cm, p.start - 1);
            if (a) {
                swap(cm, [p.start, p.end], [a.start, a.end]);
                rearrange_list(cm, line);
            }
        } else if (p = isTitle(cm, line)) {
            let _line = line;
            let a;
            do {
                _line -= 1;
                if (a = isTitle(cm, _line, p.level)) {
                    break;
                }
            } while (_line > 0);

            if (a) {
                swap(cm, [p.start, p.end], [a.start, a.end]);
                org_set_fold(cm);
            }
        }
    });
};

/*
 * Move subtree down or move table row down.
 * Calls ‘org-move-subtree-down’ or ‘org-table-move-row’ or
 * ‘org-move-item-down’, depending on context
 */
export const org_metadown = (cm) => {
    cm.operation(() => {
        const line = cm.getCursor().line;
        let p = null;

        if (p = isItemList(cm, line)) {
            const a = isItemList(cm, p.end + 1);
            if (a) {
                swap(cm, [p.start, p.end], [a.start, a.end]);
            }
        } else if (p = isNumberedList(cm, line)) {
            const a = isNumberedList(cm, p.end + 1);
            if (a) {
                swap(cm, [p.start, p.end], [a.start, a.end]);
            }
            rearrange_list(cm, line);
        } else if (p = isTitle(cm, line)) {
            const a = isTitle(cm, p.end + 1, p.level);
            if (a) {
                swap(cm, [p.start, p.end], [a.start, a.end]);
                org_set_fold(cm);
            }
        }
    });
};

export const org_shiftmetaright = function(cm) {
    cm.operation(() => {
        const line = cm.getCursor().line;
        let p = null;
        if (p = isTitle(cm, line)) {
            _metaright(cm, line);
            for (let i=p.start + 1; i<=p.end; i++) {
                if (isTitle(cm, i)) {
                    _metaright(cm, i);
                }
            }
        }
    });
};

export const org_shiftmetaleft = function(cm) {
    cm.operation(() => {
        const line = cm.getCursor().line;
        let p = null;
        if (p = isTitle(cm, line)) {
            if (p.level === 1) return;
            _metaleft(cm, line);
            for (let i=p.start + 1; i<=p.end; i++) {
                if (isTitle(cm, i)) {
                    _metaleft(cm, i);
                }
            }
        }
    });
};

function makeTitle(p) {
    let content = "*".repeat(p["level"])+" ";
    if (p["status"]) {
        content += p["status"]+" ";
    }
    content += p["content"];
    return content;
}

function previousOfType(cm, type, line) {
    let tmp;
    let i;
    for (i=line - 1; i>0; i--) {
        if (type === "list" || type === null) {
            tmp = isItemList(cm, line);
        } else if (type === "numbered" || type === null) {
            tmp = isNumberedList(cm, line);
        } else if (type === "title" || type === null) {
            tmp = isTitle(cm, line);
        }
        if (tmp !== null) {
            return tmp;
        }
    }
    return null;
}

function isItemList(cm, line) {
    const rootLineItem = findRootLine(cm, line);
    if (rootLineItem === null) return null;
    line = rootLineItem;
    const content = cm.getLine(line);

    if (content && (content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ")) return null;
    const padding = content.replace(/^(\s*).*$/, "$1").length;
    if (padding % 2 !== 0) return null;
    return {
        type: "list",
        level: padding / 2,
        content: content.trimLeft().replace(/^\s*\-\s(.*)$/, "$1"),
        start: line,
        end: (function(_cm, _line) {
            let line_candidate = _line;
            let content = null;
            do {
                _line += 1;
                content = _cm.getLine(_line);
                if (content === undefined || content.trimLeft()[0] === "-") {
                    break;
                } else if (/^\s+/.test(content)) {
                    line_candidate = _line;
                    continue;
                } else {
                    break;
                }
            } while (_line <= _cm.lineCount());
            return line_candidate;
        }(cm, line)),
    };

    function findRootLine(_cm, _line) {
        let content;
        do {
            content = _cm.getLine(_line);
            if (/^\s*\-/.test(content)) return _line;
            else if (/^\s+/.test(content) === false) {
                break;
            }
            _line -= 1;
        } while (_line >= 0);
        return null;
    }
}
function isNumberedList(cm, line) {
    const rootLineItem = findRootLine(cm, line);
    if (rootLineItem === null) return null;
    line = rootLineItem;
    const content = cm.getLine(line);

    if (/^[0-9]+[\.\)]\s.*$/.test(content && content.trimLeft()) === false) return null;
    const padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length;
    if (padding % 3 !== 0) return null;
    return {
        type: "numbered",
        level: padding / 3,
        content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, "$1"),
        start: line,
        end: (function(_cm, _line) {
            let line_candidate = _line;
            let content = null;
            do {
                _line += 1;
                content = _cm.getLine(_line);
                if (content === undefined || /^[0-9]+[\.\)]/.test(content.trimLeft())) {
                    break;
                } else if (/^\s+/.test(content)) {
                    line_candidate = _line;
                    continue;
                } else {
                    break;
                }
            } while (_line <= _cm.lineCount());
            return line_candidate;
        }(cm, line)),
        // specific
        n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")),
        separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, "$1"),
    };

    function findRootLine(_cm, _line) {
        let content;
        do {
            content = _cm.getLine(_line);
            if (/^\s*[0-9]+[\.\)]\s/.test(content)) return _line;
            else if (/^\s+/.test(content) === false) {
                break;
            }
            _line -= 1;
        } while (_line >= 0);
        return null;
    }
}
function isTitle(cm, line, level) {
    const content = cm.getLine(line);
    if (/^\*+\s/.test(content) === false) return null;
    const match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/);
    const reference_level = match[1].length;
    if (level !== undefined && level !== reference_level) return null;
    if (match === null) return null;
    return {
        type: "title",
        level: reference_level,
        content: match[3],
        start: line,
        end: (function(_cm, _line) {
            let line_candidate = _line;
            let content = null;
            do {
                _line += 1;
                content = _cm.getLine(_line);
                if (content === undefined) break;
                const match = content.match(/^(\*+)\s.*/);
                if (
                    match && match[1] &&
                    (match[1].length === reference_level || match[1].length < reference_level)
                ) {
                    break;
                } else {
                    line_candidate = _line;
                    continue;
                }
            } while (_line <= _cm.lineCount());
            return line_candidate;
        }(cm, line)),
        // specific
        status: match[2].trim(),
    };
}

function rearrange_list(cm, line) {
    const line_inferior = find_limit_inferior(cm, line);
    const line_superior = find_limit_superior(cm, line);

    let last_p = null;
    let p;

    for (let i=line_inferior; i<=line_superior; i++) {
        if (p = isNumberedList(cm, i)) {
            // rearrange numbers on the numbered list
            if (last_p) {
                if (p.level === last_p.level) {
                    const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
                    if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
                } else if (p.level > last_p.level) {
                    if (p.n !== 1) {
                        setNumber(cm, p.start, 1);
                    }
                } else if (p.level < last_p.level) {
                    const tmp = findLastAtLevel(cm, p.start, line_inferior, p.level);
                    if (tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1);
                }
            } else {
                if (p.n !== 1) setNumber(cm, p.start, 1);
            }
        }

        if (p = (isNumberedList(cm, i) || isItemList(cm, i))) {
            // rearrange spacing levels in list
            if (last_p) {
                if (p.level > last_p.level) {
                    if (p.level !== last_p.level + 1) {
                        setLevel(cm, [p.start, p.end], last_p.level + 1, p.type);
                    }
                }
            } else {
                if (p.level !== 0) {
                    setLevel(cm, [p.start, p.end], 0, p.type);
                }
            }
        }

        last_p = p;
        // we can process content block instead of line
        if (p) {
            i += (p.end - p.start);
        }
    }

    function findLastAtLevel(_cm, line, line_limit_inf, level) {
        let p;
        do {
            line -= 1;
            if ((p = isNumberedList(_cm, line)) && p.level === level) {
                return p;
            }
        } while (line > line_limit_inf);

        return null;
    }

    function setLevel(_cm, range, level, type) {
        let content;
        let i;
        for (i=range[0]; i<=range[1]; i++) {
            content = cm.getLine(i).trimLeft();
            const n_spaces = (function(_level, _line, _type) {
                let spaces = _level * 3;
                if (_line > 0) {
                    spaces += _type === "numbered" ? 3 : 2;
                }
                return spaces;
            }(level, i - range[0], type));

            content = " ".repeat(n_spaces) + content;
            cm.replaceRange(
                content,
                { line: i, ch: 0 },
                { line: i, ch: _cm.getLine(i).length },
            );
        }
    }

    function setNumber(_cm, line, level) {
        const content = _cm.getLine(line);
        const new_content = content.replace(/[0-9]+\./, level+".");
        cm.replaceRange(
            new_content,
            { line, ch: 0 },
            { line, ch: content.length },
        );
    }

    function find_limit_inferior(_cm, _line) {
        let content;
        let p;
        let match;
        let line_candidate = _line;
        do {
            content = _cm.getLine(_line);
            p = isNumberedList(_cm, _line);
            match = /(\s+).*$/.exec(content);
            if (p) line_candidate = _line;
            if (!p || !match) break;
            _line -= 1;
        } while (_line >= 0);
        return line_candidate;
    }
    function find_limit_superior(_cm, _line) {
        let content;
        let p;
        let match;
        let line_candidate = _line;
        do {
            content = _cm.getLine(_line);
            p = isNumberedList(_cm, _line);
            match = /(\s+).*$/.exec(content);
            if (p) line_candidate = _line;
            if (!p || !match) break;
            _line += 1;
        } while (_line < _cm.lineCount());
        return line_candidate;
    }
}

function swap(cm, from, to) {
    const from_content = cm.getRange(
        { line: from[0], ch: 0 },
        { line: from[1], ch: cm.getLine(from[1]).length },
    );
    const to_content = cm.getRange(
        { line: to[0], ch: 0 },
        { line: to[1], ch: cm.getLine(to[1]).length },
    );
    const cursor = cm.getCursor();

    if (to[0] > from[0]) {
        // moving down
        cm.replaceRange(
            from_content,
            { line: to[0], ch: 0 },
            { line: to[1], ch: cm.getLine(to[1]).length },
        );
        cm.replaceRange(
            to_content,
            { line: from[0], ch: 0 },
            { line: from[1], ch: cm.getLine(from[1]).length },
        );
        cm.setCursor({
            line: cursor.line + (to[1] - to[0] + 1),
            ch: cursor.ch,
        });
    } else {
        // moving up
        cm.replaceRange(
            to_content,
            { line: from[0], ch: 0 },
            { line: from[1], ch: cm.getLine(from[1]).length },
        );
        cm.replaceRange(
            from_content,
            { line: to[0], ch: 0 },
            { line: to[1], ch: cm.getLine(to[1]).length },
        );
        cm.setCursor({
            line: cursor.line - (to[1] - to[0] + 1),
            ch: cursor.ch,
        });
    }
}

export function fold(cm, start) {
    cm.foldCode(start, null, "fold");
}
export function unfold(cm, start) {
    cm.foldCode(start, null, "unfold");
}
export function isFold(cm, start) {
    const line = start.line;
    const marks = cm.findMarks(window.CodeMirror.Pos(line, 0), window.CodeMirror.Pos(line + 1, 0));
    for (let i = 0; i < marks.length; ++i) {
        if (marks[i].__isFold && marks[i].find().from.line === line) return marks[i];
    }
    return false;
}
