/**
 * Copyright (c) Whales Corp.
 * All Rights Reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { inspectSymbol } from "../inspect";
import { crc16 } from "../utils/crc16";

const bounceable_tag = 0x11;
const non_bounceable_tag = 0x51;
const test_flag = 0x80;

function parseFriendlyAddress(src: string | Buffer) {
    if (typeof src === "string" && !Address.isFriendly(src)) {
        throw new Error("Unknown address type");
    }

    const data = Buffer.isBuffer(src) ? src : Buffer.from(src, "base64");

    // 1byte tag + 1byte workchain + 32 bytes hash + 2 byte crc
    if (data.length !== 36) {
        throw new Error("Unknown address type: byte length is not equal to 36");
    }

    // Prepare data
    const addr = data.subarray(0, 34);
    const crc = data.subarray(34, 36);
    const calcedCrc = crc16(addr);
    if (!(calcedCrc[0] === crc[0] && calcedCrc[1] === crc[1])) {
        throw new Error("Invalid checksum: " + src);
    }

    // Parse tag
    let tag = addr[0];
    let isTestOnly = false;
    let isBounceable = false;
    if (tag & test_flag) {
        isTestOnly = true;
        tag = tag ^ test_flag;
    }
    if (tag !== bounceable_tag && tag !== non_bounceable_tag)
        throw "Unknown address tag";

    isBounceable = tag === bounceable_tag;

    let workchain = null;
    if (addr[1] === 0xff) {
        // TODO we should read signed integer here
        workchain = -1;
    } else {
        workchain = addr[1];
    }

    const hashPart = addr.subarray(2, 34);

    return { isTestOnly, isBounceable, workchain, hashPart };
}

export class Address {
    static isAddress(src: any): src is Address {
        return src instanceof Address;
    }

    static isFriendly(source: string) {
        // Check length
        if (source.length !== 48) {
            return false;
        }
        // Check if address is valid base64
        if (!/^[A-Za-z0-9+/_-]+$/.test(source)) {
            return false;
        }

        return true;
    }

    static isRaw(source: string) {
        // Check if has delimiter
        if (source.indexOf(":") === -1) {
            return false;
        }

        let [wc, hash] = source.split(":");

        // wc is not valid
        if (!Number.isInteger(parseFloat(wc))) {
            return false;
        }

        // hash is not valid
        if (!/[a-f0-9]+/.test(hash.toLowerCase())) {
            return false;
        }

        // has is not correct
        if (hash.length !== 64) {
            return false;
        }

        return true;
    }

    static normalize(source: string | Address) {
        if (typeof source === "string") {
            return Address.parse(source).toString();
        } else {
            return source.toString();
        }
    }

    static parse(source: string) {
        if (Address.isFriendly(source)) {
            return this.parseFriendly(source).address;
        } else if (Address.isRaw(source)) {
            return this.parseRaw(source);
        } else {
            throw new Error("Unknown address type: " + source);
        }
    }

    static parseRaw(source: string) {
        let workChain = parseInt(source.split(":")[0]);
        let hash = Buffer.from(source.split(":")[1], "hex");

        return new Address(workChain, hash);
    }

    static parseFriendly(source: string | Buffer) {
        if (Buffer.isBuffer(source)) {
            let r = parseFriendlyAddress(source);
            return {
                isBounceable: r.isBounceable,
                isTestOnly: r.isTestOnly,
                address: new Address(r.workchain, r.hashPart),
            };
        } else {
            let addr = source.replace(/\-/g, "+").replace(/_/g, "\/"); // Convert from url-friendly to true base64
            let r = parseFriendlyAddress(addr);
            return {
                isBounceable: r.isBounceable,
                isTestOnly: r.isTestOnly,
                address: new Address(r.workchain, r.hashPart),
            };
        }
    }

    readonly workChain: number;
    readonly hash: Buffer;

    constructor(workChain: number, hash: Buffer) {
        if (hash.length !== 32) {
            throw new Error("Invalid address hash length: " + hash.length);
        }

        this.workChain = workChain;
        this.hash = hash;
        Object.freeze(this);
    }

    toRawString = () => {
        return this.workChain + ":" + this.hash.toString("hex");
    };

    equals(src: Address) {
        if (src.workChain !== this.workChain) {
            return false;
        }
        return src.hash.equals(this.hash);
    }

    toRaw = () => {
        const addressWithChecksum = Buffer.alloc(36);
        addressWithChecksum.set(this.hash);
        addressWithChecksum.set(
            [this.workChain, this.workChain, this.workChain, this.workChain],
            32,
        );
        return addressWithChecksum;
    };

    toStringBuffer = (args?: { bounceable?: boolean; testOnly?: boolean }) => {
        let testOnly =
            args && args.testOnly !== undefined ? args.testOnly : false;
        let bounceable =
            args && args.bounceable !== undefined ? args.bounceable : true;

        let tag = bounceable ? bounceable_tag : non_bounceable_tag;
        if (testOnly) {
            tag |= test_flag;
        }

        const addr = Buffer.alloc(34);
        addr[0] = tag;
        addr[1] = this.workChain;
        addr.set(this.hash, 2);
        const addressWithChecksum = Buffer.alloc(36);
        addressWithChecksum.set(addr);
        addressWithChecksum.set(crc16(addr), 34);
        return addressWithChecksum;
    };

    toString = (args?: {
        urlSafe?: boolean;
        bounceable?: boolean;
        testOnly?: boolean;
    }) => {
        let urlSafe = args && args.urlSafe !== undefined ? args.urlSafe : true;
        let buffer = this.toStringBuffer(args);
        if (urlSafe) {
            return buffer
                .toString("base64")
                .replace(/\+/g, "-")
                .replace(/\//g, "_");
        } else {
            return buffer.toString("base64");
        }
    };

    private [inspectSymbol] = () => this.toString();
}

export function address(src: string) {
    return Address.parse(src);
}
