#!/usr/bin/env node

import { Compiler } from '@0x/sol-compiler';
import { NameResolver } from '@0x/sol-resolver';
import { PackageJSON } from '@0x/types';
import { logUtils } from '@0x/utils';
import { CompilerOptions } from 'ethereum-types';
import * as fs from 'fs';
import * as _ from 'lodash';
import * as mkdirp from 'mkdirp';
import * as path from 'path';
import * as prettier from 'prettier';
import toSnakeCase = require('to-snake-case');

const SOLIDITY_EXTENSION = '.sol';
const DEFAULT_ARTIFACTS_DIR = 'test/artifacts';
const DEFAULT_CONTRACTS_DIR = 'contracts';
const DEFAULT_WRAPPERS_DIR = 'test/generated-wrappers';
const SRC_ARTIFACTS_TS_FILE_PATH = 'src/artifacts.ts';
const TEST_ARTIFACTS_TS_FILE_PATH = 'test/artifacts.ts';
const SRC_WRAPPERS_TS_FILE_PATH = 'src/wrappers.ts';
const TEST_WRAPPERS_TS_FILE_PATH = 'test/wrappers.ts';
const AUTO_GENERATED_BANNER = `/*
* -----------------------------------------------------------------------------
* Warning: This file is auto-generated by contracts-gen. Don't edit manually.
* -----------------------------------------------------------------------------
*/`;
const AUTO_GENERATED_BANNER_FOR_LISTS = `This list is auto-generated by contracts-gen. Don't edit manually.`;
const ALL_CONTRACTS_IDENTIFIER = '*';
const GENERATE = 'generate';
const COPY = 'copy';

(async () => {
    const command = process.argv.pop();
    if (command !== GENERATE && command !== COPY) {
        throw new Error(`Unknown command found: ${command}`);
    }

    const compilerJSON = readJSONFile<CompilerOptions>('compiler.json');
    const compiler = new Compiler(compilerJSON);
    const testContracts = compiler.getContractNamesToCompile();
    if (!_.isArray(testContracts)) {
        throw new Error('Unable to run the generator bacause contracts key in compiler.json is not of type array');
    }
    let srcContracts = testContracts;
    const packageJSON = readJSONFile<PackageJSON>('package.json');
    if (packageJSON.config !== undefined && (packageJSON.config as any).publicInterfaceContracts !== undefined) {
        srcContracts = (packageJSON.config as any).publicInterfaceContracts.split(',');
    }
    if (!_.isArray(testContracts)) {
        throw new Error('Unable to run the generator bacause contracts key in compiler.json is not of type array');
    }
    const testArtifactsDir = compilerJSON.artifactsDir || DEFAULT_ARTIFACTS_DIR;
    const srcArtifactsDir = convertToTopLevelDir('testArtifactsDir', testArtifactsDir);
    const testWrappersDir = DEFAULT_WRAPPERS_DIR;
    const srcWrappersDir = convertToTopLevelDir('testWrappersDir', testWrappersDir);

    // Make sure all dirs exist, if not, create them
    mkdirp.sync(testArtifactsDir);
    mkdirp.sync(srcArtifactsDir);
    mkdirp.sync(testWrappersDir);
    mkdirp.sync(srcWrappersDir);

    if (command === GENERATE) {
        await regenerateContractPackageAsync(
            testContracts,
            srcContracts,
            testArtifactsDir,
            srcArtifactsDir,
            testWrappersDir,
            srcWrappersDir,
        );
    } else if (command === COPY) {
        copyOverTestArtifactsAndWrappersToSrc(
            srcContracts,
            testArtifactsDir,
            srcArtifactsDir,
            testWrappersDir,
            srcWrappersDir,
        );
    }
    process.exit(0);
})().catch(err => {
    logUtils.log(err);
    process.exit(1);
});

function copyOverTestArtifactsAndWrappersToSrc(
    srcContracts: string[],
    testArtifactsDir: string,
    srcArtifactsDir: string,
    testWrappersDir: string,
    srcWrappersDir: string,
): void {
    // Copy over artifacts
    srcContracts.forEach(contract => {
        const srcPath = `${srcArtifactsDir}/${contract}.json`;
        mkdirp.sync(srcArtifactsDir);
        fs.copyFileSync(`${testArtifactsDir}/${contract}.json`, srcPath);
    });

    // Copy over wrappers
    srcContracts.forEach(contract => {
        const wrapperFileName = makeOutputFileName(contract);
        const srcPath = `${srcWrappersDir}/${wrapperFileName}.ts`;
        mkdirp.sync(srcWrappersDir);
        fs.copyFileSync(`${testWrappersDir}/${wrapperFileName}.ts`, srcPath);
    });
}

