import { constants, TraceInfo } from '@0x/sol-tracing-utils';
import { logUtils } from '@0x/utils';
import { OpCode } from 'ethereum-types';
import { stripHexPrefix } from 'ethereumjs-util';
import * as _ from 'lodash';

const ZERO_BYTE_CALL_DATA_COST = 4;
const NON_ZERO_BYTE_CALL_DATA_COST = 68;
const WORD_SIZE = 32;
const G_MEMORY = 3;
const G_QUAD_COEF = 512;
const HEX_BASE = 16;
const G_COPY = 3;

export const costUtils = {
    reportCallDataCost(traceInfo: TraceInfo): number {
        if (traceInfo.dataIfExists === undefined) {
            // No call data to report
            return 0;
        }
        const callData = traceInfo.dataIfExists;
        const callDataBuf = Buffer.from(stripHexPrefix(callData), 'hex');
        const { true: zeroBytesCountIfExist, false: nonZeroBytesCountIfExist } = _.countBy(
            callDataBuf,
            byte => byte === 0,
        );
        const zeroBytesCost = (zeroBytesCountIfExist || 0) * ZERO_BYTE_CALL_DATA_COST;
        const nonZeroBytesCost = (nonZeroBytesCountIfExist || 0) * NON_ZERO_BYTE_CALL_DATA_COST;
        const callDataCost = zeroBytesCost + nonZeroBytesCost;
        logUtils.header('Call data breakdown', '-');
        logUtils.table({
            'call data size (bytes)': callData.length,
            callDataCost,
            zeroBytesCost,
            nonZeroBytesCost,
            zeroBytesCountIfExist,
            nonZeroBytesCountIfExist,
        });
        return callDataCost;
    },
    reportMemoryCost(traceInfo: TraceInfo): number {
        const structLogs = traceInfo.trace.structLogs;
        const MEMORY_OPCODES = [OpCode.MLoad, OpCode.MStore, OpCode.MStore8];
        const CALL_DATA_OPCODES = [OpCode.CallDataCopy];
        const memoryLogs = _.filter(structLogs, structLog =>
            _.includes([...MEMORY_OPCODES, ...CALL_DATA_OPCODES], structLog.op),
        );
        const memoryLocationsAccessed = _.map(memoryLogs, structLog => {
            if (_.includes(CALL_DATA_OPCODES, structLog.op)) {
                const memoryOffsetStackOffset = constants.opCodeToParamToStackOffset[structLog.op as any].memoryOffset;
                const lengthStackOffset = constants.opCodeToParamToStackOffset[structLog.op as any].length;
                const memOffset = parseInt(
                    structLog.stack[structLog.stack.length - memoryOffsetStackOffset - 1],
                    HEX_BASE,
                );
                const length = parseInt(structLog.stack[structLog.stack.length - lengthStackOffset - 1], HEX_BASE);
                return memOffset + length;
            } else {
                const memoryLocationStackOffset = constants.opCodeToParamToStackOffset[structLog.op].offset;
                return parseInt(structLog.stack[structLog.stack.length - memoryLocationStackOffset - 1], HEX_BASE);
            }
        });
        const highestMemoryLocationAccessed = _.max(memoryLocationsAccessed);
        return costUtils._printMemoryCost(highestMemoryLocationAccessed);
    },
    reportCopyingCost(traceInfo: TraceInfo): number {
        const structLogs = traceInfo.trace.structLogs;
        const COPY_OPCODES = [OpCode.CallDataCopy];
        const copyLogs = _.filter(structLogs, structLog => _.includes(COPY_OPCODES, structLog.op));
        const copyCosts = _.map(copyLogs, structLog => {
            const lengthStackOffset = constants.opCodeToParamToStackOffset[structLog.op as any].length;
            const length = parseInt(structLog.stack[structLog.stack.length - lengthStackOffset - 1], HEX_BASE);
            return Math.ceil(length / WORD_SIZE) * G_COPY;
        });
        return _.sum(copyCosts);
    },
    reportOpcodesCost(traceInfo: TraceInfo): number {
        const structLogs = traceInfo.trace.structLogs;
        const gasCosts = _.map(structLogs, structLog => structLog.gasCost);
        const gasCost = _.sum(gasCosts);
        return gasCost;
    },
    _printMemoryCost(highestMemoryLocationAccessed?: number): number {
        if (highestMemoryLocationAccessed === undefined) {
            return 0;
        }
        const memoryWordsUsed = Math.ceil((highestMemoryLocationAccessed + WORD_SIZE) / WORD_SIZE);
        const linearMemoryCost = G_MEMORY * memoryWordsUsed;
        const quadraticMemoryCost = Math.floor((memoryWordsUsed * memoryWordsUsed) / G_QUAD_COEF);
        const memoryCost = linearMemoryCost + quadraticMemoryCost;
        logUtils.header('Memory breakdown', '-');
        logUtils.table({
            'memoryCost = linearMemoryCost + quadraticMemoryCost': memoryCost,
            linearMemoryCost,
            quadraticMemoryCost,
            highestMemoryLocationAccessed,
            memoryWordsUsed,
        });
        return memoryCost;
    },
};
