import * as g from './generator';
import * as tsa from './tsAnalyzer';
import * as tsch from './tsCompilerHost';
import * as log from './logger';
import * as nameUnifier from './nameUnifier';
import * as ts from 'typescript';
import * as fs from 'fs';
import * as pathPlatformDependent from 'path';

const path = pathPlatformDependent.posix; // This works everythere, just use forward slashes

var defaultLibFilename = path.join(path.dirname(require.resolve("typescript").replace(/\\/g, '/')), "lib.es6.d.ts");

export default (project: g.IGenerationProject, tsAnalyzer: tsa.ITsAnalyzer, logger: log.ILogger, rootStateKey: string = null): g.IGenerationProcess => {
    return {
        run: () => runBase(false, project, tsAnalyzer, logger, rootStateKey),
        runRecurse: () => runBase(true, project, tsAnalyzer, logger, rootStateKey)
    }
}

function runBase(applyRecurse: boolean, project: g.IGenerationProject, tsAnalyzer: tsa.ITsAnalyzer, logger: log.ILogger, rootStateKey: string): Promise<any> {
    const writeCallback = (fn, c) => { project.writeFileCallback(fn, new Buffer(c, 'utf-8')); }
    function writeCursors(params: g.ILoadedParams, currentStateName: string, rootStateKey: string) {
        let stateFilePath = params.stateFilePath;
        let mainState = g.resolveState(params.data.states, currentStateName);
        if (!mainState)
            return;
        const stateAlias = g.createUnusedAlias(g.stateImportKey, params.data.imports);
        const bobfluxPrefix = g.resolveBobfluxPrefix(mainState);
        logger.info('Generating has been started for: ', stateFilePath);
        const bobfluxImport = params.data.sourceDeps[bobfluxPrefix];
        let imports: { [fullPath: string]: tsa.IImportData } = bobfluxImport ? { [bobfluxImport.fullPath]: bobfluxImport } : {};
        let stateFieldCursors = createCursorsForStateFields(params, imports, rootStateKey, params.data, mainState, bobfluxPrefix, stateAlias);
        writeCallback(
            createCursorsFilePath(stateFilePath),
            g.createAutogeneratedHeader(project.version)
            + g.createFullImports(stateAlias, `./${params.data.fileName}`, Object.keys(imports).map(key => repairRelativePath(imports[key], stateFilePath)))
            + createRootKey(rootStateKey, bobfluxPrefix)
            + createRootCursor(rootStateKey, bobfluxPrefix, stateAlias, mainState.typeName)
            + stateFieldCursors
        );
        logger.info('Generating ended for: ', stateFilePath);
    }
    function createCursorsForStateFields(
        params: g.ILoadedParams,
        topLevelImports: { [fullPath: string]: tsa.IImportData },
        parentStateKey: string,
        data: tsa.IStateSourceData,
        state: tsa.IStateData,
        bobfluxPrefix: string,
        stateAlias: string,
        prefix: string = null
    ): string {
        let nexts: INextIteration[] = [];
        function queue(p: { state: tsa.IStateData, data: tsa.IStateSourceData, externalFileAlias: string, prefix: string }) {
            nexts.push(p);
            p.state.heritages.forEach(heritageName => {
                let heritage = p.data.states.find(s => s.typeName === heritageName);
                // cross-file heritage is not implemented yet
                if (heritage)
                    nexts.push(Object.assign({}, p, { state: heritage }));
            });
        }
        let inner = state.fields.map(f => {
            let key = parentStateKey === null ? g.composeCursorKey(parentStateKey, prefix, f.name) : g.composeCursorKey(prefix, f.name);
            if (applyRecurse) {
                f.type.forEach(t => {
                    if (t.isArray || !g.isExternalState(t.name, data))
                        return;
                    let alias = g.getExternalAlias(t.name, data, topLevelImports);
                    let innerFilePath = path.join(path.dirname(data.filePath), alias.relativePath + '.ts');
                    let innerSourceFile = g.resolveSourceFile(params.sourceFiles, innerFilePath);
                    if (innerSourceFile) {
                        let innerData = tsAnalyzer.getSourceData(innerSourceFile, params.typeChecker);
                        let innerResolvedState = g.resolveState(innerData.states, alias.sourceType);
                        if (innerResolvedState)
                            if (g.isRouteComponentState(...innerResolvedState.heritages) && f.type.length === 1)
                                writeCursors({
                                    stateFilePath: innerSourceFile.path,
                                    data: tsAnalyzer.getSourceData(innerSourceFile, params.typeChecker),
                                    sourceFiles: params.sourceFiles,
                                    typeChecker: params.typeChecker
                                }, alias.sourceType, g.composeCursorKey(parentStateKey, key));
                            else
                                queue({ state: innerResolvedState, data: innerData, externalFileAlias: alias.prefix, prefix: key });
                    }
                });
            }
            const fieldType = g.getFullType(f.type, data, stateAlias, topLevelImports);
            f.type.forEach(t => {
                if (t.isArray)
                    return;
                let states = data.states.filter(s => s.typeName === t.name);
                if (states.length > 0 && !t.arguments /* not implemented yet*/)
                    queue({ state: states[0], data: data, externalFileAlias: stateAlias, prefix: key });
                if (states.length > 1)
                    throw 'Two states with same name could not be parsed. It\'s compilation error.';
            });
            // if (f.type.map(t => t.name).indexOf('undefined') >= 0)
            //     return '';
            return createFieldCursor(prefix, key, f.name, bobfluxPrefix, fieldType, parentStateKey !== null, f);
        }).join('\n');
        return inner + (nexts.length > 0 ? '\n' : '') + nexts.map(n => createCursorsForStateFields(params, topLevelImports, parentStateKey, n.data, n.state, bobfluxPrefix, n.externalFileAlias, n.prefix)).filter(n => n).join('\n');
    }
    return new Promise((f, r) => {
        g.loadSourceFiles(project, tsAnalyzer, logger)
            .then(p => {
                try {
                    writeCursors(p, project.appStateName, rootStateKey);
                } catch (e) {
                    logger.error('Error on cursors writing.', e);
                }
                f();
            })
            .catch(e => r(e));
    })
}

