import fs from './fileSystem'
import async from 'async'
import path from 'path'
import { Compiler as RemixCompiler } from '@remix-project/remix-solidity'
import { SrcIfc, CompilerConfiguration, CompilationErrors } from './types'

function regexIndexOf (inputString: string, regex: RegExp, startpos = 0) {
    const indexOf = inputString.substring(startpos).search(regex)
    return (indexOf >= 0) ? (indexOf + (startpos)) : indexOf
}

function writeTestAccountsContract (accounts: string[]) {
    const testAccountContract = require('../sol/tests_accounts.sol')
    let body = `address[${accounts.length}] memory accounts;`
    if (!accounts.length) body += ';'
    else {
        accounts.map((address, index) => {
            body += `\naccounts[${index}] = ${address};\n`
        })
    }
    return testAccountContract.replace('>accounts<', body)
}

/**
 * @dev Check if path includes name of a remix test file
 * @param path file path to check
 */

function isRemixTestFile(path: string) {
    return ['tests.sol', 'remix_tests.sol', 'remix_accounts.sol'].some(name => path.includes(name))
}

/**
 * @dev Process file to prepare sources object to be passed in solc compiler input
 * 
 * See: https://solidity.readthedocs.io/en/latest/using-the-compiler.html#input-description
 * 
 * @param filePath path of file to process
 * @param sources existing 'sources' object in which keys are the "global" names of the source files and 
 *                value is object containing content of corresponding file with under key 'content'
 * @param isRoot True, If file is a root test contract file which is getting processed, not an imported file
 */

function processFile(filePath: string, sources: SrcIfc, isRoot = false) {
    const importRegEx = /import ['"](.+?)['"];/g;
    let group: RegExpExecArray| null = null;
    const isFileAlreadyInSources: boolean = Object.keys(sources).includes(filePath)

    // Return if file is a remix test file or already processed
    if(isRemixTestFile(filePath) || isFileAlreadyInSources)
        return

    let content: string = fs.readFileSync(filePath, { encoding: 'utf-8' });
    const testFileImportRegEx = /^(import)\s['"](remix_tests.sol|tests.sol)['"];/gm

    // import 'remix_tests.sol', if file is a root test contract file and doesn't already have it 
    if (isRoot && filePath.endsWith('_test.sol') && regexIndexOf(content, testFileImportRegEx) < 0) {
        const includeTestLibs = '\nimport \'remix_tests.sol\';\n'
        content = includeTestLibs.concat(content)
    }
    sources[filePath] = {content};
    importRegEx.exec(''); // Resetting state of RegEx

    // Process each 'import' in file content
    while ((group = importRegEx.exec(content))) {
        const importedFile: string = group[1];
        const importedFilePath: string = path.join(path.dirname(filePath), importedFile);
        processFile(importedFilePath, sources)
    }
}

const userAgent = (typeof (navigator) !== 'undefined') && navigator.userAgent ? navigator.userAgent.toLowerCase() : '-'
const isBrowser = !(typeof (window) === 'undefined' || userAgent.indexOf(' electron/') > -1)

/**
 * @dev Compile file or files before running tests (used for CLI execution)
 * @param filename Name of file
 * @param isDirectory True, if path is a directory
 * @param opts Options
 * @param cb Callback
 * 
 * TODO: replace this with remix's own compiler code
 */

export function compileFileOrFiles(filename: string, isDirectory: boolean, opts: any, cb): void {
    let compiler: any
    const accounts: string[] = opts.accounts || []
    const sources: SrcIfc = {
        'tests.sol': { content: require('../sol/tests.sol') },
        'remix_tests.sol': { content: require('../sol/tests.sol') },
        'remix_accounts.sol': { content: writeTestAccountsContract(accounts) }
    }
    const filepath: string = (isDirectory ? filename : path.dirname(filename))
    try {
        if(!isDirectory && fs.existsSync(filename)) {
            if (filename.split('.').pop() === 'sol') {
                processFile(filename, sources, true)
            } else {
                throw new Error('Not a solidity file')
            }
        } else {
            // walkSync only if it is a directory
            fs.walkSync(filepath, (foundpath: string) => {
                // only process .sol files
                if (foundpath.split('.').pop() === 'sol') {
                    processFile(foundpath, sources, true)
                }
            })
        }
        
    } catch (e) { // eslint-disable-line no-useless-catch
        throw e
    } finally {
        async.waterfall([
            function loadCompiler(next) {
                compiler = new RemixCompiler()
                compiler.onInternalCompilerLoaded()
                // compiler.event.register('compilerLoaded', this, function (version) {
                next()
                // });
            },
            function doCompilation(next) {
                // @ts-ignore
                compiler.event.register('compilationFinished', this, (success, data, source) => {
                    next(null, data)
                })
                compiler.compile(sources, filepath)
            }
        ], function (err: Error | null | undefined, result: any) {
            const error: Error[] = []
            if (result.error) error.push(result.error)
            const errors = (result.errors || error).filter((e) => e.type === 'Error' || e.severity === 'error')
            if (errors.length > 0) {
                if (!isBrowser) require('signale').fatal(errors)
                return cb(new CompilationErrors(errors))
            }
            cb(err, result.contracts, result.sources) //return callback with contract details & ASTs
        })
    }
}

/**
 * @dev Compile contract source before running tests (used for IDE tests execution)
 * @param sources sources
 * @param compilerConfig current compiler configuration
 * @param importFileCb Import file callback
 * @param opts Options
 * @param cb Callback
 */
export function compileContractSources(sources: SrcIfc, compilerConfig: CompilerConfiguration, importFileCb: any, opts: any, cb): void {
    let compiler, filepath: string
    const accounts: string[] = opts.accounts || []
    // Iterate over sources keys. Inject test libraries. Inject test library import statements.
    if (!('remix_tests.sol' in sources) && !('tests.sol' in sources)) {
        sources['tests.sol'] = { content: require('../sol/tests.sol.js') }
        sources['remix_tests.sol'] = { content: require('../sol/tests.sol.js') }
        sources['remix_accounts.sol'] = { content: writeTestAccountsContract(accounts) }
    }
    const testFileImportRegEx = /^(import)\s['"](remix_tests.sol|tests.sol)['"];/gm

    const includeTestLibs = '\nimport \'remix_tests.sol\';\n'
    for (const file in sources) {
        const c: string = sources[file].content
        if (file.endsWith('_test.sol') && c && regexIndexOf(c, testFileImportRegEx) < 0) {
            sources[file].content = includeTestLibs.concat(c)
        }
    }

    async.waterfall([
        function loadCompiler (next) {
            const {currentCompilerUrl, evmVersion, optimize, usingWorker} = compilerConfig
            compiler = new RemixCompiler(importFileCb)
            compiler.set('evmVersion', evmVersion)
            compiler.set('optimize', optimize)
            compiler.loadVersion(usingWorker, currentCompilerUrl)
            // @ts-ignore
            compiler.event.register('compilerLoaded', this, (version) => {
                next()
            })
        },
        function doCompilation (next) {
            // @ts-ignore
            compiler.event.register('compilationFinished', this, (success, data, source) => {
                next(null, data)
            })
            compiler.compile(sources, filepath)
        }
    ], function (err: Error | null | undefined , result: any) {
        const error: Error[] = []
        if (result.error) error.push(result.error)
        const errors = (result.errors || error).filter((e) => e.type === 'Error' || e.severity === 'error')
        if (errors.length > 0) {
            if (!isBrowser) require('signale').fatal(errors)
            return cb(new CompilationErrors(errors))
        }
        cb(err, result.contracts, result.sources) // return callback with contract details & ASTs
    })
}