async function regenerateContractPackageAsync(
    testContracts: string[],
    srcContracts: string[],
    testArtifactsDir: string,
    srcArtifactsDir: string,
    testWrappersDir: string,
    srcWrappersDir: string,
): Promise<void> {
    const compilerJSON = readJSONFile<CompilerOptions>('compiler.json');

    const packageDir = process.cwd();
    const testContractsDir = compilerJSON.contractsDir || DEFAULT_CONTRACTS_DIR;
    const prettierConfig = await prettier.resolveConfig(packageDir);
    generateCompilerJSONContractsList(testContracts, testContractsDir, prettierConfig);
    generateArtifactsTs(testContracts, testArtifactsDir, TEST_ARTIFACTS_TS_FILE_PATH, prettierConfig);
    generateArtifactsTs(srcContracts, srcArtifactsDir, SRC_ARTIFACTS_TS_FILE_PATH, prettierConfig);
    generateWrappersTs(testContracts, testWrappersDir, TEST_WRAPPERS_TS_FILE_PATH, prettierConfig);
    generateWrappersTs(srcContracts, srcWrappersDir, SRC_WRAPPERS_TS_FILE_PATH, prettierConfig);
    generateTsConfigJSONFilesList(testContracts, testArtifactsDir, srcContracts, srcArtifactsDir, prettierConfig);
    generatePackageJSONABIConfig(testContracts, 'abis', testArtifactsDir, prettierConfig);
}

function convertToTopLevelDir(name: string, aPath: string): string {
    let finalPath = aPath;
    const hasDotPrefix = aPath.startsWith('./');
    if (hasDotPrefix) {
        finalPath = aPath.substr(2);
    }
    const segments = finalPath.split('/');
    if (segments.length === 0) {
        throw new Error(`Cannot have empty path for ${name}`);
    }
    if (segments.length === 1) {
        return aPath;
    }
    segments.shift();
    return `${hasDotPrefix ? './' : ''}${segments.join('/')}`;
}

function generateCompilerJSONContractsList(
    contracts: string[],
    contractsDir: string,
    prettierConfig: prettier.Options | null,
): void {
    const COMPILER_JSON_FILE_PATH = 'compiler.json';
    const compilerJSON = readJSONFile<CompilerOptions>(COMPILER_JSON_FILE_PATH);
    if (compilerJSON.contracts !== undefined && compilerJSON.contracts !== ALL_CONTRACTS_IDENTIFIER) {
        compilerJSON.contracts = _.map(contracts, contract => {
            if (contract.endsWith(SOLIDITY_EXTENSION)) {
                // If it's already a relative path - NO-OP.
                return contract;
            } else {
                // If it's just a contract name - resolve it and rewrite.
                return new NameResolver(contractsDir).resolve(contract).path;
            }
        });
        compilerJSON.contracts = _.sortBy(compilerJSON.contracts);
    }
    const compilerJSONString = JSON.stringify(compilerJSON);
    const formattedCompilerJSON = prettier.format(compilerJSONString, {
        ...prettierConfig,
        filepath: COMPILER_JSON_FILE_PATH,
    });
    fs.writeFileSync(COMPILER_JSON_FILE_PATH, formattedCompilerJSON);
}

function generateArtifactsTs(
    contracts: string[],
    artifactsDir: string,
    artifactsTsFilePath: string,
    prettierConfig: prettier.Options | null,
): void {
    const imports = _.map(contracts, contract => {
        const contractName = path.basename(contract, SOLIDITY_EXTENSION);
        const importPath = path.join('..', artifactsDir, `${contractName}.json`);
        return `import * as ${contractName} from '${importPath}';`;
    });
    const sortedImports = _.sortBy(imports, _import => _import.toLowerCase());
    const artifacts = _.map(contracts, contract => {
        const contractName = path.basename(contract, SOLIDITY_EXTENSION);
        if (contractName === 'ZRXToken') {
            // HACK(albrow): "as any" hack still required here because ZRXToken does not
            // conform to the v2 artifact type.
            return `${contractName}: (${contractName} as any) as ContractArtifact,`;
        } else {
            return `${contractName}: ${contractName} as ContractArtifact,`;
        }
    });
    const artifactsTs = `
    ${AUTO_GENERATED_BANNER}
    import { ContractArtifact } from 'ethereum-types';

    ${sortedImports.join('\n')}
    export const artifacts = {${artifacts.join('\n')}};
    `;
    const formattedArtifactsTs = prettier.format(artifactsTs, { ...prettierConfig, filepath: artifactsTsFilePath });
    fs.writeFileSync(artifactsTsFilePath, formattedArtifactsTs);
}

