import { Tag } from './constants';
import { GenericEmbedded } from './embedded';
import { Encoder, Preservable } from './encoder';
import { Value } from './values';
import { Writer, PreserveWritable } from './writer';

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

export const IsPreservesBytes = Symbol.for('IsPreservesBytes');

export type BytesLike = Bytes | Uint8Array;

export class Bytes implements Preservable<any>, PreserveWritable<any> {
    readonly _view: Uint8Array;

    constructor(maybeByteIterable: any = new Uint8Array()) {
        if (Bytes.isBytes(maybeByteIterable)) {
            this._view = maybeByteIterable._view;
        } else if (ArrayBuffer.isView(maybeByteIterable)) {
            this._view = new Uint8Array(maybeByteIterable.buffer,
                                        maybeByteIterable.byteOffset,
                                        maybeByteIterable.byteLength);
        } else if (maybeByteIterable instanceof ArrayBuffer) {
            this._view = new Uint8Array(maybeByteIterable);
        } else if (typeof maybeByteIterable === 'string') {
            this._view = textEncoder.encode(maybeByteIterable);
        } else if (typeof maybeByteIterable === 'number') {
            this._view = new Uint8Array(maybeByteIterable);
        } else if (typeof maybeByteIterable.length === 'number') {
            this._view = Uint8Array.from(maybeByteIterable);
        } else {
            throw new TypeError("Attempt to initialize Bytes from unsupported value: " +
                maybeByteIterable);
        }
    }

    dataview(): DataView {
        return new DataView(this._view.buffer, this._view.byteOffset, this._view.byteLength);
    }

    get length(): number {
        return this._view.length;
    }

    static from(x: any): Bytes {
        return new Bytes(x);
    }

    static of(...bytes: number[]): Bytes {
        return new Bytes(Uint8Array.of(...bytes));
    }

    static fromHex(s: string): Bytes {
        if (s.length & 1) throw new Error("Cannot decode odd-length hexadecimal string");
        const len = s.length >> 1;
        const result = new Bytes(len);
        for (let i = 0; i < len; i++) {
            result._view[i] =
                (unhexDigit(s.charCodeAt(i << 1)) << 4) | unhexDigit(s.charCodeAt((i << 1) + 1));
        }
        return result;
    }

    static fromIO(io: string | BytesLike): string | Bytes {
        if (typeof io === 'string') return io;
        if (Bytes.isBytes(io)) return io;
        return new Bytes(io);
    }

    static toIO(b : string | BytesLike): string | Uint8Array {
        if (typeof b === 'string') return b;
        if (Bytes.isBytes(b)) return b._view;
        return b;
    }

    static concat = function (bss: BytesLike[]): Bytes {
        let len = 0;
        for (let i = 0; i < bss.length; i++) { len += underlying(bss[i]).length; }

        const result = new Bytes(len);
        let index = 0;
        for (let i = 0; i < bss.length; i++) {
            const bs = underlying(bss[i]);
            result._view.set(bs, index);
            index += bs.length;
        }
        return result;
    }

    get(index: number): number {
        return this._view[index];
    }

    equals(other: any): boolean {
        if (!Bytes.isBytes(other)) return false;
        if (other.length !== this.length) return false;
        const va = this._view;
        const vb = other._view;
        for (let i = 0; i < va.length; i++) {
            if (va[i] !== vb[i]) return false;
        }
        return true;
    }

    hashCode(): number {
        // Immutable.js uses this function for strings.
        const v = this._view;
        let hash = 0;
        for (let i = 0; i < v.length; i++) {
            hash = ((31 * hash) + v[i]) | 0;
        }
        return hash;
    }

    static compare(a: Bytes, b: Bytes): number {
        if (a < b) return -1;
        if (b < a) return 1;
        return 0;
    }

    static decodeUtf8(bs: Bytes | Uint8Array): string {
        return textDecoder.decode(underlying(bs));
    }

    fromUtf8(): string {
        return textDecoder.decode(this._view);
    }

    __as_preserve__<T = GenericEmbedded>(): Value<T> {
        return this;
    }

    static __from_preserve__<T>(v: Value<T>): undefined | Bytes {
        return Bytes.isBytes(v) ? v : void 0;
    }