function createRootKey(key: string, bobfluxPrefix: string): string {
    return `export const rootKey = ${key ? `'${key}'` : `${bobfluxPrefix}.rootCursor.key`};

`;
}

function createFieldCursor(prefix: string, key: string, fieldName: string, bobfluxPrefix: string, typeName: string, withRoot: boolean, f: tsa.IStateFieldData): string {
    let undefinedDef = f.type.some(t => t.name === 'undefined') ? `,
    isUndefinable: true` : '';
    return `export const ${prefix === null ? fieldName : nameUnifier.getStatePrefixFromKeyPrefix(prefix, fieldName)}Cursor: ${bobfluxPrefix}.ICursor<${typeName}> = {
    key: ${withRoot ? `rootKey + '.${key}'` : `'${key}'`}${undefinedDef}
};
`;
}

function createArrayIndexFactoryCursor(prefix: string, key: string, fieldName: string, bobfluxPrefix: string, typeName: string, withRoot: boolean): string {
    return `
export const ${prefix === null ? fieldName : nameUnifier.getStatePrefixFromKeyPrefix(prefix, fieldName)}IndexFactoryCursor: (index: number) => ${bobfluxPrefix}.IIndexCursorReslut = (index: number) => {
    return {
        cursor: <${bobfluxPrefix}.ICursor<${typeName}>>{ key: ${withRoot ? `rootKey + '.${key}.'` : `'${key}.'`} + index }
    };
};
`;
}


function createRootCursor(key: string, bobfluxPrefix: string, stateAlias: string, typeName: string): string {
    return `${key
        ? `export const rootCursor: ${bobfluxPrefix}.ICursor<${stateAlias}.${typeName}> = {
    key: rootKey
};
`
        : `export const rootCursor: ${bobfluxPrefix}.ICursor<${stateAlias}.${typeName}> = ${bobfluxPrefix}.rootCursor;
`}
export default rootCursor;

`;
}

export function createCursorsFilePath(stateFilePath: string): string {
    return `${path.join(path.dirname(stateFilePath), path.basename(stateFilePath).replace(path.extname(stateFilePath), ''))}.cursors.ts`;
}

function repairRelativePath(imp: tsa.IImportData, stateFilePath: string): tsa.IImportData {
    if (!imp.relativePath.startsWith('.'))  // modules
        return imp;

    let dir = path.dirname(stateFilePath);
    if (!path.isAbsolute(dir))
        dir = path.join(process.cwd(), dir);

    let fullPath = imp.fullPath;
    if (!path.isAbsolute(fullPath))
        fullPath = path.join(process.cwd(), fullPath);

    let relativePath = path.relative(dir, fullPath);
    if (!relativePath.startsWith('.'))
        relativePath = `.${path.sep}${relativePath}`;
    return Object.assign({}, imp, { relativePath });
}

interface INextIteration {
    externalFileAlias: string
    state: tsa.IStateData
    data: tsa.IStateSourceData
    prefix: string
}

interface IExternalChildState {
    state: string
    import: tsa.IImportData
    prefix: string
    parentFullPath: string
}
