import * as t from '@babel/types';
import { ProtoField } from '@cosmology/types';
import { getDefaultTSTypeFromProtoType } from '../../types';
import { ToJSONMethod } from './index';
import { getFieldOptionalityForDefaults } from '../types';

const notUndefinedSetValue = (messageProp: string, objProp: string, expr: t.Expression) => {
    return t.expressionStatement(
        t.logicalExpression(
            '&&',
            t.binaryExpression(
                '!==',
                t.memberExpression(
                    t.identifier('message'),
                    t.identifier(messageProp)
                ),
                t.identifier('undefined')
            ),
            t.assignmentExpression(
                '=',
                t.memberExpression(
                    t.identifier('obj'),
                    t.identifier(objProp)
                ),
                expr
            )
        )
    );
}

const getPropNames = (field: ProtoField) => {
    const messageProp = field.name;
    const objProp = field.options?.json_name ?? field.name;
    return {
        messageProp,
        objProp
    }
}

export const toJSON = {

    //  message.sender !== undefined && (obj.sender = message.sender);
    identity(args: ToJSONMethod) {
        const { messageProp, objProp } = getPropNames(args.field);
        return notUndefinedSetValue(messageProp, objProp, t.memberExpression(
            t.identifier('message'),
            t.identifier(messageProp)
        ));
    },

    string(args: ToJSONMethod) {
        return toJSON.identity(args);
    },
    double(args: ToJSONMethod) {
        return toJSON.identity(args);
    },
    float(args: ToJSONMethod) {
        return toJSON.identity(args);
    },
    bool(args: ToJSONMethod) {
        return toJSON.identity(args);
    },

    // message.maxDepth !== undefined && (obj.maxDepth = Math.round(message.maxDepth));
    number(args: ToJSONMethod) {
        const { messageProp, objProp } = getPropNames(args.field);
        return notUndefinedSetValue(
            messageProp,
            objProp,
            t.callExpression(
                t.memberExpression(
                    t.identifier('Math'),
                    t.identifier('round')
                ),
                [
                    t.memberExpression(
                        t.identifier('message'),
                        t.identifier(messageProp)
                    )
                ]
            )
        );
    },
    // message.maxDepth !== undefined && (obj.maxDepth = Math.round(message.maxDepth));
    int32(args: ToJSONMethod) {
        return toJSON.number(args);
    },

    uint32(args: ToJSONMethod) {
        return toJSON.number(args);
    },

    sint32(args: ToJSONMethod) {
        return toJSON.number(args);
    },
    fixed32(args: ToJSONMethod) {
        return toJSON.number(args);
    },
    sfixed32(args: ToJSONMethod) {
        return toJSON.number(args);
    },

    // message.poolId !== undefined && (obj.poolId = (message.poolId || Long.UZERO).toString());
    // message.poolId !== undefined && (obj.poolId = (message.poolId || undefined).toString());
    long(args: ToJSONMethod) {
        const { messageProp, objProp } = getPropNames(args.field);

        const isOptional = getFieldOptionalityForDefaults(args.context, args.field, args.isOneOf);

        if(isOptional){
          return t.ifStatement(
            t.binaryExpression(
              "!==",
              t.memberExpression(t.identifier("message"), t.identifier(messageProp)),
              t.identifier("undefined")
            ),
            t.blockStatement([
              t.expressionStatement(
                t.assignmentExpression(
                  "=",
                  t.memberExpression(t.identifier("obj"), t.identifier(objProp)),
                  t.callExpression(
                    t.memberExpression(
                      t.memberExpression(
                        t.identifier("message"),
                        t.identifier(messageProp)
                      ),
                      t.identifier("toString")
                    ),
                    []
                  )
                )
              ),
            ])
          );
        } else {
          return notUndefinedSetValue(
            messageProp,
            objProp,
            t.callExpression(
                t.memberExpression(
                    t.logicalExpression(
                        '||',
                        t.memberExpression(
                            t.identifier('message'),
                            t.identifier(messageProp)
                        ),
                        getDefaultTSTypeFromProtoType(args.context, args.field, args.isOneOf)
                    ),
                    t.identifier('toString')
                ),
                []
            )
          );
        }
    },

    int64(args: ToJSONMethod) {
        return toJSON.long(args);
    },
    uint64(args: ToJSONMethod) {
        return toJSON.long(args);
    },
    sint64(args: ToJSONMethod) {
        return toJSON.long(args);
    },
    fixed64(args: ToJSONMethod) {
        return toJSON.long(args);
    },
    sfixed64(args: ToJSONMethod) {
        return toJSON.long(args);
    },

    // message.signDoc !== undefined && (obj.signDoc = message.signDoc ? SignDocDirectAux.toJSON(message.signDoc) : undefined);
    type(args: ToJSONMethod) {
        let name = args.context.getTypeName(args.field);
        const { messageProp, objProp } = getPropNames(args.field);

        if (
          !args.context.options.aminoEncoding.useLegacyInlineEncoding &&
          args.context.options.interfaces.enabled &&
          args.context.options.interfaces?.useGlobalDecoderRegistry &&
          args.field.type === 'google.protobuf.Any' &&
          args.field.options['(cosmos_proto.accepts_interface)']
        ) {
          name = 'GlobalDecoderRegistry';
        }

        // TODO isn't the nested conditional a waste? (using ts-proto as reference)
        // maybe null is OK?
        return notUndefinedSetValue(messageProp, objProp, t.conditionalExpression(
            t.memberExpression(
                t.identifier('message'),
                t.identifier(messageProp)
            ),
            t.callExpression(
                t.memberExpression(
                    t.identifier(name),
                    t.identifier('toJSON')
                ),
                [
                    t.memberExpression(
                        t.identifier('message'),
                        t.identifier(messageProp)
                    )
                ]
            ),
            t.identifier('undefined')
        ));
    },

    // message.mode !== undefined && (obj.mode = signModeToJSON(message.mode));
    enum(args: ToJSONMethod) {
        const { messageProp, objProp } = getPropNames(args.field);
        const enumFuncName = args.context.getToEnum(args.field);
        return notUndefinedSetValue(messageProp, objProp, t.callExpression(
            t.identifier(enumFuncName),
            [
                t.memberExpression(
                    t.identifier('message'),
                    t.identifier(messageProp)
                )
            ]
        ));
    },

    // TODO again, another ts-proto reference that does not necessarily make sense
    // message.queryData !== undefined && (obj.queryData = base64FromBytes(message.queryData !== undefined ? message.queryData : new Uint8Array()));
    // message.queryData !== undefined && (obj.queryData = base64FromBytes(message.queryData !== undefined ? message.queryData : undefined));
    bytes(args: ToJSONMethod) {
        args.context.addUtil('base64FromBytes');
        const { messageProp, objProp } = getPropNames(args.field);

        let expr;
        if (args.isOptional) {
            // message.bytesValue !== undefined && (obj.bytesValue = message.bytesValue !== undefined ? base64FromBytes(message.bytesValue) : undefined);

            expr = t.conditionalExpression(
                t.binaryExpression(
                    '!==',
                    t.memberExpression(
                        t.identifier('message'),
                        t.identifier(messageProp)
                    ),
                    t.identifier('undefined')
                ),
                t.callExpression(
                    t.identifier('base64FromBytes'),
                    [
                        t.memberExpression(
                            t.identifier('message'),
                            t.identifier(messageProp)
                        )
                    ]
                ),
                t.identifier('undefined')
            );
        } else {
            // message.queryData !== undefined && (obj.queryData = base64FromBytes(message.queryData !== undefined ? message.queryData : new Uint8Array()));
            expr = t.callExpression(
                t.identifier('base64FromBytes'),
                [
                    t.conditionalExpression(
                        t.binaryExpression(
                            '!==',
                            t.memberExpression(
                                t.identifier('message'),
                                t.identifier(messageProp)
                            ),
                            t.identifier('undefined')
                        ),
                        t.memberExpression(
                            t.identifier('message'),
                            t.identifier(messageProp)
                        ),
                        getDefaultTSTypeFromProtoType(args.context, args.field, args.isOneOf)
                    )
                ]
            )
        }
        return notUndefinedSetValue(messageProp, objProp, expr);
    },

    // message.period !== undefined && (obj.period = message.period);

    duration(args: ToJSONMethod) {
        const durationFormat = args.context.pluginValue('prototypes.typingsFormat.duration');
        switch (durationFormat) {
            case 'string':
                return toJSON.durationString(args);
            case 'duration':
            default:
                return toJSON.type(args);
        }
    },

    durationString(args: ToJSONMethod) {
        return toJSON.identity(args);
    },

    timestamp(args: ToJSONMethod) {
        const timestampFormat = args.context.pluginValue('prototypes.typingsFormat.timestamp')
        switch (timestampFormat) {
            case 'timestamp':
                return toJSON.timestampTimestamp(args);
            case 'date':
            default:
                return toJSON.timestampDate(args);
        }
    },

    // message.periodReset !== undefined && (obj.periodReset = fromTimestamp(message.periodReset).toISOString());

    timestampTimestamp(args: ToJSONMethod) {
        args.context.addUtil('fromTimestamp');
        const { messageProp, objProp } = getPropNames(args.field);
        return notUndefinedSetValue(messageProp, objProp, t.callExpression(
            t.memberExpression(
                t.callExpression(
                    t.identifier('fromTimestamp'), [
                    t.memberExpression(
                        t.identifier('message'),
                        t.identifier(messageProp)
                    )
                ]),
                t.identifier('toISOString')
            ),
            []
        ));
    },

    // message.periodReset !== undefined && (obj.periodReset = message.periodReset.toISOString());

    timestampDate(args: ToJSONMethod) {
        const { messageProp, objProp } = getPropNames(args.field);
        return notUndefinedSetValue(messageProp, objProp, t.callExpression(
            t.memberExpression(
                t.memberExpression(
                    t.identifier('message'),
                    t.identifier(messageProp)
                ),
                t.identifier('toISOString')
            ),
            []
        ));
    },

    // obj.labels = {};

    //   if (message.labels) {
    //     Object.entries(message.labels).forEach(([k, v]) => {
    //       obj.labels[k] = v;
    //     });
    //   }


    // obj.typeMap = {};

    // if (message.typeMap) {
    //   Object.entries(message.typeMap).forEach(([k, v]) => {
    //     obj.typeMap[k] = Type.toJSON(v);
    //   });
    // }

    keyHash(args: ToJSONMethod) {

        const { messageProp, objProp } = getPropNames(args.field);
        const keyType = args.field.keyType;
        const valueType = args.field.parsedType.name;

        let toJSON = null;
        switch (valueType) {
            case 'string':
                toJSON = t.identifier('v')
                break;
            case 'uint32':
            case 'int32':
                toJSON = t.callExpression(
                    t.memberExpression(
                        t.identifier('Math'),
                        t.identifier('round')
                    ),
                    [
                        t.identifier('v')
                    ]
                )
                break;
            case 'int64':
            case 'uint64':
                toJSON = t.callExpression(
                    t.memberExpression(
                        t.identifier('v'),
                        t.identifier('toString')
                    ),
                    []
                )
                break;
            default:
                toJSON = t.callExpression(
                    t.memberExpression(
                        t.identifier(valueType),
                        t.identifier('toJSON')
                    ),
                    [
                        t.identifier('v')
                    ]
                )
        }


        return [
            t.expressionStatement(
                t.assignmentExpression(
                    '=',
                    t.memberExpression(
                        t.identifier('obj'),
                        t.identifier(objProp)
                    ),
                    t.objectExpression([])
                )
            ),
            //
            t.ifStatement(
                t.memberExpression(
                    t.identifier('message'),
                    t.identifier(messageProp)
                ),
                t.blockStatement([
                    t.expressionStatement(
                        t.callExpression(
                            t.memberExpression(
                                t.callExpression(
                                    t.memberExpression(
                                        t.identifier('Object'),
                                        t.identifier('entries')
                                    ),
                                    [
                                        t.memberExpression(
                                            t.identifier('message'),
                                            t.identifier(messageProp)
                                        )
                                    ]
                                ),
                                t.identifier('forEach')
                            ),
                            [
                                t.arrowFunctionExpression(
                                    [
                                        t.arrayPattern(
                                            [
                                                t.identifier('k'),
                                                t.identifier('v')
                                            ]
                                        )
                                    ],
                                    t.blockStatement([
                                        t.expressionStatement(
                                            t.assignmentExpression(
                                                '=',
                                                t.memberExpression(
                                                    t.memberExpression(
                                                        t.identifier('obj'),
                                                        t.identifier(objProp)
                                                    ),
                                                    t.identifier('k'),
                                                    true
                                                ),
                                                toJSON
                                            )
                                        )
                                    ])
                                )
                            ]
                        )
                    )
                ])
            )
        ]
    },

    // if (message.codeIds) {
    //     obj.codeIds = message.codeIds.map(e => (e || Long.UZERO).toString());
    // } else {
    //     obj.codeIds = [];
    // }

    array(args: ToJSONMethod, expr: t.Expression) {
        const { messageProp, objProp } = getPropNames(args.field);
        return t.ifStatement(
            t.memberExpression(
                t.identifier('message'),
                t.identifier(messageProp)
            ),
            t.blockStatement([
                t.expressionStatement(
                    t.assignmentExpression(
                        '=',
                        t.memberExpression(
                            t.identifier('obj'),
                            t.identifier(objProp)
                        ),
                        t.callExpression(
                            t.memberExpression(
                                t.memberExpression(
                                    t.identifier('message'),
                                    t.identifier(messageProp)
                                ),
                                t.identifier('map')
                            ),
                            [
                                t.arrowFunctionExpression(
                                    [
                                        t.identifier('e')
                                    ],
                                    expr
                                )
                            ]
                        )
                    )
                )
            ]),
            t.blockStatement([
                t.expressionStatement(
                    t.assignmentExpression(
                        '=',
                        t.memberExpression(
                            t.identifier('obj'),
                            t.identifier(objProp)
                        ),
                        t.arrayExpression([])
                    )
                )
            ])
        );
    }

};