function generateWrappersTs(
    contracts: string[],
    wrappersDir: string,
    wrappersTsFilePath: string,
    prettierConfig: prettier.Options | null,
): void {
    const imports = _.map(contracts, contract => {
        const contractName = path.basename(contract, SOLIDITY_EXTENSION);
        const outputFileName = makeOutputFileName(contractName);
        const exportPath = path.join('..', wrappersDir, outputFileName);
        return `export * from '${exportPath}';`;
    });
    const sortedImports = _.sortBy(imports);
    const wrappersTs = `
    ${AUTO_GENERATED_BANNER}
    ${sortedImports.join('\n')}
    `;
    const formattedArtifactsTs = prettier.format(wrappersTs, { ...prettierConfig, filepath: wrappersTsFilePath });
    fs.writeFileSync(wrappersTsFilePath, formattedArtifactsTs);
}

function generateTsConfigJSONFilesList(
    testContracts: string[],
    testArtifactsDir: string,
    srcContracts: string[],
    srcArtifactsDir: string,
    prettierConfig: prettier.Options | null,
): void {
    const TS_CONFIG_FILE_PATH = 'tsconfig.json';
    const tsConfig = readJSONFile<any>(TS_CONFIG_FILE_PATH);
    const testFiles = _.map(testContracts, contract => {
        const contractName = path.basename(contract, SOLIDITY_EXTENSION);
        const artifactPath = path.join(testArtifactsDir, `${contractName}.json`);
        return artifactPath;
    });
    const srcFiles = _.map(srcContracts, contract => {
        const contractName = path.basename(contract, SOLIDITY_EXTENSION);
        const artifactPath = path.join(srcArtifactsDir, `${contractName}.json`);
        return artifactPath;
    });
    tsConfig.files = [...testFiles, ...srcFiles];
    tsConfig.files = _.sortBy(tsConfig.files);
    const tsConfigString = JSON.stringify(tsConfig);
    const formattedTsConfig = prettier.format(tsConfigString, { ...prettierConfig, filepath: TS_CONFIG_FILE_PATH });
    fs.writeFileSync(TS_CONFIG_FILE_PATH, formattedTsConfig);
}

function generatePackageJSONABIConfig(
    contracts: string[],
    configName: string,
    artifactsDir: string,
    prettierConfig: prettier.Options | null,
): void {
    let packageJSON = readJSONFile<PackageJSON>('package.json');
    const contractNames = _.map(contracts, contract => {
        const contractName = path.basename(contract, SOLIDITY_EXTENSION);
        return contractName;
    });
    const sortedContractNames = _.sortBy(contractNames);
    packageJSON = {
        ...packageJSON,
        config: {
            ...packageJSON.config,
            [`${configName}:comment`]: AUTO_GENERATED_BANNER_FOR_LISTS,
            [configName]: `${artifactsDir}/@(${sortedContractNames.join('|')}).json`,
        },
    };
    const PACKAGE_JSON_FILE_PATH = 'package.json';
    const packageJSONString = JSON.stringify(packageJSON);
    const formattedPackageJSON = prettier.format(packageJSONString, {
        ...prettierConfig,
        filepath: PACKAGE_JSON_FILE_PATH,
    });
    fs.writeFileSync(PACKAGE_JSON_FILE_PATH, formattedPackageJSON);
}

function makeOutputFileName(name: string): string {
    let fileName = toSnakeCase(name);
    // HACK: Snake case doesn't make a lot of sense for abbreviated names but we can't reliably detect abbreviations
    // so we special-case the abbreviations we use.
    fileName = fileName.replace('z_r_x', 'zrx').replace('e_r_c', 'erc');
    return fileName;
}

function readJSONFile<T>(filePath: string): T {
    const JSONString = fs.readFileSync(filePath, 'utf8');
    const parsed: T = JSON.parse(JSONString);
    return parsed;
}
