import * as Lint from "tslint/lib";
import * as ts from "typescript";

export class Rule extends Lint.Rules.AbstractRule {
    public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
        return this.applyWithFunction(sourceFile, ctx => walk(ctx));
    }
}

function walk(ctx: Lint.WalkContext<void>): void {
    const { sourceFile } = ctx;
    ts.forEachChild(sourceFile, function recur(node: ts.Node): void {
        if (node.kind === ts.SyntaxKind.CallExpression) {
            checkCall(node as ts.CallExpression);
        }
        ts.forEachChild(node, recur);
    });

    function checkCall(node: ts.CallExpression): void {
        if (!shouldIgnoreCalledExpression(node.expression)) {
            for (const arg of node.arguments) {
                checkArg(arg);
            }
        }
    }

    /** Skip certain function/method names whose parameter names are not informative. */
    function shouldIgnoreCalledExpression(expression: ts.Expression): boolean {
        if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
            const methodName = (expression as ts.PropertyAccessExpression).name.text as string;
            if (methodName.indexOf("set") === 0) {
                return true;
            }
            switch (methodName) {
                case "apply":
                case "assert":
                case "assertEqual":
                case "call":
                case "equal":
                case "fail":
                case "isTrue":
                case "output":
                case "stringify":
                    return true;
            }
        }
        else if (expression.kind === ts.SyntaxKind.Identifier) {
            const functionName = (expression as ts.Identifier).text as string;
            if (functionName.indexOf("set") === 0) {
                return true;
            }
            switch (functionName) {
                case "assert":
                case "contains":
                case "createAnonymousType":
                case "createImportSpecifier":
                case "createProperty":
                case "createSignature":
                case "resolveName":
                    return true;
            }
        }
        return false;
    }

    function checkArg(arg: ts.Expression): void {
        if (!isTrivia(arg)) {
            return;
        }

        const ranges = ts.getTrailingCommentRanges(sourceFile.text, arg.pos) || ts.getLeadingCommentRanges(sourceFile.text, arg.pos);
        if (ranges === undefined || ranges.length !== 1 || ranges[0].kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
            ctx.addFailureAtNode(arg, "Tag argument with parameter name");
            return;
        }

        const range = ranges[0];
        const argStart = arg.getStart(sourceFile);
        if (range.end + 1 !== argStart && sourceFile.text.slice(range.end, argStart).indexOf("\n") === -1) {
            ctx.addFailureAtNode(arg, "There should be 1 space between an argument and its comment.");
        }
    }

    function isTrivia(arg: ts.Expression): boolean {
        switch (arg.kind) {
            case ts.SyntaxKind.TrueKeyword:
            case ts.SyntaxKind.FalseKeyword:
            case ts.SyntaxKind.NullKeyword:
                return true;
            case ts.SyntaxKind.Identifier:
                return (arg as ts.Identifier).originalKeywordKind === ts.SyntaxKind.UndefinedKeyword;
            default:
                return false;
        }
    }
}