export const arrayTypes = {

    identity() {
        return t.identifier('e');
    },


    // if (message.overloadId) {
    //     obj.overloadId = message.overloadId.map(e => e);
    // } else {
    //     obj.overloadId = [];
    // }

    string() {
        return arrayTypes.identity();
    },
    double() {
        return arrayTypes.identity();
    },
    float() {
        return arrayTypes.identity();
    },
    bool() {
        return arrayTypes.identity();
    },

    //   if (message.lineOffsets) {
    //     obj.lineOffsets = message.lineOffsets.map(e => Math.round(e));
    //   } else {
    //     obj.lineOffsets = [];
    //   }

    number() {
        return t.callExpression(
            t.memberExpression(
                t.identifier('Math'),
                t.identifier('round')
            ),
            [
                t.identifier('e')
            ]
        )
    },

    int32() {
        return arrayTypes.number();
    },
    uint32() {
        return arrayTypes.number();
    },
    sint32() {
        return arrayTypes.number();
    },
    fixed32() {
        return arrayTypes.number();
    },
    sfixed32() {
        return arrayTypes.number();
    },


    // if (message.codeIds) {
    //     obj.codeIds = message.codeIds.map(e => (e || Long.UZERO).toString());
    // } else {
    //     obj.codeIds = [];
    // }

    long(args: ToJSONMethod) {
        return t.callExpression(
            t.memberExpression(
                t.logicalExpression(
                    '||',
                    t.identifier('e'),
                    getDefaultTSTypeFromProtoType(args.context, {
                        ...args.field,
                        rule: undefined, // so it's treated as type not an array...
                    }, args.isOneOf)
                ),
                t.identifier('toString')
            ),
            []
        )
    },

    int64(args: ToJSONMethod) {
        return arrayTypes.long(args);
    },
    uint64(args: ToJSONMethod) {
        return arrayTypes.long(args);
    },
    sint64(args: ToJSONMethod) {
        return arrayTypes.long(args);
    },
    fixed64(args: ToJSONMethod) {
        return arrayTypes.long(args);
    },
    sfixed64(args: ToJSONMethod) {
        return arrayTypes.long(args);
    },

    //   if (message.myBytesArray) {
    //     obj.myBytesArray = message.myBytesArray.map(e => base64FromBytes(e !== undefined ? e : new Uint8Array()));
    //   } else {
    //     obj.myBytesArray = [];
    //   }

    bytes(args: ToJSONMethod) {
        args.context.addUtil('base64FromBytes');
        return t.callExpression(
            t.identifier('base64FromBytes'),
            [
                t.conditionalExpression(
                    t.binaryExpression(
                        '!==',
                        t.identifier('e'),
                        t.identifier('undefined')
                    ),
                    t.identifier('e'),
                    getDefaultTSTypeFromProtoType(args.context, {
                        ...args.field,
                        rule: undefined, // so it's treated as type not an array...
                    }, args.isOneOf)
                )
            ]
        );
    },

    enum(args: ToJSONMethod) {
        const enumFuncName = args.context.getToEnum(args.field);
        return t.callExpression(
            t.identifier(enumFuncName),
            [
                t.identifier('e')
            ]
        );
    },

    // if (message.tokenInMaxs) {
    //     obj.tokenInMaxs = message.tokenInMaxs.map(e => e ? Coin.toJSON(e) : undefined);
    // } else {
    //     obj.tokenInMaxs = [];
    // }

    type(args: ToJSONMethod) {
        let name = args.context.getTypeName(args.field);

        if (
          !args.context.options.aminoEncoding.useLegacyInlineEncoding &&
          args.context.options.interfaces.enabled &&
          args.context.options.interfaces?.useGlobalDecoderRegistry &&
          args.field.type === 'google.protobuf.Any' &&
          args.field.options['(cosmos_proto.accepts_interface)']
        ) {
          name = 'GlobalDecoderRegistry';
        }

        return t.conditionalExpression(
            t.identifier('e'),
            t.callExpression(
                t.memberExpression(
                    t.identifier(name),
                    t.identifier('toJSON')
                ),
                [
                    t.identifier('e')
                ]
            ),
            t.identifier('undefined')
        );
    }
}

