#!/usr/bin/env python3
#
# Copyright © 2019 Michael Catanzaro <mcatanzaro@gnome.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Based on original C lineup-parameters by Sébastien Wilmet <swilmet@gnome.org>
# Rewritten in Python to allow simple execution from source directory.
#
# Usage: lineup-parameters [file]
# If the file is not given, stdin is read.
# The result is printed to stdout.
#
# The restrictions:
# - The function name must be at column 0, followed by a space and an opening
#   parenthesis;
# - One parameter per line;
# - A parameter must follow certain rules (see the regex in the code), but it
#   doesn't accept all possibilities of the C language.
# - The opening curly brace ("{") of the function must also be at column 0.
#
# If one restriction is missing, the function declaration is not modified.
#
# Example:
#
# gboolean
# frobnitz (Frobnitz *frobnitz,
#           gint magic_number,
#           GError **error)
# {
#   ...
# }
#
# Becomes:
#
# gboolean
# frobnitz (Frobnitz  *frobnitz,
#           gint       magic_number,
#           GError   **error)
# {
#   ...
# }
#
# TODO: support "..." vararg parameter

import argparse
import re
import sys

from typing import NamedTuple

# https://regexr.com/ is your friend.
functionNameRegex = re.compile(r"^(\w+) ?\(")
parameterRegex = re.compile(
    r"^\s*(?P<type>(const\s+)?\w+)"
    r"\s+(?P<stars>\**)"
    r"\s*(?P<name>\w+)"
    r"\s*(?P<end>,|\))"
    r"\s*$"
)
openingCurlyBraceRegex = re.compile(r"^{\s*$")


def matchFunctionName(line):
    match = functionNameRegex.match(line)
    if match:
        functionName = match.group(1)
        firstParamPosition = match.end(0)
        return (functionName, firstParamPosition)
    return (None, 0)


class ParameterInfo(NamedTuple):
    paramType: str
    name: str
    numStars: int
    isLastParameter: bool


def matchParameter(line):
    _, firstParamPosition = matchFunctionName(line)
    match = parameterRegex.match(line[firstParamPosition:])
    if match is None:
        return None
    paramType = match.group("type")
    assert paramType is not None
    name = match.group("name")
    assert name is not None
    stars = match.group("stars")
    numStars = len(stars) if stars is not None else 0
    end = match.group("end")
    isLastParameter = True if end is not None and end == ")" else False
    return ParameterInfo(paramType, name, numStars, isLastParameter)


def matchOpeningCurlyBrace(line):
    return True if openingCurlyBraceRegex.match(line) is not None else False


# Length returned is number of lines the declaration takes up
def getFunctionDeclarationLength(remainingLines):
    for i in range(len(remainingLines)):
        currentLine = remainingLines[i]
        parameterInfo = matchParameter(currentLine)
        if parameterInfo is None:
            return 0
        if parameterInfo.isLastParameter:
            if i + 1 == len(remainingLines):
                return 0
            nextLine = remainingLines[i + 1]
            if not matchOpeningCurlyBrace(nextLine):
                return 0
            return i + 1
    return 0


def getParameterInfos(remainingLines, length):
    parameterInfos = []
    for i in range(length):
        parameterInfos.append(matchParameter(remainingLines[i]))
    return parameterInfos


def computeSpacing(parameterInfos):
    maxTypeLength = 0
    maxStarsLength = 0
    for parameterInfo in parameterInfos:
        maxTypeLength = max(maxTypeLength, len(parameterInfo.paramType))
        maxStarsLength = max(maxStarsLength, parameterInfo.numStars)
    return (maxTypeLength, maxStarsLength)


def printParameter(parameterInfo, maxTypeLength, maxStarsLength, outfile):
    outfile.write(f"{parameterInfo.paramType:<{maxTypeLength + 1}}")
    paramNamePaddedWithStars = (
        f"{parameterInfo.name:*>{parameterInfo.numStars + len(parameterInfo.name)}}"
    )
    outfile.write(
        f"{paramNamePaddedWithStars:>{maxStarsLength + len(parameterInfo.name)}}"
    )


def printFunctionDeclaration(remainingLines, length, useTabs, outfile):
    functionName, _ = matchFunctionName(remainingLines[0])
    assert functionName is not None
    outfile.write(f"{functionName} (")
    numSpacesToParenthesis = len(functionName) + 2
    whitespace = ""
    if useTabs:
        tabs = "".ljust(numSpacesToParenthesis // 8, "\t")
        spaces = "".ljust(numSpacesToParenthesis % 8)
        whitespace = tabs + spaces
    else:
        whitespace = "".ljust(numSpacesToParenthesis)
    parameterInfos = getParameterInfos(remainingLines, length)
    maxTypeLength, maxStarsLength = computeSpacing(parameterInfos)
    numParameters = len(parameterInfos)
    for i in range(numParameters):
        parameterInfo = parameterInfos[i]
        if i != 0:
            outfile.write(whitespace)
        printParameter(parameterInfo, maxTypeLength, maxStarsLength, outfile)
        if i + 1 != numParameters:
            outfile.write(",\n")
    outfile.write(")\n")


def parseContents(infile, useTabs, outfile):
    lines = infile.readlines()
    i = 0
    while i < len(lines):
        line = lines[i]
        functionName, _ = matchFunctionName(line)
        if functionName is None:
            outfile.write(line)
            i += 1
            continue
        remainingLines = lines[i:]
        length = getFunctionDeclarationLength(remainingLines)
        if length == 0:
            outfile.write(line)
            i += 1
            continue
        printFunctionDeclaration(remainingLines, length, useTabs, outfile)
        i += length


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Line up parameters of C functions")
    parser.add_argument(
        "infile",
        nargs="?",
        type=argparse.FileType("r"),
        default=sys.stdin,
        help="input C source file",
    )
    parser.add_argument(
        "-o",
        metavar="outfile",
        nargs="?",
        type=argparse.FileType("w"),
        default=sys.stdout,
        help="where to output modified file",
    )
    parser.add_argument(
        "--tabs", action="store_true", help="whether use tab characters in the output"
    )
    args = parser.parse_args()
    parseContents(args.infile, args.tabs, args.o)
    args.infile.close()
    args.o.close()
