import { SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN, SolanaErrorCode } from './codes';
import { encodeContextObject } from './context';
import { SolanaErrorMessages } from './messages';

const INSTRUCTION_ERROR_RANGE_SIZE = 1000;

const enum StateType {
    EscapeSequence,
    Text,
    Variable,
}
type State = Readonly<{
    [START_INDEX]: number;
    [TYPE]: StateType;
}>;
const START_INDEX = 'i';
const TYPE = 't';

export function getHumanReadableErrorMessage<TErrorCode extends SolanaErrorCode>(
    code: TErrorCode,
    context: object = {},
): string {
    const messageFormatString = SolanaErrorMessages[code];
    if (messageFormatString.length === 0) {
        return '';
    }
    let state: State;
    function commitStateUpTo(endIndex?: number) {
        if (state[TYPE] === StateType.Variable) {
            const variableName = messageFormatString.slice(state[START_INDEX] + 1, endIndex);

            fragments.push(
                variableName in context
                    ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
                      `${context[variableName as keyof typeof context]}`
                    : `$${variableName}`,
            );
        } else if (state[TYPE] === StateType.Text) {
            fragments.push(messageFormatString.slice(state[START_INDEX], endIndex));
        }
    }
    const fragments: string[] = [];
    messageFormatString.split('').forEach((char, ii) => {
        if (ii === 0) {
            state = {
                [START_INDEX]: 0,
                [TYPE]:
                    messageFormatString[0] === '\\'
                        ? StateType.EscapeSequence
                        : messageFormatString[0] === '$'
                          ? StateType.Variable
                          : StateType.Text,
            };
            return;
        }
        let nextState;
        switch (state[TYPE]) {
            case StateType.EscapeSequence:
                nextState = { [START_INDEX]: ii, [TYPE]: StateType.Text };
                break;
            case StateType.Text:
                if (char === '\\') {
                    nextState = { [START_INDEX]: ii, [TYPE]: StateType.EscapeSequence };
                } else if (char === '$') {
                    nextState = { [START_INDEX]: ii, [TYPE]: StateType.Variable };
                }
                break;
            case StateType.Variable:
                if (char === '\\') {
                    nextState = { [START_INDEX]: ii, [TYPE]: StateType.EscapeSequence };
                } else if (char === '$') {
                    nextState = { [START_INDEX]: ii, [TYPE]: StateType.Variable };
                } else if (!char.match(/\w/)) {
                    nextState = { [START_INDEX]: ii, [TYPE]: StateType.Text };
                }
                break;
        }
        if (nextState) {
            if (state !== nextState) {
                commitStateUpTo(ii);
            }
            state = nextState;
        }
    });
    commitStateUpTo();
    let message = fragments.join('');
    if (
        code >= SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN &&
        code < SOLANA_ERROR__INSTRUCTION_ERROR__UNKNOWN + INSTRUCTION_ERROR_RANGE_SIZE &&
        'index' in context
    ) {
        message += ` (instruction #${(context as { index: number }).index + 1})`;
    }
    return message;
}

export function getErrorMessage<TErrorCode extends SolanaErrorCode>(
    code: TErrorCode,
    context: Record<string, unknown> = {},
): string {
    if (__DEV__) {
        return getHumanReadableErrorMessage(code, context);
    } else {
        let decodingAdviceMessage = `Solana error #${code}; Decode this error by running \`npx @solana/errors decode -- ${code}`;
        if (Object.keys(context).length) {
            /**
             * DANGER: Be sure that the shell command is escaped in such a way that makes it
             *         impossible for someone to craft malicious context values that would result in
             *         an exploit against anyone who bindly copy/pastes it into their terminal.
             */
            decodingAdviceMessage += ` '${encodeContextObject(context)}'`;
        }
        return `${decodingAdviceMessage}\``;
    }
}
