import * as ts from "typescript";




let checker: ts.TypeChecker;
let sourceFiles: ts.SourceFile[];
let rootFileNames: string[];
let dependencyMap: Map<string[]>;
let pathWeightMap: Map<number>;


interface Map<T> {
    [index: string]: T;
    [index: number]: T;
}

interface delayedProperty extends ts.VariableDeclaration {
    callerList?: string[];
    delayInitializerList?: ts.Expression[];
}

interface sortBlcok extends ts.Block {
    multiLine?: boolean;
    visitedBySorting?: boolean;
}

function createMap<T>(): Map<T> {
    const map: Map<T> = Object.create(null);
    // Using 'delete' on an object causes V8 to put the object in dictionary mode.
    // This disables creation of hidden classes, which are expensive when an object is
    // constantly changing shape.
    map["__"] = undefined;
    delete map["__"];
    return map;
}
//change
export function hasModifier(statement: ts.Statement | ts.ClassElement, keyword: ts.SyntaxKind): boolean {
    let declareKeword = false;
    //将hasModifier检测declare关键词功能提取
    if (statement.modifiers) {
        statement.modifiers.forEach((modify) => {
            if (modify.kind == keyword) {
                declareKeword = true;
            }
        });
    }
    return declareKeword;
}

export interface SortingResult {
    sortedFileNames: string[],
    circularReferences: string[]
}

export function reorderSourceFiles(program: ts.Program): SortingResult {
    sourceFiles = <any>program.getSourceFiles();
    rootFileNames = <any>program.getRootFileNames();
    checker = program.getTypeChecker();
    buildDependencyMap();
    let result = sortOnDependency();
    // sourceFiles = null;
    // rootFileNames = null;
    // checker = null;
    // dependencyMap = null;
    return result;
}


function addDependency(file: string, dependent: string): void {
    if (file == dependent) {
        return;
    }
    let list = dependencyMap[file];
    if (!list) {
        list = dependencyMap[file] = [];
    }
    if (list.indexOf(dependent) == -1) {
        list.push(dependent);
    }
}

function buildDependencyMap(): void {
    dependencyMap = createMap<string[]>();
    for (let i = 0; i < sourceFiles.length; i++) {
        let sourceFile = sourceFiles[i];
        if (sourceFile.isDeclarationFile) {
            continue;
        }
        visitFile(sourceFile);
    }
}

function visitFile(sourceFile: ts.SourceFile): void {
    let statements = sourceFile.statements;
    let length = statements.length;
    for (let i = 0; i < length; i++) {
        let statement = statements[i];

        if (hasModifier(statement, ts.SyntaxKind.DeclareKeyword)) { // has the 'declare' keyword
            continue;
        }
        visitStatement(statements[i]);
    }
}