    toHex(): string {
        var nibbles = [];
        for (let i = 0; i < this.length; i++) {
            nibbles.push(hexDigit(this._view[i] >> 4));
            nibbles.push(hexDigit(this._view[i] & 15));
        }
        return nibbles.join('');
    }

    valueOf(): string {
        // Defined mostly in order to get lexicographic comparison to "work reliably"
        return this.toHex();
    }

    __preserve_on__(encoder: Encoder<any>) {
        encoder.state.emitbyte(Tag.ByteString);
        encoder.state.varint(this.length);
        encoder.state.emitbytes(this._view);
    }

    __preserve_text_on__(w: Writer<any>) {
        w.state.writeBytes(this._view);
    }

    get [IsPreservesBytes](): boolean {
        return true;
    }

    static isBytes(x: any): x is Bytes {
        return !!x?.[IsPreservesBytes];
    }
}

export function hexDigit(n: number): string {
    return '0123456789abcdef'[n];
}

export function unhexDigit(asciiCode: number) {
    if (asciiCode >= 48 && asciiCode <= 57) return asciiCode - 48;
    if (asciiCode >= 97 && asciiCode <= 102) return asciiCode - 97 + 10;
    if (asciiCode >= 65 && asciiCode <= 70) return asciiCode - 65 + 10;
    throw new Error("Invalid hex digit: " + String.fromCharCode(asciiCode));
}

export function underlying(b: Bytes | Uint8Array): Uint8Array {
    return (b instanceof Uint8Array) ? b : b._view;
}

export function dataview(b: Bytes | DataView): DataView {
    return (b instanceof DataView) ? b : b.dataview();
}

// Uint8Array / TypedArray methods

export interface Bytes {
    entries(): IterableIterator<[number, number]>;
    every(predicate: (value: number, index: number, array: Uint8Array) => unknown,
          thisArg?: any): boolean;
    find(predicate: (value: number, index: number, obj: Uint8Array) => boolean,
         thisArg?: any): number;
    findIndex(predicate: (value: number, index: number, obj: Uint8Array) => boolean,
              thisArg?: any): number;
    forEach(callbackfn: (value: number, index: number, array: Uint8Array) => void,
            thisArg?: any): void;
    includes(searchElement: number, fromIndex?: number): boolean;
    indexOf(searchElement: number, fromIndex?: number): number;
    join(separator?: string): string;
    keys(): IterableIterator<number>;
    lastIndexOf(searchElement: number, fromIndex?: number): number;
    reduce(callbackfn: (previousValue: number,
                        currentValue: number,
                        currentIndex: number,
                        array: Uint8Array) => number,
           initialValue?: number): number;
    reduceRight(callbackfn: (previousValue: number,
                             currentValue: number,
                             currentIndex: number,
                             array: Uint8Array) => number,
                initialValue?: number): number;
    some(predicate: (value: number, index: number, array: Uint8Array) => unknown,
         thisArg?: any): boolean;
    toLocaleString(): string;
    values(): IterableIterator<number>;

    filter(predicate: (value: number, index: number, array: Uint8Array) => any,
           thisArg?: any): Bytes;
    map(callbackfn: (value: number, index: number, array: Uint8Array) => number,
        thisArg?: any): Bytes;
    slice(start?: number, end?: number): Bytes;
    subarray(begin?: number, end?: number): Bytes;

    reverse(): Bytes;
    sort(compareFn?: (a: number, b: number) => number): Bytes;

    [Symbol.iterator](): IterableIterator<number>;
}

(function () {
    for (const k of `entries every find findIndex forEach includes indexOf join
keys lastIndexOf reduce reduceRight some toLocaleString values`.split(/\s+/))
    {
        (Bytes as any).prototype[k] =
            function (...args: any[]) { return this._view[k](...args); };
    }

    for (const k of `filter map slice subarray`.split(/\s+/))
    {
        (Bytes as any).prototype[k] =
            function (...args: any[]) { return new Bytes(this._view[k](...args)); };
    }

    for (const k of `reverse sort`.split(/\s+/))
    {
        (Bytes as any).prototype[k] =
            function (...args: any[]) { return new Bytes(this._view.slice()[k](...args)); };
    }

    Bytes.prototype[Symbol.iterator] = function () { return this._view[Symbol.iterator](); };
})();
