"""Python formatexpr for clang-format & yapf"""

import json
import sys
import subprocess
import difflib

import vim


class FormatError(Exception):
    """A formatting error."""


def clang_format():
    """Call clang-format on the current text object."""
    clang_format = vim.vars['clang_format_path']
    # TODO: The cursor position before gq is invoked is not preserved in the
    # jump list, this expression will return the cursor position at the
    # beginning of the motion or text object.
    # Is there a way to get the cursor position immediately before gq is
    # invoked? So that we can pass the position to clang-format in order for
    # it to return the updated position to continue editing.
    # Query the current absolute cursor positon.
    cursor = int(vim.eval('line2byte(".") + col(".")')) - 2
    if cursor < 0:
        return
    # Determine the lines to format.
    startline = int(vim.eval('v:lnum'))
    endline = startline + int(vim.eval('v:count')) - 1
    lines = f'{startline}:{endline}'
    fallback_style = vim.vars['clang_format_style']
    # Construct the clang-format command to call.
    command = [
        clang_format, '-style', 'file', '-cursor',
        str(cursor), '-lines', lines, '-fallback-style', fallback_style
    ]
    if vim.current.buffer.name:
        command += ['-assume-filename', vim.current.buffer.name]
    # Call the clang-format command.
    output = call(command)
    if not output:
        return
    # Read the clang-format json header.
    header = json.loads(output[0])
    if header.get('IncompleteFormat'):
        raise FormatError('clang-format: incomplete (syntax errors).')
    # Replace the formatted regions.
    replace_regions(output[1:])
    # Set the updated cursor position.
    vim.command('goto %d' % (header['Cursor'] + 1))


def yapf():
    """Call yapf on the current text object."""
    yapf = vim.vars['yapf_path']
    # Determine the lines to format.
    start = int(vim.eval('v:lnum'))
    end = start + int(vim.eval('v:count'))
    lines = '{0}-{1}'.format(start, end)
    style = vim.vars['yapf_style']
    # Construct the clang-format command to call.
    command = [yapf, '--style', style, '--lines', lines]
    # TODO: Since yapf is a Python package, we could import the module and
    # call it directly instead. It would then be possible to output better
    # error messages and act on the buffer directly.
    # Call the yapf command.
    output = call(command)
    if not output:
        return
    # Replace the formatted regions.
    replace_regions(output[:-1])


def call(command):
    """Call the command to format the text.

    Arguments:
        command (list): Formatting command to call.
        text (str): Text to be passed to stdin of command.

    Returns:
        list: The output of the formatter split on new lines.
        None: If the subprocess failed.
    """
    # Don't open a cmd prompt window on Windows.
    startupinfo = None
    if sys.platform.startswith('win32'):
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        startupinfo.wShowWindow = subprocess.SW_HIDE
    # Call the formatting command.
    process = subprocess.Popen(command,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE,
                               stdin=subprocess.PIPE,
                               startupinfo=startupinfo)
    stdout, stderr = process.communicate(
        input='\n'.join(vim.current.buffer).encode('utf-8'))
    if stderr:
        raise FormatError(stderr)
    if not stdout:
        raise FormatError('No output from {0}.'.format(command[0]))
    # Split the lines into a list of elements.
    return stdout.decode('utf-8').split('\n')


def replace_regions(lines):
    """Replace formatted regions in the current buffer.

    Arguments:
        lines (list): The formatted buffer lines.
    """
    matcher = difflib.SequenceMatcher(None, vim.current.buffer, lines)
    for tag, i1, i2, j1, j2 in reversed(matcher.get_opcodes()):
        if tag != 'equal':
            vim.current.buffer[i1:i2] = lines[j1:j2]