//change
function visitStatement(statement: ts.Statement): void {
    if (!statement) {
        return;
    }
    switch (statement.kind) {
        case ts.SyntaxKind.ExpressionStatement:
            let expression = <ts.ExpressionStatement>statement;
            visitExpression(expression.expression);
            break;
        case ts.SyntaxKind.ClassDeclaration:
            checkInheriting(<ts.ClassDeclaration>statement);
            visitStaticMember(<ts.ClassDeclaration>statement);
            visitClassDecorators(<ts.ClassDeclaration>statement);
            // if (statement.transformFlags & TransformFlags.ContainsDecorators) {
            //     visitClassDecorators(<ts.ClassDeclaration>statement);
            // }
            break;
        case ts.SyntaxKind.VariableStatement:
            visitVariableList((<ts.VariableStatement>statement).declarationList);
            break;
        case ts.SyntaxKind.ImportEqualsDeclaration:
            let importDeclaration = <ts.ImportEqualsDeclaration>statement;
            checkDependencyAtLocation(importDeclaration.moduleReference);
            break;
        case ts.SyntaxKind.ModuleDeclaration:
            visitModule(<ts.ModuleDeclaration>statement);
            break;
        case ts.SyntaxKind.Block:
            visitBlock(<ts.Block>statement);
            break;
        case ts.SyntaxKind.IfStatement:
            const ifStatement = <ts.IfStatement>statement;
            visitExpression(ifStatement.expression);
            visitStatement(ifStatement.thenStatement);
            visitStatement(ifStatement.elseStatement);
            break;
        case ts.SyntaxKind.DoStatement:
        case ts.SyntaxKind.WhileStatement:
        case ts.SyntaxKind.WithStatement:
            const doStatement = <ts.DoStatement>statement;
            visitExpression(doStatement.expression);
            visitStatement(doStatement.statement);
            break;
        case ts.SyntaxKind.ForStatement:
            const forStatement = <ts.ForStatement>statement;
            visitExpression(forStatement.condition);
            visitExpression(forStatement.incrementor);
            if (forStatement.initializer) {
                if (forStatement.initializer.kind === ts.SyntaxKind.VariableDeclarationList) {
                    visitVariableList(<ts.VariableDeclarationList>forStatement.initializer);
                }
                else {
                    visitExpression(<ts.Expression>forStatement.initializer);
                }
            }
            break;
        case ts.SyntaxKind.ForInStatement:
        case ts.SyntaxKind.ForOfStatement:
            const forInStatement = <ts.ForInStatement>statement;
            visitExpression(forInStatement.expression);
            if (forInStatement.initializer) {
                if (forInStatement.initializer.kind === ts.SyntaxKind.VariableDeclarationList) {
                    visitVariableList(<ts.VariableDeclarationList>forInStatement.initializer);
                }
                else {
                    visitExpression(<ts.Expression>forInStatement.initializer);
                }
            }
            break;
        case ts.SyntaxKind.ReturnStatement:
            visitExpression((<ts.ReturnStatement>statement).expression);
            break;
        case ts.SyntaxKind.SwitchStatement:
            const switchStatment = <ts.SwitchStatement>statement;
            visitExpression(switchStatment.expression);
            switchStatment.caseBlock.clauses.forEach(element => {
                if (element.kind === ts.SyntaxKind.CaseClause) {
                    visitExpression((<ts.CaseClause>element).expression);
                }
                (<ts.DefaultClause>element).statements.forEach(element => {
                    visitStatement(element);
                })
            });
            break;
        case ts.SyntaxKind.LabeledStatement:
            visitStatement((<ts.LabeledStatement>statement).statement);
            break;
        case ts.SyntaxKind.ThrowStatement:
            visitExpression((<ts.ThrowStatement>statement).expression);
            break;
        case ts.SyntaxKind.TryStatement:
            const tryStatement = <ts.TryStatement>statement;
            visitBlock(tryStatement.tryBlock);
            visitBlock(tryStatement.finallyBlock);
            if (tryStatement.catchClause) {
                visitBlock(tryStatement.catchClause.block);
            }
            break;
    }
}

function visitModule(node: ts.ModuleDeclaration): void {
    if (node.body.kind === ts.SyntaxKind.ModuleDeclaration) {
        visitModule(<ts.ModuleDeclaration>node.body);
        return;
    }
    if (node.body.kind === ts.SyntaxKind.ModuleBlock) {
        for (let statement of (<ts.ModuleBlock>node.body).statements) {
            if (hasModifier(statement, ts.SyntaxKind.DeclareKeyword)) { // has the 'declare' keyword
                continue;
            }
            visitStatement(statement);
        }
    }

}

function checkDependencyAtLocation(node: ts.Node): void {
    let symbol = checker.getSymbolAtLocation(node);
    if (!symbol || !symbol.declarations) {
        return;
    }
    let sourceFile = getSourceFileOfNode(symbol.declarations[0]);
    if (!sourceFile || sourceFile.isDeclarationFile) {
        return;
    }
    addDependency(getSourceFileOfNode(node).fileName, sourceFile.fileName);
}

function checkInheriting(node: ts.ClassDeclaration): void {
    if (!node.heritageClauses) {
        return;
    }
    let heritageClause: ts.HeritageClause = null;
    for (const clause of node.heritageClauses) {
        if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
            heritageClause = clause;
            break;
        }
    }
    if (!heritageClause) {
        return;
    }
    let superClasses = heritageClause.types;
    if (!superClasses) {
        return;
    }
    superClasses.forEach(superClass => {
        checkDependencyAtLocation(superClass.expression);
    });
}

