/*
 * Copyright 2021 Fluence Labs Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { CallServiceResult } from '@fluencelabs/avm-runner-interface';
import { encode, decode } from 'bs58';
import { sha256 } from 'multiformats/hashes/sha2';
import { ResultCodes } from '../commonTypes';
import { jsonify } from '../utils';
import Buffer from '../Buffer';

const success = (result: any): CallServiceResult => {
    return {
        result: result,
        retCode: ResultCodes.success,
    };
};

const error = (error: string): CallServiceResult => {
    return {
        result: error,
        retCode: ResultCodes.error,
    };
};

const errorNotImpl = (methodName: string) => {
    return error(`The JS implementation of Peer does not support "${methodName}"`);
};

export const builtInServices = {
    peer: {
        identify: (req) => {
            return errorNotImpl('peer.identify');
        },

        timestamp_ms: (req) => {
            return success(Date.now());
        },

        timestamp_sec: (req) => {
            return success(Math.floor(Date.now() / 1000));
        },

        is_connected: (req) => {
            return errorNotImpl('peer.is_connected');
        },

        connect: (req) => {
            return errorNotImpl('peer.connect');
        },

        get_contact: (req) => {
            return errorNotImpl('peer.get_contact');
        },

        timeout: (req) => {
            if (req.args.length !== 2) {
                return error('timeout accepts exactly two arguments: timeout duration in ms and a message string');
            }
            const durationMs = req.args[0];
            const message = req.args[1];

            return new Promise((resolve) => {
                setTimeout(() => {
                    const res = success(message);
                    resolve(res);
                }, durationMs);
            });
        },
    },

    kad: {
        neighborhood: (req) => {
            return errorNotImpl('kad.neighborhood');
        },

        merge: (req) => {
            return errorNotImpl('kad.merge');
        },
    },

    srv: {
        list: (req) => {
            return errorNotImpl('srv.list');
        },

        create: (req) => {
            return errorNotImpl('srv.create');
        },

        get_interface: (req) => {
            return errorNotImpl('srv.get_interface');
        },

        resolve_alias: (req) => {
            return errorNotImpl('srv.resolve_alias');
        },

        add_alias: (req) => {
            return errorNotImpl('srv.add_alias');
        },

        remove: (req) => {
            return errorNotImpl('srv.remove');
        },
    },

    dist: {
        add_module_from_vault: (req) => {
            return errorNotImpl('dist.add_module_from_vault');
        },

        add_module: (req) => {
            return errorNotImpl('dist.add_module');
        },

        add_blueprint: (req) => {
            return errorNotImpl('dist.add_blueprint');
        },

        make_module_config: (req) => {
            return errorNotImpl('dist.make_module_config');
        },

        load_module_config: (req) => {
            return errorNotImpl('dist.load_module_config');
        },

        default_module_config: (req) => {
            return errorNotImpl('dist.default_module_config');
        },

        make_blueprint: (req) => {
            return errorNotImpl('dist.make_blueprint');
        },

        load_blueprint: (req) => {
            return errorNotImpl('dist.load_blueprint');
        },

        list_modules: (req) => {
            return errorNotImpl('dist.list_modules');
        },

        get_module_interface: (req) => {
            return errorNotImpl('dist.get_module_interface');
        },

        list_blueprints: (req) => {
            return errorNotImpl('dist.list_blueprints');
        },
    },

    script: {
        add: (req) => {
            return errorNotImpl('script.add');
        },

        remove: (req) => {
            return errorNotImpl('script.remove');
        },

        list: (req) => {
            return errorNotImpl('script.list');
        },
    },

    op: {
        noop: (req) => {
            return success({});
        },

        array: (req) => {
            return success(req.args);
        },

        array_length: (req) => {
            if (req.args.length !== 1) {
                return error('array_length accepts exactly one argument, found: ' + req.args.length);
            } else {
                return success(req.args[0].length);
            }
        },

        identity: (req) => {
            if (req.args.length > 1) {
                return error(`identity accepts up to 1 arguments, received ${req.args.length} arguments`);
            } else {
                return success(req.args.length === 0 ? {} : req.args[0]);
            }
        },

        concat: (req) => {
            const incorrectArgIndices = req.args //
                .map((x, i) => [Array.isArray(x), i])
                .filter(([isArray, _]) => !isArray)
                .map(([_, index]) => index);

            if (incorrectArgIndices.length > 0) {
                const str = incorrectArgIndices.join(', ');
                return error(`All arguments of 'concat' must be arrays: arguments ${str} are not`);
            } else {
                return success([].concat.apply([], req.args));
            }
        },

        string_to_b58: (req) => {
            if (req.args.length !== 1) {
                return error('string_to_b58 accepts only one string argument');
            } else {
                return success(encode(new TextEncoder().encode(req.args[0])));
            }
        },

        string_from_b58: (req) => {
            if (req.args.length !== 1) {
                return error('string_from_b58 accepts only one string argument');
            } else {
                return success(new TextDecoder().decode(decode(req.args[0])));
            }
        },

        bytes_to_b58: (req) => {
            if (req.args.length !== 1 || !Array.isArray(req.args[0])) {
                return error('bytes_to_b58 accepts only single argument: array of numbers');
            } else {
                const argumentArray = req.args[0] as number[];
                return success(encode(new Uint8Array(argumentArray)));
            }
        },

        bytes_from_b58: (req) => {
            if (req.args.length !== 1) {
                return error('bytes_from_b58 accepts only one string argument');
            } else {
                return success(Array.from(decode(req.args[0])));
            }
        },

        sha256_string: async (req) => {
            if (req.args.length < 1 || req.args.length > 3) {
                return error('sha256_string accepts 1-3 arguments, found: ' + req.args.length);
            } else {
                const [input, digestOnly, asBytes] = req.args;
                const inBuffer = Buffer.from(input);
                const multihash = await sha256.digest(inBuffer);

                const outBytes = digestOnly ? multihash.digest : multihash.bytes;
                const res = asBytes ? Array.from(outBytes) : encode(outBytes);

                return success(res);
            }
        },

        concat_strings: (req) => {
            const res = ''.concat(...req.args);
            return success(res);
        },
    },

    debug: {
        stringify: (req) => {
            let out;

            if (req.args.length === 0) {
                out = '<empty argument list>';
            } else if (req.args.length === 1) {
                out = req.args[0];
            } else {
                out = req.args;
            }

            return success(jsonify(out));
        },
    },

    math: {
        add: (req) => {
            return errorNotImpl('math.add');
        },

        sub: (req) => {
            return errorNotImpl('math.sub');
        },

        mul: (req) => {
            return errorNotImpl('math.mul');
        },

        fmul: (req) => {
            return errorNotImpl('math.fmul');
        },

        div: (req) => {
            return errorNotImpl('math.div');
        },

        rem: (req) => {
            return errorNotImpl('math.rem');
        },

        pow: (req) => {
            return errorNotImpl('math.pow');
        },

        log: (req) => {
            return errorNotImpl('math.log');
        },
    },

    cmp: {
        gt: (req) => {
            return errorNotImpl('cmp.gt');
        },

        gte: (req) => {
            return errorNotImpl('cmp.gte');
        },

        lt: (req) => {
            return errorNotImpl('cmp.lt');
        },

        lte: (req) => {
            return errorNotImpl('cmp.lte');
        },

        cmp: (req) => {
            return errorNotImpl('cmp.cmp');
        },
    },

    array: {
        sum: (req) => {
            return errorNotImpl('array.sum');
        },

        dedup: (req) => {
            return errorNotImpl('array.dedup');
        },

        intersect: (req) => {
            return errorNotImpl('array.intersect');
        },

        diff: (req) => {
            return errorNotImpl('array.diff');
        },

        sdiff: (req) => {
            return errorNotImpl('array.sdiff');
        },
    },
};