import { isAnnotated } from './is';
import { Record, Tuple } from "./record";
import type { GenericEmbedded, Embedded, EmbeddedTypeEncode } from "./embedded";
import { Encoder, EncoderState } from "./encoder";
import type { Value } from "./values";
import { NUMBER_RE } from './reader';

export type Writable<T> =
    Value<T> | PreserveWritable<T> | Iterable<Value<T>> | ArrayBufferView;

export interface PreserveWritable<T> {
    __preserve_text_on__(writer: Writer<T>): void;
}

export function isPreserveWritable<T>(v: any): v is PreserveWritable<T> {
    return typeof v === 'object' && v !== null && '__preserve_text_on__' in v && typeof v.__preserve_text_on__ === 'function';
}

function isIterable<T>(v: any): v is Iterable<T> {
    return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function';
}

export type EmbeddedWriter<T> =
    { write(s: WriterState, v: T): void } | { toValue(v: T): Value<GenericEmbedded> };

export const genericEmbeddedTypeEncode: EmbeddedTypeEncode<GenericEmbedded> & EmbeddedWriter<GenericEmbedded> = {
    encode(s: EncoderState, v: GenericEmbedded): void {
        new Encoder(s, this).push(v.generic);
    },

    toValue(v: GenericEmbedded): Value<GenericEmbedded> {
        return v.generic;
    }
};

export const neverEmbeddedTypeEncode: EmbeddedTypeEncode<never> & EmbeddedWriter<never> = {
    encode(_s: EncoderState, _v: never): void {
        throw new Error("Embeddeds not permitted encoding Preserves document");
    },

    toValue(_v: never): Value<GenericEmbedded> {
        throw new Error("Embeddeds not permitted writing Preserves document");
    }
};

export interface WriterStateOptions {
    includeAnnotations?: boolean;
    indent?: number;
    maxBinaryAsciiLength?: number;
    maxBinaryAsciiProportion?: number;
}

export interface WriterOptions<T> extends WriterStateOptions {
    embeddedWrite?: EmbeddedWriter<T>;
}

export class WriterState {
    pieces: string[] = [];
    options: WriterStateOptions;
    indentDelta: string;
    indentCount = 0;

    constructor (options: WriterStateOptions) {
        this.options = options;
        this.indentDelta = ' '.repeat(options.indent ?? 0);
    }

    get isIndenting(): boolean {
        return this.indentDelta.length > 0;
    }

    get includeAnnotations(): boolean {
        return this.options.includeAnnotations ?? true;
    }

    writeIndent() {
        if (this.isIndenting) {
            this.pieces.push('\n');
            for (let i = 0; i < this.indentCount; i++) {
                this.pieces.push(this.indentDelta);
            }
        }
    }

    writeIndentSpace() {
        if (this.isIndenting) {
            this.writeIndent();
        } else {
            this.pieces.push(' ');
        }
    }

    escapeStringlikeChar(c: string, k: (c: string) => string = (c) => c): string {
        switch (c) {
            case "\\": return "\\\\";
            case "\x08": return "\\b";
            case "\x0c": return "\\f";
            case "\x0a": return "\\n";
            case "\x0d": return "\\r";
            case "\x09": return "\\t";
            default: return k(c);
        }
    }

    escapeStringlike(s: string, quoteChar: string): string {
        let buf = quoteChar;
        for (let c of s) {
            buf = buf + ((c === quoteChar) ? "\\" + quoteChar : this.escapeStringlikeChar(c));
        }
        return buf + quoteChar;
    }

    writeSeq<V>(opener: string, closer: string, vs: Iterable<V>, appender: (v: V) => void) {
        let iter = vs[Symbol.iterator]();
        this.pieces.push(opener);
        const first_i = iter.next();
        if (first_i.done !== true) {
            const first_v = first_i.value;
            const second_i = iter.next();
            if (second_i.done === true) {
                appender(first_v);
            } else {
                this.indentCount++;
                this.writeIndent();
                appender(first_v);
                this.writeIndentSpace();
                appender(second_i.value);
                let i: IteratorResult<V>;
                while ((i = iter.next()).done !== true) {
                    this.writeIndentSpace();
                    appender(i.value);
                }
                this.indentCount--;
                this.writeIndent();
            }
        }
        this.pieces.push(closer);
    }

    writeBytes(bs: Uint8Array) {
        const limit = this.options.maxBinaryAsciiLength ?? 1024;
        const proportion = this.options.maxBinaryAsciiProportion ?? 0.75;
        if (bs.length >= limit) {
            this.writeBase64(bs);
        } else {
            let count = 0;
            let sampleSize = Math.min(bs.length, limit);
            for (let i = 0; i < sampleSize; i++) {
                const b = bs[i];
                switch (b) {
                    case 9:
                    case 10:
                    case 13:
                        count++;
                        break;
                    default:
                        if (b >= 32 && b <= 126) {
                            count++;
                        }
                        break;
                }
            }
            if (sampleSize === 0 || (count / sampleSize) >= proportion) {
                this.writeBinaryStringlike(bs);
            } else {
                this.writeBase64(bs);
            }
        }
    }

