import * as t from '@babel/types';
import { ProtoField, ProtoType } from '@cosmology/types';
import { arrowFunctionExpression } from '../../../utils';
import { AminoParseContext } from '../../context';
import { protoFieldsToArray } from '../utils';
import { arrayTypes, fromAmino } from './utils';
import { getFieldOptionality, getOneOfs } from '../../proto';

const needsImplementation = (name: string, field: ProtoField) => {
    throw new Error(`need to implement fromAmino (${field.type} rules[${field.rule}] name[${name}])`);
}

export interface FromAminoParseField {
    context: AminoParseContext;
    field: ProtoField;
    currentProtoPath: string;
    scope: string[];
    fieldPath: ProtoField[];
    nested: number;
    isOptional: boolean;
}

export const fromAminoParseField = ({
    context,
    field,
    currentProtoPath,
    scope: previousScope,
    fieldPath: previousFieldPath,
    nested,
    isOptional
}: FromAminoParseField) => {

    const scope = [field.name, ...previousScope];
    const fieldPath = [field, ...previousFieldPath];

    const args = {
        context,
        field,
        currentProtoPath,
        scope,
        fieldPath,
        nested,
        isOptional
    };

    // arrays
    if (field.rule === 'repeated') {
        switch (field.type) {
            case 'string':
                return fromAmino.string(args);

            case 'int64':
            case 'sint64':
            case 'uint64':
            case 'fixed64':
            case 'sfixed64':
                return fromAmino.scalarArray(args, arrayTypes.long);

            case 'double':
            case 'float':
            case 'int32':
            case 'sint32':
            case 'uint32':
            case 'fixed32':
            case 'sfixed32':
            case 'bool':
            case 'bytes':
                return fromAmino.defaultType(args);
        }

        switch (field.parsedType.type) {
            case 'Type':
                return fromAmino.typeArray(args);
            case 'Enum':
                return fromAmino.enumArray(args);
            case 'cosmos.base.v1beta1.Coin':
                return fromAmino.arrayFrom(args);
        }

        return needsImplementation(field.name, field);
    }

    // casting special types
    if (field.type === 'google.protobuf.Any') {
        switch (field.options?.['(cosmos_proto.accepts_interface)']) {
            case 'cosmos.crypto.PubKey':
                return fromAmino.pubkey(args);
        }
    }

    // special types...
    switch (field.type) {
        case 'Timestamp':
        case 'google.protobuf.Timestamp':
            return fromAmino.defaultType(args)

        // TODO check can we just
        // make pieces optional and avoid hard-coding this type?
        case 'ibc.core.client.v1.Height':
        case 'Height':
            return fromAmino.height(args);

        case 'Duration':
        case 'google.protobuf.Duration':
            return fromAmino.duration(args);

        default:
    }

    // Types/Enums
    switch (field.parsedType.type) {
        case 'Type':
            return fromAmino.type(args);

        case 'Enum':
            return fromAmino.enum(args);
    }

    if (field.type === 'bytes') {
        // bytes [RawContractMessage]
        if (field.options?.['(gogoproto.casttype)'] === 'RawContractMessage') {
            return fromAmino.rawBytes(args);
        }
        // bytes [WASMByteCode]
        // TODO use a better option for this in proto source
        if (field.options?.['(gogoproto.customname)'] === 'WASMByteCode') {
            return fromAmino.wasmByteCode(args);
        }
    }

    // scalar types...
    switch (field.type) {
        case 'string':
            return fromAmino.string(args);
        case 'int64':
        case 'sint64':
        case 'uint64':
        case 'fixed64':
        case 'sfixed64':
            return fromAmino.long(args);
        case 'double':
        case 'float':
        case 'int32':
        case 'sint32':
        case 'uint32':
        case 'fixed32':
        case 'sfixed32':
        case 'bool':
            return fromAmino.defaultType(args)
        case 'bytes':
            return fromAmino.defaultType(args)

        default:
            return fromAmino.defaultType(args)
    }
};

interface fromAminoJSON {
    context: AminoParseContext;
    proto: ProtoType;
}
export const fromAminoJsonMethod = ({
    context,
    proto
}: fromAminoJSON) => {

    const fromAminoParams = t.objectPattern(
        Object.keys(proto.fields).map((field) => t.objectProperty(
            t.identifier(context.aminoCaseField(proto.fields[field])),
            t.identifier(context.aminoCaseField(proto.fields[field])),
            false,
            true)
        )
    );
    fromAminoParams.typeAnnotation = t.tsTypeAnnotation(t.tsIndexedAccessType(
        t.tsTypeReference(t.identifier(proto.name + 'AminoType')),
        t.tsLiteralType(t.stringLiteral('value'))
    ));

    const oneOfs = getOneOfs(proto);
    const fields = protoFieldsToArray(proto).map((field) => {
        const isOneOf = oneOfs.includes(field.name);
        const isOptional = getFieldOptionality(context, field, isOneOf);

        const aminoField = fromAminoParseField({
            context,
            field,
            currentProtoPath: context.ref.filename,
            scope: [],
            fieldPath: [],
            nested: 0,
            isOptional
        });
        return {
            ctx: context,
            field: aminoField
        }
    });

    const ctxs = fields.map(({ ctx }) => ctx);
    ctxs.forEach(ctx => {
        // console.log('imports, ', ctx.imports)
    })

    return arrowFunctionExpression(
        [
            fromAminoParams
        ],
        t.blockStatement([
            t.returnStatement(
                t.objectExpression(
                    fields.map(({ field }) => field)
                )
            )
        ]),
        t.tsTypeAnnotation(t.tsTypeReference(t.identifier(proto.name)))
    );
};