function visitStaticMember(node: ts.ClassDeclaration): void {
    let members = node.members;
    if (!members) {
        return;
    }
    for (let member of members) {
        if (!hasModifier(member, ts.SyntaxKind.StaticKeyword)) {
            continue;
        }
        if (member.kind == ts.SyntaxKind.PropertyDeclaration) {
            let property = <ts.PropertyDeclaration>member;
            visitExpression(property.initializer);
        }
    }
}
//change
function visitClassDecorators(node: ts.ClassDeclaration): void {
    if (node.decorators) {
        visitDecorators(node.decorators);
    }
    let members = node.members;
    if (!members) {
        return;
    }
    for (let member of members) {
        let decorators: ts.NodeArray<ts.Decorator>;
        let functionLikeMember: ts.FunctionLikeDeclaration;
        decorators = member.decorators;
        if (member.kind === ts.SyntaxKind.SetAccessor) {
            functionLikeMember = member as ts.AccessorDeclaration;
        } else if (member.kind === ts.SyntaxKind.MethodDeclaration) {
            functionLikeMember = <ts.MethodDeclaration>member;
        }
        if (decorators) {
            visitDecorators(decorators);
        }

        if (functionLikeMember) {
            for (const parameter of functionLikeMember.parameters) {
                if (parameter.decorators) {
                    visitDecorators(parameter.decorators);
                }
            }
        }
    }
}

function visitDecorators(decorators: ts.NodeArray<ts.Decorator>): void {
    for (let decorator of decorators) {
        visitExpression(decorator.expression);
    }
}

function visitExpression(expression: ts.Expression): void {
    if (!expression) {
        return;
    }
    switch (expression.kind) {
        case ts.SyntaxKind.NewExpression:
        case ts.SyntaxKind.CallExpression:
            visitCallExpression(<ts.CallExpression>expression);
            break;
        case ts.SyntaxKind.Identifier:
            checkDependencyAtLocation(expression);
            break;
        case ts.SyntaxKind.PropertyAccessExpression:
            checkDependencyAtLocation(expression);
            break;
        case ts.SyntaxKind.ElementAccessExpression:
            visitExpression((<ts.PropertyAccessExpression>expression).expression);
            break;
        case ts.SyntaxKind.ObjectLiteralExpression:
            visitObjectLiteralExpression(<ts.ObjectLiteralExpression>expression);
            break;
        case ts.SyntaxKind.ArrayLiteralExpression:
            let arrayLiteral = <ts.ArrayLiteralExpression>expression;
            arrayLiteral.elements.forEach(visitExpression);
            break;
        case ts.SyntaxKind.TemplateExpression:
            let template = <ts.TemplateExpression>expression;
            template.templateSpans.forEach(span => {
                visitExpression(span.expression);
            });
            break;
        case ts.SyntaxKind.ParenthesizedExpression:
            let parenthesized = <ts.ParenthesizedExpression>expression;
            visitExpression(parenthesized.expression);
            break;
        case ts.SyntaxKind.BinaryExpression:
            visitBinaryExpression(<ts.BinaryExpression>expression);
            break;
        case ts.SyntaxKind.PostfixUnaryExpression:
        case ts.SyntaxKind.PrefixUnaryExpression:
            visitExpression((<ts.PrefixUnaryExpression>expression).operand);
            break;
        case ts.SyntaxKind.DeleteExpression:
            visitExpression((<ts.DeleteExpression>expression).expression);
            break;
        case ts.SyntaxKind.TaggedTemplateExpression:
            visitExpression((<ts.TaggedTemplateExpression>expression).tag);
            visitExpression((<ts.TaggedTemplateExpression>expression).template);
            break;
        case ts.SyntaxKind.ConditionalExpression:
            visitExpression((<ts.ConditionalExpression>expression).condition);
            visitExpression((<ts.ConditionalExpression>expression).whenTrue);
            visitExpression((<ts.ConditionalExpression>expression).whenFalse);
            break;

        case ts.SyntaxKind.SpreadElement:
            visitExpression((<ts.SpreadElement>expression).expression);
            break;
        case ts.SyntaxKind.VoidExpression:
            visitExpression((<ts.VoidExpression>expression).expression);
            break;
        case ts.SyntaxKind.YieldExpression:
            visitExpression((<ts.YieldExpression>expression).expression);
            break;
        case ts.SyntaxKind.AwaitExpression:
            visitExpression((<ts.AwaitExpression>expression).expression);
            break;
        case ts.SyntaxKind.TypeOfExpression:
            visitExpression((<ts.TypeOfExpression>expression).expression);
            break;
        case ts.SyntaxKind.NonNullExpression:
            visitExpression((<ts.NonNullExpression>expression).expression);
            break;
        case ts.SyntaxKind.TypeAssertionExpression:
            visitExpression((<ts.TypeAssertion>expression).expression);
            break;
    }

    // FunctionExpression
    // ArrowFunction
    // ClassExpression
    // OmittedExpression
    // ExpressionWithTypeArguments
    // AsExpression
}