    writeBase64(bs: Uint8Array) {
        this.pieces.push("#[", encodeBase64(bs), "]");
    }

    writeBinaryStringlike(bs: Uint8Array) {
        let buf = '#"';
        for (let b of bs) {
            if (b === 0x22) {
                buf = buf + '\\"';
            } else {
                buf = buf + this.escapeStringlikeChar(String.fromCharCode(b), c => {
                    if ((b >= 0x20 && b <= 0x7e) && (b !== 0x5c)) {
                        return c;
                    } else {
                        return '\\x' + ('0' + b.toString(16)).slice(-2);
                    }
                });
            }
        }
        this.pieces.push(buf + '"');
    }

    couldBeFlat<T>(vs: Writable<T>[]): boolean {
        let seenCompound = false;
        for (let v of vs) {
            if (Array.isArray(v) || Set.isSet(v) || Map.isMap(v)) {
                if (seenCompound) return false;
                seenCompound = true;
            }
            if (this.includeAnnotations && isAnnotated(v) && v.annotations.length > 1) {
                return false;
            }
        }
        return true;
    }
}

const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

export function encodeBase64(bs: Uint8Array): string {
    let s = '';
    let buffer = 0;
    let bitcount = 0;
    for (let b of bs) {
        buffer = ((buffer & 0x3f) << 8) | b;
        bitcount += 8;
        while (bitcount >= 6) {
            bitcount -= 6;
            const v = (buffer >> bitcount) & 0x3f;
            s = s + BASE64[v];
        }
    }
    if (bitcount > 0) {
        const v = (buffer << (6 - bitcount)) & 0x3f;
        s = s + BASE64[v];
    }
    return s;
}

export class Writer<T> {
    state: WriterState;
    embeddedWrite: EmbeddedWriter<T>;

    constructor(state: WriterState, embeddedWrite: EmbeddedWriter<T>);
    constructor(options?: WriterOptions<T>);
    constructor(
        state_or_options: (WriterState | WriterOptions<T>) = {},
        embeddedWrite?: EmbeddedWriter<T>
    ) {
        if (state_or_options instanceof WriterState) {
            this.state = state_or_options;
            this.embeddedWrite = embeddedWrite!;
        } else {
            this.state = new WriterState(state_or_options);
            this.embeddedWrite = state_or_options.embeddedWrite ?? neverEmbeddedTypeEncode;
        }
    }

    static stringify<T>(v: Writable<T>, options?: WriterOptions<T>): string {
        const w = new Writer(options);
        w.push(v);
        return w.contents();
    }

    contents(): string {
        return this.state.pieces.join('');
    }

    get includeAnnotations(): boolean {
        return this.state.includeAnnotations;
    }

    push(v: Writable<T>) {
        switch (typeof v) {
            case 'boolean':
                this.state.pieces.push(v ? '#t' : '#f');
                break;
            case 'string':
                this.state.pieces.push(this.state.escapeStringlike(v, '"'));
                break;
            case 'symbol': {
                const s = v.description!;
                // FIXME: This regular expression is conservatively correct, but Anglo-chauvinistic.
                if (/^[-a-zA-Z0-9~!$%^&*?_=+/.]+$/.exec(s) && !NUMBER_RE.exec(s)) {
                    this.state.pieces.push(s);
                } else {
                    this.state.pieces.push(this.state.escapeStringlike(s, '|'));
                }
                break;
            }
            case 'number':
                this.state.pieces.push('' + v);
                break;
            case 'object':
                if (isPreserveWritable<unknown>(v)) {
                    v.__preserve_text_on__(this);
                }
                else if (isPreserveWritable<T>(v)) {
                    v.__preserve_text_on__(this);
                }
                else if (ArrayBuffer.isView(v)) {
                    if (v instanceof Uint8Array) {
                        this.state.writeBytes(v);
                    } else {
                        const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
                        this.state.writeBytes(bs);
                    }
                }
                else if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
                    const flat = this.state.couldBeFlat(v);
                    this.state.pieces.push('<');
                    this.push(v.label);
                    if (!flat) this.state.indentCount++;
                    for (let i of v) {
                        if (flat) {
                            this.state.pieces.push(' ');
                        } else {
                            this.state.writeIndentSpace();
                        }
                        this.push(i);
                    }
                    if (!flat) this.state.indentCount--;
                    this.state.pieces.push('>');
                }
                else if (isIterable(v)) {
                    this.state.writeSeq('[', ']', v, vv => this.push(vv));
                }
                else {
                    ((v: Embedded<T>) => {
                        this.state.pieces.push('#!');
                        if ('write' in this.embeddedWrite) {
                            this.embeddedWrite.write(this.state, v.embeddedValue);
                        } else {
                            new Writer(this.state, genericEmbeddedTypeEncode)
                                .push(this.embeddedWrite.toValue(v.embeddedValue));
                        }
                    })(v);
                }
                break;
            default:
                throw new Error(`Internal error: unhandled in Preserves Writer.push for ${v}`);
        }
        return this; // for chaining
    }
}