function visitBinaryExpression(binary: ts.BinaryExpression): void {
    let left = binary.left;
    let right = binary.right;
    visitExpression(left);
    visitExpression(right);
    if (binary.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
        (left.kind === ts.SyntaxKind.Identifier || left.kind === ts.SyntaxKind.PropertyAccessExpression) &&
        (right.kind === ts.SyntaxKind.Identifier || right.kind === ts.SyntaxKind.PropertyAccessExpression)) {
        let symbol = checker.getSymbolAtLocation(left);
        if (!symbol || !symbol.declarations) {
            return;
        }
        for (let declaration of symbol.declarations) {
            if (declaration.kind === ts.SyntaxKind.VariableDeclaration || declaration.kind === ts.SyntaxKind.PropertyDeclaration) {
                let variable = <delayedProperty>declaration;
                if (variable.initializer) {
                    continue;
                }
                if (!variable.delayInitializerList) {
                    variable.delayInitializerList = [];
                }
                variable.delayInitializerList.push(right);
                if (variable.callerList) {
                    for (let callerFileName of variable.callerList) {
                        checkCallTarget(callerFileName, right);
                    }
                }
            }
        }
    }
}

function visitObjectLiteralExpression(objectLiteral: ts.ObjectLiteralExpression): void {
    objectLiteral.properties.forEach(element => {
        switch (element.kind) {
            case ts.SyntaxKind.PropertyAssignment:
                visitExpression((<ts.PropertyAssignment>element).initializer);
                break;
            case ts.SyntaxKind.ShorthandPropertyAssignment:
                visitExpression((<ts.ShorthandPropertyAssignment>element).objectAssignmentInitializer);
                break;
            case ts.SyntaxKind.SpreadAssignment:
                visitExpression((<ts.SpreadAssignment>element).expression);
                break;
        }
    });
}

function visitCallExpression(callExpression: ts.CallExpression): void {
    if (callExpression.arguments) {
        callExpression.arguments.forEach(argument => {
            visitExpression(argument);
        });
    }

    let expression = escapeParenthesized(callExpression.expression);
    visitExpression(expression);
    switch (expression.kind) {
        case ts.SyntaxKind.FunctionExpression:
            let functionExpression = <ts.FunctionExpression>expression;
            visitBlock(functionExpression.body);
            break;
        case ts.SyntaxKind.PropertyAccessExpression:
        case ts.SyntaxKind.Identifier:
            let callerFileName = getSourceFileOfNode(callExpression).fileName;
            checkCallTarget(callerFileName, expression);
            break;
    }

}

function escapeParenthesized(expression: ts.Expression): ts.Expression {
    if (expression.kind === ts.SyntaxKind.ParenthesizedExpression) {
        return escapeParenthesized((<ts.ParenthesizedExpression>expression).expression);
    }
    return expression;
}

function checkCallTarget(callerFileName: string, target: ts.Node): void {
    let declarations: ts.Declaration[] = [];
    getForwardDeclarations(target, declarations, callerFileName);
    for (let declaration of declarations) {
        let sourceFile = getSourceFileOfNode(declaration);
        if (!sourceFile || sourceFile.isDeclarationFile) {
            return;
        }
        addDependency(callerFileName, sourceFile.fileName);
        if (declaration.kind === ts.SyntaxKind.FunctionDeclaration ||
            declaration.kind === ts.SyntaxKind.MethodDeclaration) {
            visitBlock((<ts.FunctionDeclaration>declaration).body);
        }
        else if (declaration.kind === ts.SyntaxKind.ClassDeclaration) {
            checkClassInstantiation(<ts.ClassDeclaration>declaration);
        }
    }
}

function getForwardDeclarations(reference: ts.Node, declarations: ts.Declaration[], callerFileName: string): void {
    let symbol = checker.getSymbolAtLocation(reference);
    if (!symbol || !symbol.declarations) {
        return;
    }
    for (let declaration of symbol.declarations) {
        switch (declaration.kind) {
            case ts.SyntaxKind.FunctionDeclaration:
            case ts.SyntaxKind.MethodDeclaration:
            case ts.SyntaxKind.ClassDeclaration:
                if (declarations.indexOf(declaration) == -1) {
                    declarations.push(declaration);
                }
                break;
            case ts.SyntaxKind.ImportEqualsDeclaration:
                getForwardDeclarations((<ts.ImportEqualsDeclaration>declaration).moduleReference, declarations, callerFileName);
                break;
            case ts.SyntaxKind.VariableDeclaration:
            case ts.SyntaxKind.PropertyDeclaration:
                const variable = <delayedProperty>declaration;
                const initializer = variable.initializer;
                if (initializer) {
                    if (initializer.kind === ts.SyntaxKind.Identifier || initializer.kind === ts.SyntaxKind.PropertyAccessExpression) {
                        getForwardDeclarations(initializer, declarations, callerFileName);
                    }
                }
                else {
                    if (variable.delayInitializerList) {
                        for (let expression of variable.delayInitializerList) {
                            getForwardDeclarations(expression, declarations, callerFileName);
                        }
                    }
                    if (variable.callerList) {
                        if (variable.callerList.indexOf(callerFileName) == -1) {
                            variable.callerList.push(callerFileName);
                        }
                    }
                    else {
                        variable.callerList = [callerFileName];
                    }
                }
                break;
        }
    }
}

function checkClassInstantiation(node: ts.ClassDeclaration): void {
    let members = node.members;
    if (!members) {
        return;
    }
    for (let member of members) {
        if (hasModifier(member, ts.SyntaxKind.StaticKeyword)) {
            continue;
        }
        if (member.kind === ts.SyntaxKind.PropertyDeclaration) {
            let property = <ts.PropertyDeclaration>member;
            visitExpression(property.initializer);
        }
        else if (member.kind === ts.SyntaxKind.Constructor) {
            let constructor = <ts.ConstructorDeclaration>member;
            visitBlock(constructor.body);
        }
    }
}

function visitBlock(block: ts.Block): void {
    let sortBlock = block as sortBlcok;
    if (!sortBlock || sortBlock.visitedBySorting) {
        return;
    }
    sortBlock.visitedBySorting = true;
    for (let statement of block.statements) {
        visitStatement(statement);
    }
}

function visitVariableList(variables: ts.VariableDeclarationList) {
    if (!variables) {
        return;
    }
    variables.declarations.forEach(declaration => {
        visitExpression(declaration.initializer);
    });
}

function sortOnDependency(): SortingResult {
    let result: SortingResult = <any>{};
    result.sortedFileNames = [];
    result.circularReferences = [];
    pathWeightMap = createMap<number>();
    let dtsFiles: ts.SourceFile[] = [];
    let tsFiles: ts.SourceFile[] = [];
    for (let sourceFile of sourceFiles) {
        let path = sourceFile.fileName;
        if (sourceFile.isDeclarationFile) {
            pathWeightMap[path] = 10000;
            dtsFiles.push(sourceFile);
            continue;
        }
        let references = updatePathWeight(path, 0, [path]);
        if (references) {
            result.circularReferences = references;
            break;
        }
        tsFiles.push(sourceFile);
    }
    if (result.circularReferences.length === 0) {
        tsFiles.sort(function (a: ts.SourceFile, b: ts.SourceFile): number {
            return pathWeightMap[b.fileName] - pathWeightMap[a.fileName];
        });
        sourceFiles.length = 0;
        rootFileNames.length = 0;
        dtsFiles.concat(tsFiles).forEach(sourceFile => {
            sourceFiles.push(sourceFile);
            rootFileNames.push(sourceFile.fileName);
            result.sortedFileNames.push(sourceFile.fileName);
        });
    }
    pathWeightMap = null;
    return result;
}

function updatePathWeight(path: string, weight: number, references: string[]): string[] {
    if (pathWeightMap[path] === undefined) {
        pathWeightMap[path] = weight;
    }
    else {
        if (pathWeightMap[path] < weight) {
            pathWeightMap[path] = weight;
        }
        else {
            return null;
        }
    }
    let list = dependencyMap[path];
    if (!list) {
        return null;
    }
    for (let parentPath of list) {
        if (references.indexOf(parentPath) != -1) {
            references.push(parentPath);
            return references;
        }
        let result = updatePathWeight(parentPath, weight + 1, references.concat(parentPath));
        if (result) {
            return result;
        }
    }
    return null;
}

function getSourceFileOfNode(node: ts.Node): ts.SourceFile {
    while (node && node.kind !== ts.SyntaxKind.SourceFile) {
        node = node.parent;
    }
    return <ts.SourceFile>node;
}